From f3287422b3f89c2864e452b07a1c8d39d12f7951 Mon Sep 17 00:00:00 2001 From: Brayan Almonte Date: Thu, 11 Jul 2024 16:00:08 -0400 Subject: [PATCH 01/78] feat(app): Add Aspirate and Dispense advanced settings for Quick Transfer. (#15593) Co-authored-by: smb2268 --- .../localization/en/quick_transfer.json | 78 ++- .../QuickTransferAdvancedSettings/AirGap.tsx | 213 +++++++ .../QuickTransferAdvancedSettings/BlowOut.tsx | 220 +++++++ .../QuickTransferAdvancedSettings/Delay.tsx | 264 ++++++++ .../FlowRate.tsx | 29 +- .../QuickTransferAdvancedSettings/Mix.tsx | 243 ++++++++ .../PipettePath.tsx | 232 +++++-- .../TipPosition.tsx | 144 +++++ .../TouchTip.tsx | 209 +++++++ .../QuickTransferAdvancedSettings/index.tsx | 581 +++++++++++++++++- .../__tests__/SummaryAndSettings.test.tsx | 2 +- .../utils/getInitialSummaryState.test.ts | 6 +- .../organisms/QuickTransferFlow/reducers.ts | 22 +- app/src/organisms/QuickTransferFlow/types.ts | 8 +- .../utils/createQuickTransferFile.ts | 2 +- .../utils/generateQuickTransferArgs.ts | 114 ++-- .../utils/getInitialSummaryState.ts | 6 +- 17 files changed, 2236 insertions(+), 137 deletions(-) create mode 100644 app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/AirGap.tsx create mode 100644 app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/BlowOut.tsx create mode 100644 app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/Delay.tsx create mode 100644 app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/Mix.tsx create mode 100644 app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/TipPosition.tsx create mode 100644 app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/TouchTip.tsx diff --git a/app/src/assets/localization/en/quick_transfer.json b/app/src/assets/localization/en/quick_transfer.json index 436fb81247f..3a0d81ffe79 100644 --- a/app/src/assets/localization/en/quick_transfer.json +++ b/app/src/assets/localization/en/quick_transfer.json @@ -1,14 +1,31 @@ { - "add_or_remove_columns": "add or remove columns", "add_or_remove": "add or remove", + "add_or_remove_columns": "add or remove columns", + "advanced_setting_disabled": "Advanced setting disabled for this transfer", "advanced_settings": "Advanced settings", + "air_gap": "Air gap", + "air_gap_before_aspirating": "Air gap before aspirating", + "air_gap_before_dispensing": "Air gap before dispensing", + "air_gap_value": "{{volume}} µL", + "air_gap_volume_µL": "Air gap volume (µL)", "all": "All labware", "always": "Before every aspirate", - "aspirate_volume": "Aspirate volume per well", - "aspirate_volume_µL": "Aspirate volume per well (µL)", "aspirate_flow_rate": "Aspirate flow rate", "aspirate_flow_rate_µL": "Aspirate flow rate (µL/s)", - "flow_rate_value": "{{flow_rate}} µL/s", + "aspirate_settings": "Aspirate Settings", + "aspirate_tip_position": "Aspirate tip position", + "aspirate_volume": "Aspirate volume per well", + "aspirate_volume_µL": "Aspirate volume per well (µL)", + "blow_out": "Blowout", + "blow_out_after_dispensing": "Blowout after dispensing", + "blow_out_destination_well": "Destination well", + "blow_out_into_destination_well": "into destination well", + "blow_out_into_source_well": "into source well", + "blow_out_into_trash_bin": "into trash bin", + "blow_out_into_waste_chute": "into waste chute", + "blow_out_source_well": "Source well", + "blow_out_trash_bin": "Trash bin", + "blow_out_waste_chute": "Waste chute", "both_mounts": "Left + Right Mount", "change_tip": "Change tip", "character_limit_error": "Character limit exceeded", @@ -16,30 +33,50 @@ "columns": "columns", "create_new_transfer": "Create new quick transfer", "create_transfer": "Create transfer", + "delay": "Delay", + "delay_before_aspirating": "Delay before aspirating", + "delay_before_dispensing": "Delay before dispensing", + "delay_duration_s": "Delay duration (seconds)", + "delay_position_mm": "Delay position from bottom of well (mm)", + "delay_value": "{{delay}}s, {{position}} mm from bottom", "create_to_get_started": "Create a new quick transfer to get started.", "delete_this_transfer": "Delete this quick transfer?", "delete_transfer": "Delete quick transfer", "deleted_transfer": "Deleted quick transfer", "destination": "Destination", "destination_labware": "Destination labware", - "dispense_volume": "Dispense volume per well", - "dispense_volume_µL": "Dispense volume per well (µL)", "dispense_flow_rate": "Dispense flow rate", "dispense_flow_rate_µL": "Dispense flow rate (µL/s)", + "dispense_settings": "Dispense Settings", + "dispense_tip_position": "Dispense tip position", + "dispense_volume": "Dispense volume per well", + "dispense_volume_µL": "Dispense volume per well (µL)", + "disposal_volume_µL": "Disposal volume (µL)", + "distance_bottom_of_well_mm": "Distance from bottom of well (mm)", "enter_characters": "Enter up to 60 characters", "error_analyzing": "An error occurred while attempting to analyze {{transferName}}.", "exit_quick_transfer": "Exit quick transfer?", + "flow_rate_value": "{{flow_rate}} µL/s", "failed_analysis": "failed analysis", "grid": "grid", "grids": "grids", + "labware": "Labware", "learn_more": "Learn more", "left_mount": "Left Mount", "lose_all_progress": "You will lose all progress on this quick transfer.", + "mix": "Mix", + "mix_before_aspirating": "Mix before aspirating", + "mix_before_dispensing": "Mix before dispensing", + "mix_repetitions": "Mix repetitions", + "mix_value": "{{volume}} µL, {{reps}} times", + "mix_volume_µL": "Mix volume (µL)", "name_your_transfer": "Name your quick transfer", "none_to_show": "No quick transfers to show!", "number_wells_selected_error_learn_more": "Quick transfers with multiple source {{selectionUnits}} can either be one-to-one (select {{wellCount}} destination {{selectionUnits}} for this transfer) or consolidate (select 1 destination {{selectionUnit}}).", "number_wells_selected_error_message": "Select 1 or {{wellCount}} {{selectionUnits}} to make this transfer.", "once": "Once at the start of the transfer", + "option_disabled": "Disabled", + "option_enabled": "Enabled", "overview": "Overview", "perDest": "Per destination well", "perSource": "Per source well", @@ -47,21 +84,22 @@ "pinned_transfer": "Pinned quick transfer", "pinned_transfers": "Pinned Quick Transfers", "pipette": "Pipette", + "pipette_currently_attached": "Quick transfer options depend on the pipettes currently attached to your robot.", "pipette_path": "Pipette path", - "pipette_path_single": "Single transfers", "pipette_path_multi_aspirate": "Multi-aspirate", - "pipette_path_multi_dispense": "Multi-dispense", + "pipette_path_multi_dispense": "Multi-dispense, {{volume}} disposal volume, blowout into {{blowOutLocation}}", + "pipette_path_single": "Single transfers", + "pre_wet_tip": "Pre-wet tip", "quick_transfer": "Quick transfer", "quick_transfer_volume": "Quick Transfer {{volume}}µL", - "right_mount": "Right Mount", "reservoir": "Reservoirs", + "right_mount": "Right Mount", "run_now": "Run now", "run_transfer": "Run quick transfer", "run_quick_transfer_now": "Do you want to run your quick transfer now?", "save": "Save", - "save_to_run_later": "Save your quick transfer to run it in the future.", "save_for_later": "Save for later", - "source": "Source", + "save_to_run_later": "Save your quick transfer to run it in the future.", "select_attached_pipette": "Select attached pipette", "select_by": "select by", "select_dest_labware": "Select destination labware", @@ -72,13 +110,22 @@ "set_aspirate_volume": "Set aspirate volume", "set_dispense_volume": "Set dispense volume", "set_transfer_volume": "Set transfer volume", + "source": "Source", "source_labware": "Source labware", "source_labware_d2": "Source labware in D2", "starting_well": "starting well", - "use_deck_slots": "Quick transfers use deck slots B2-D2. These slots hold a tip rack, a source labware, and a destination labware.Make sure that your deck configuration is up to date to avoid collisions.", "tip_drop_location": "Tip drop location", "tip_management": "Tip management", + "tip_position": "Tip position", + "tip_position_value": "{{position}} mm from the bottom", "tip_rack": "Tip rack", + "touch_tip": "Touch tip", + "touch_tip_before_aspirating": "Touch tip before aspirating", + "touch_tip_before_dispensing": "Touch tip before dispensing", + "touch_tip_position_mm": "Touch tip position from bottom of well (mm)", + "touch_tip_value": "{{position}} mm from bottom", + "use_deck_slots": "Quick transfers use deck slots B2-D2. These slots hold a tip rack, a source labware, and a destination labware.Make sure that your deck configuration is up to date to avoid collisions.", + "value_out_of_range": "Value must be between {{min}}-{{max}}", "too_many_pins_body": "Remove a quick transfer in order to add more transfers to your pinned list.", "too_many_pins_header": "You've hit your max!", "transfer_analysis_failed": "quick transfer analysis failed", @@ -90,15 +137,12 @@ "unpinned_transfer": "Unpinned quick transfer", "volume_per_well": "Volume per well", "volume_per_well_µL": "Volume per well (µL)", - "value_out_of_range": "Value must be between {{min}}-{{max}}", - "labware": "Labware", - "pipette_currently_attached": "Quick transfer options depend on the pipettes currently attached to your robot.", "wasteChute": "Waste chute", "wasteChute_location": "Waste chute in {{slotName}}", + "well": "well", "wellPlate": "Well plates", - "well_selection": "Well selection", "well_ratio": "Quick transfers with multiple source wells can either be one-to-one (select {{wells}} for this transfer) or consolidate (select 1 destination well).", - "well": "well", + "well_selection": "Well selection", "wells": "wells", "will_be_deleted": " will be permanently deleted." } diff --git a/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/AirGap.tsx b/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/AirGap.tsx new file mode 100644 index 00000000000..1101cf3b9e3 --- /dev/null +++ b/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/AirGap.tsx @@ -0,0 +1,213 @@ +import * as React from 'react' +import { useTranslation } from 'react-i18next' +import { createPortal } from 'react-dom' +import { + Flex, + SPACING, + DIRECTION_COLUMN, + POSITION_FIXED, + COLORS, + ALIGN_CENTER, +} from '@opentrons/components' +import { getTopPortalEl } from '../../../App/portal' +import { LargeButton } from '../../../atoms/buttons' +import { ChildNavigation } from '../../ChildNavigation' +import { InputField } from '../../../atoms/InputField' +import { ACTIONS } from '../constants' + +import type { + QuickTransferSummaryState, + QuickTransferSummaryAction, + FlowRateKind, +} from '../types' +import { i18n } from '../../../i18n' +import { NumericalKeyboard } from '../../../atoms/SoftwareKeyboard' + +interface AirGapProps { + onBack: () => void + state: QuickTransferSummaryState + dispatch: React.Dispatch + kind: FlowRateKind +} + +export function AirGap(props: AirGapProps): JSX.Element { + const { kind, onBack, state, dispatch } = props + const { t } = useTranslation('quick_transfer') + const keyboardRef = React.useRef(null) + + const [airGapEnabled, setAirGapEnabled] = React.useState( + kind === 'aspirate' + ? state.airGapAspirate != null + : state.airGapDispense != null + ) + const [currentStep, setCurrentStep] = React.useState(1) + const [volume, setVolume] = React.useState( + kind === 'aspirate' + ? state.airGapAspirate ?? null + : state.airGapDispense ?? null + ) + + const action = + kind === 'aspirate' + ? ACTIONS.SET_AIR_GAP_ASPIRATE + : ACTIONS.SET_AIR_GAP_DISPENSE + + const enableAirGapDisplayItems = [ + { + option: true, + description: t('option_enabled'), + onClick: () => { + setAirGapEnabled(true) + }, + }, + { + option: false, + description: t('option_disabled'), + onClick: () => { + setAirGapEnabled(false) + }, + }, + ] + + const handleClickBackOrExit = (): void => { + currentStep > 1 ? setCurrentStep(currentStep - 1) : onBack() + } + + const handleClickSaveOrContinue = (): void => { + if (currentStep === 1) { + if (airGapEnabled) { + setCurrentStep(currentStep + 1) + } else { + dispatch({ type: action, volume: undefined }) + onBack() + } + } else if (currentStep === 2) { + dispatch({ type: action, volume: volume ?? undefined }) + onBack() + } + } + + const setSaveOrContinueButtonText = + airGapEnabled && currentStep < 2 ? t('shared:continue') : t('shared:save') + + const maxPipetteVolume = Object.values(state.pipette.liquids)[0].maxVolume + const tipVolume = Object.values(state.tipRack.wells)[0].totalLiquidVolume + + // dispense air gap is performed whenever a tip is on its way to the trash, so + // we can have the max be at the max tip capacity + let maxAvailableCapacity = Math.min(maxPipetteVolume, tipVolume) + + // for aspirate, air gap behaves differently depending on the path + if (kind === 'aspirate') { + if (state.path === 'single') { + // for a single path, air gap capacity is just the difference between the + // pipette/tip capacity and the volume per well + maxAvailableCapacity = + Math.min(maxPipetteVolume, tipVolume) - state.volume + } else if (state.path === 'multiAspirate') { + // an aspirate air gap for multi aspirate will aspirate an air gap + // after each aspirate action, so we need to halve the available capacity for single path + // to get the amount available, assuming a min of 2 aspirates per dispense + maxAvailableCapacity = + (Math.min(maxPipetteVolume, tipVolume) - state.volume) / 2 + } else { + // aspirate air gap for multi dispense occurs once per asprirate and + // available volume is max capacity - volume*3 assuming a min of 2 dispenses + // per aspirate plus 1x the volume for disposal + maxAvailableCapacity = + Math.min(maxPipetteVolume, tipVolume) - state.volume / 3 + } + } + + const volumeRange = { min: 1, max: Math.floor(maxAvailableCapacity) } + const volumeError = + volume !== null && (volume < volumeRange.min || volume > volumeRange.max) + ? t(`value_out_of_range`, { + min: volumeRange.min, + max: volumeRange.max, + }) + : null + + let buttonIsDisabled = false + if (currentStep === 2) { + buttonIsDisabled = volume == null || volumeError != null + } + + return createPortal( + + + {currentStep === 1 ? ( + + {enableAirGapDisplayItems.map(displayItem => ( + + ))} + + ) : null} + {currentStep === 2 ? ( + + + + + + { + setVolume(Number(e)) + }} + /> + + + ) : null} + , + getTopPortalEl() + ) +} diff --git a/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/BlowOut.tsx b/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/BlowOut.tsx new file mode 100644 index 00000000000..a2d536459c0 --- /dev/null +++ b/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/BlowOut.tsx @@ -0,0 +1,220 @@ +import * as React from 'react' +import { useTranslation } from 'react-i18next' +import { createPortal } from 'react-dom' +import { + Flex, + SPACING, + DIRECTION_COLUMN, + POSITION_FIXED, + COLORS, +} from '@opentrons/components' +import { + WASTE_CHUTE_FIXTURES, + FLEX_SINGLE_SLOT_BY_CUTOUT_ID, + TRASH_BIN_ADAPTER_FIXTURE, +} from '@opentrons/shared-data' +import { getTopPortalEl } from '../../../App/portal' +import { LargeButton } from '../../../atoms/buttons' +import { useNotifyDeckConfigurationQuery } from '../../../resources/deck_configuration' +import { ChildNavigation } from '../../ChildNavigation' +import { ACTIONS } from '../constants' + +import type { DeckConfiguration } from '@opentrons/shared-data' +import type { + QuickTransferSummaryState, + QuickTransferSummaryAction, + FlowRateKind, + BlowOutLocation, + TransferType, +} from '../types' +import { i18n } from '../../../i18n' + +interface BlowOutProps { + onBack: () => void + state: QuickTransferSummaryState + dispatch: React.Dispatch + kind: FlowRateKind +} + +export const useBlowOutLocationOptions = ( + deckConfig: DeckConfiguration, + transferType: TransferType +): Array<{ location: BlowOutLocation; description: string }> => { + const { t } = useTranslation('quick_transfer') + + const trashLocations = deckConfig.filter( + cutoutConfig => + WASTE_CHUTE_FIXTURES.includes(cutoutConfig.cutoutFixtureId) || + TRASH_BIN_ADAPTER_FIXTURE === cutoutConfig.cutoutFixtureId + ) + + // add trash bin in A3 if no trash or waste chute configured + if (trashLocations.length === 0) { + trashLocations.push({ + cutoutId: 'cutoutA3', + cutoutFixtureId: TRASH_BIN_ADAPTER_FIXTURE, + }) + } + const blowOutLocationItems: Array<{ + location: BlowOutLocation + description: string + }> = [] + if (transferType !== 'distribute') { + blowOutLocationItems.push({ + location: 'source_well', + description: t('blow_out_source_well'), + }) + } + if (transferType !== 'consolidate') { + blowOutLocationItems.push({ + location: 'dest_well', + description: t('blow_out_destination_well'), + }) + } + trashLocations.forEach(location => { + blowOutLocationItems.push({ + location, + description: + location.cutoutFixtureId === TRASH_BIN_ADAPTER_FIXTURE + ? t('trashBin_location', { + slotName: FLEX_SINGLE_SLOT_BY_CUTOUT_ID[location.cutoutId], + }) + : t('wasteChute_location', { + slotName: FLEX_SINGLE_SLOT_BY_CUTOUT_ID[location.cutoutId], + }), + }) + }) + return blowOutLocationItems +} + +export function BlowOut(props: BlowOutProps): JSX.Element { + const { onBack, state, dispatch } = props + const { t } = useTranslation('quick_transfer') + const deckConfig = useNotifyDeckConfigurationQuery().data ?? [] + + const [isBlowOutEnabled, setisBlowOutEnabled] = React.useState( + state.blowOut != null + ) + const [currentStep, setCurrentStep] = React.useState(1) + const [blowOutLocation, setBlowOutLocation] = React.useState< + BlowOutLocation | undefined + >(state.blowOut) + + const enableBlowOutDisplayItems = [ + { + value: true, + description: t('option_enabled'), + onClick: () => { + setisBlowOutEnabled(true) + }, + }, + { + value: false, + description: t('option_disabled'), + onClick: () => { + setisBlowOutEnabled(false) + }, + }, + ] + + const blowOutLocationItems = useBlowOutLocationOptions( + deckConfig, + state.transferType + ) + + const handleClickBackOrExit = (): void => { + currentStep > 1 ? setCurrentStep(currentStep - 1) : onBack() + } + + const handleClickSaveOrContinue = (): void => { + if (currentStep === 1) { + if (!isBlowOutEnabled) { + dispatch({ + type: ACTIONS.SET_BLOW_OUT, + location: undefined, + }) + onBack() + } else { + setCurrentStep(currentStep + 1) + } + } else { + dispatch({ + type: ACTIONS.SET_BLOW_OUT, + location: blowOutLocation, + }) + onBack() + } + } + + const saveOrContinueButtonText = + isBlowOutEnabled && currentStep < 2 + ? t('shared:continue') + : t('shared:save') + + let buttonIsDisabled = false + if (currentStep === 2) { + buttonIsDisabled = blowOutLocation == null + } + + return createPortal( + + + {currentStep === 1 ? ( + + {enableBlowOutDisplayItems.map(displayItem => ( + { + setisBlowOutEnabled(displayItem.value) + }} + buttonText={displayItem.description} + /> + ))} + + ) : null} + {currentStep === 2 ? ( + + {blowOutLocationItems.map(blowOutLocationItem => ( + { + setBlowOutLocation( + blowOutLocationItem.location as BlowOutLocation + ) + }} + buttonText={blowOutLocationItem.description} + /> + ))} + + ) : null} + , + getTopPortalEl() + ) +} diff --git a/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/Delay.tsx b/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/Delay.tsx new file mode 100644 index 00000000000..4b8414addbb --- /dev/null +++ b/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/Delay.tsx @@ -0,0 +1,264 @@ +import * as React from 'react' +import { useTranslation } from 'react-i18next' +import { createPortal } from 'react-dom' +import { + Flex, + SPACING, + DIRECTION_COLUMN, + POSITION_FIXED, + COLORS, + ALIGN_CENTER, +} from '@opentrons/components' +import { getTopPortalEl } from '../../../App/portal' +import { LargeButton } from '../../../atoms/buttons' +import { ChildNavigation } from '../../ChildNavigation' +import { InputField } from '../../../atoms/InputField' +import { ACTIONS } from '../constants' + +import type { + QuickTransferSummaryState, + QuickTransferSummaryAction, + FlowRateKind, +} from '../types' +import { i18n } from '../../../i18n' +import { NumericalKeyboard } from '../../../atoms/SoftwareKeyboard' + +interface DelayProps { + onBack: () => void + state: QuickTransferSummaryState + dispatch: React.Dispatch + kind: FlowRateKind +} + +export function Delay(props: DelayProps): JSX.Element { + const { kind, onBack, state, dispatch } = props + const { t } = useTranslation('quick_transfer') + const keyboardRef = React.useRef(null) + + const [currentStep, setCurrentStep] = React.useState(1) + const [delayIsEnabled, setDelayIsEnabled] = React.useState( + kind === 'aspirate' + ? state.delayAspirate != null + : state.delayDispense != null + ) + const [delayDuration, setDelayDuration] = React.useState( + kind === 'aspirate' + ? state.delayAspirate?.delayDuration ?? null + : state.delayDispense?.delayDuration ?? null + ) + const [position, setPosition] = React.useState( + kind === 'aspirate' + ? state.delayAspirate?.positionFromBottom ?? null + : state.delayDispense?.positionFromBottom ?? null + ) + + const action = + kind === 'aspirate' + ? ACTIONS.SET_DELAY_ASPIRATE + : ACTIONS.SET_DELAY_DISPENSE + + const delayEnabledDisplayItems = [ + { + option: true, + description: t('option_enabled'), + onClick: () => { + setDelayIsEnabled(true) + }, + }, + { + option: false, + description: t('option_disabled'), + onClick: () => { + setDelayIsEnabled(false) + }, + }, + ] + + const handleClickBackOrExit = (): void => { + currentStep > 1 ? setCurrentStep(currentStep - 1) : onBack() + } + + const handleClickSaveOrContinue = (): void => { + if (currentStep === 1) { + if (!delayIsEnabled) { + dispatch({ + type: action, + delaySettings: undefined, + }) + onBack() + } else { + setCurrentStep(2) + } + } else if (currentStep === 2) { + setCurrentStep(3) + } else { + if (delayDuration != null && position != null) { + dispatch({ + type: action, + delaySettings: { + delayDuration, + positionFromBottom: position, + }, + }) + } + onBack() + } + } + + const setSaveOrContinueButtonText = + delayIsEnabled && currentStep < 3 ? t('shared:continue') : t('shared:save') + + let wellHeight = 1 + if (kind === 'aspirate') { + wellHeight = Math.max( + ...state.sourceWells.map(well => + state.source != null ? state.source.wells[well].depth : 0 + ) + ) + } else if (kind === 'dispense') { + const destLabwareDefinition = + state.destination === 'source' ? state.source : state.destination + wellHeight = Math.max( + ...state.destinationWells.map(well => + destLabwareDefinition != null + ? destLabwareDefinition.wells[well].depth + : 0 + ) + ) + } + + // the maxiumum allowed position for delay is 2x the height of the well + const positionRange = { min: 1, max: Math.floor(wellHeight * 2) } + const positionError = + position != null && + (position < positionRange.min || position > positionRange.max) + ? t(`value_out_of_range`, { + min: positionRange.min, + max: positionRange.max, + }) + : null + + let buttonIsDisabled = false + if (currentStep === 2) { + buttonIsDisabled = delayDuration == null + } else if (currentStep === 3) { + buttonIsDisabled = positionError != null || position == null + } + + return createPortal( + + + {currentStep === 1 ? ( + + {delayEnabledDisplayItems.map(displayItem => ( + + ))} + + ) : null} + {currentStep === 2 ? ( + + + + + + { + setDelayDuration(Number(e)) + }} + /> + + + ) : null} + {currentStep === 3 ? ( + + + + + + { + setPosition(Number(e)) + }} + /> + + + ) : null} + , + getTopPortalEl() + ) +} diff --git a/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/FlowRate.tsx b/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/FlowRate.tsx index d649dd38eb9..14d84dcd292 100644 --- a/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/FlowRate.tsx +++ b/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/FlowRate.tsx @@ -39,12 +39,8 @@ export function FlowRateEntry(props: FlowRateEntryProps): JSX.Element { const { i18n, t } = useTranslation(['quick_transfer', 'shared']) const keyboardRef = React.useRef(null) - const [flowRate, setFlowRate] = React.useState( - kind === 'aspirate' - ? state.aspirateFlowRate - : kind === 'dispense' - ? state.dispenseFlowRate - : null + const [flowRate, setFlowRate] = React.useState( + kind === 'aspirate' ? state.aspirateFlowRate : state.dispenseFlowRate ) // TODO (ba, 2024-07-02): use the pipette name once we add it to the v2 spec @@ -66,28 +62,27 @@ export function FlowRateEntry(props: FlowRateEntryProps): JSX.Element { LOW_VOLUME_PIPETTES.includes(pipetteName) ? liquidSpecs.lowVolumeDefault.supportedTips[tipType] : liquidSpecs.default.supportedTips[tipType] - const minFlowRate = 0.1 - const maxFlowRate = flowRatesForSupportedTip?.uiMaxFlowRate ?? 0 + const minFlowRate = 1 + const maxFlowRate = Math.floor(flowRatesForSupportedTip?.uiMaxFlowRate ?? 0) + + const flowRateAction = + kind === 'aspirate' + ? ACTIONS.SET_ASPIRATE_FLOW_RATE + : ACTIONS.SET_DISPENSE_FLOW_RATE let headerCopy: string = '' let textEntryCopy: string = '' - let flowRateAction: - | typeof ACTIONS.SET_ASPIRATE_FLOW_RATE - | typeof ACTIONS.SET_DISPENSE_FLOW_RATE - | null = null if (kind === 'aspirate') { headerCopy = t('aspirate_flow_rate') textEntryCopy = t('aspirate_flow_rate_µL') - flowRateAction = ACTIONS.SET_ASPIRATE_FLOW_RATE } else if (kind === 'dispense') { headerCopy = t('dispense_flow_rate') textEntryCopy = t('dispense_flow_rate_µL') - flowRateAction = ACTIONS.SET_DISPENSE_FLOW_RATE } const handleClickSave = (): void => { // the button will be disabled if this values is null - if (flowRate != null && flowRateAction != null) { + if (flowRate != null) { dispatch({ type: flowRateAction, rate: flowRate, @@ -97,7 +92,7 @@ export function FlowRateEntry(props: FlowRateEntryProps): JSX.Element { } const error = - flowRate !== null && (flowRate < minFlowRate || flowRate > maxFlowRate) + flowRate != null && (flowRate < minFlowRate || flowRate > maxFlowRate) ? t(`value_out_of_range`, { min: minFlowRate, max: maxFlowRate, @@ -112,7 +107,7 @@ export function FlowRateEntry(props: FlowRateEntryProps): JSX.Element { onClickBack={onBack} onClickButton={handleClickSave} top={SPACING.spacing8} - buttonIsDisabled={error != null || flowRate === null} + buttonIsDisabled={error != null || flowRate == null} /> void + state: QuickTransferSummaryState + dispatch: React.Dispatch + kind: FlowRateKind +} + +export function Mix(props: MixProps): JSX.Element { + const { kind, onBack, state, dispatch } = props + const { t } = useTranslation('quick_transfer') + const keyboardRef = React.useRef(null) + + const [mixIsEnabled, setMixIsEnabled] = React.useState( + kind === 'aspirate' + ? state.mixOnAspirate != null + : state.mixOnDispense != null + ) + const [currentStep, setCurrentStep] = React.useState(1) + const [mixVolume, setMixVolume] = React.useState( + kind === 'aspirate' + ? state.mixOnAspirate?.mixVolume ?? null + : state.mixOnDispense?.mixVolume ?? null + ) + const [mixReps, setMixReps] = React.useState( + kind === 'aspirate' + ? state.mixOnAspirate?.repititions ?? null + : state.mixOnDispense?.repititions ?? null + ) + + const mixAction = + kind === 'aspirate' + ? ACTIONS.SET_MIX_ON_ASPIRATE + : ACTIONS.SET_MIX_ON_DISPENSE + + const enableMixDisplayItems = [ + { + option: true, + description: t('option_enabled'), + onClick: () => { + setMixIsEnabled(true) + }, + }, + { + option: false, + description: t('option_disabled'), + onClick: () => { + setMixIsEnabled(false) + }, + }, + ] + + const handleClickBackOrExit = (): void => { + currentStep > 1 ? setCurrentStep(currentStep - 1) : onBack() + } + + const handleClickSaveOrContinue = (): void => { + if (currentStep === 1) { + if (!mixIsEnabled) { + dispatch({ + type: mixAction, + mixSettings: undefined, + }) + } else { + setCurrentStep(2) + } + } else if (currentStep === 2) { + setCurrentStep(3) + } else if (currentStep === 3) { + if (mixVolume != null && mixReps != null) { + dispatch({ + type: mixAction, + mixSettings: { mixVolume, repititions: mixReps }, + }) + } + onBack() + } + } + + const setSaveOrContinueButtonText = + mixIsEnabled && currentStep < 3 ? t('shared:continue') : t('shared:save') + + const maxPipetteVolume = Object.values(state.pipette.liquids)[0].maxVolume + const tipVolume = Object.values(state.tipRack.wells)[0].totalLiquidVolume + + const volumeRange = { min: 1, max: Math.min(maxPipetteVolume, tipVolume) } + const volumeError = + mixVolume != null && + (mixVolume < volumeRange.min || mixVolume > volumeRange.max) + ? t(`value_out_of_range`, { + min: volumeRange.min, + max: volumeRange.max, + }) + : null + + let buttonIsDisabled = false + if (currentStep === 2) { + buttonIsDisabled = mixVolume == null || volumeError != null + } else if (currentStep === 3) { + buttonIsDisabled = mixReps == null + } + + return createPortal( + + + {currentStep === 1 ? ( + + {enableMixDisplayItems.map(displayItem => ( + + ))} + + ) : null} + {currentStep === 2 ? ( + + + + + + { + setMixVolume(Number(e)) + }} + /> + + + ) : null} + {currentStep === 3 ? ( + + + + + + { + setMixReps(Number(e)) + }} + /> + + + ) : null} + , + getTopPortalEl() + ) +} diff --git a/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/PipettePath.tsx b/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/PipettePath.tsx index dd0964ca181..b93cee1a9d7 100644 --- a/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/PipettePath.tsx +++ b/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/PipettePath.tsx @@ -7,16 +7,25 @@ import { DIRECTION_COLUMN, POSITION_FIXED, COLORS, + ALIGN_CENTER, } from '@opentrons/components' +import { useNotifyDeckConfigurationQuery } from '../../../resources/deck_configuration' import { getTopPortalEl } from '../../../App/portal' import { LargeButton } from '../../../atoms/buttons' import { ChildNavigation } from '../../ChildNavigation' +import { useBlowOutLocationOptions } from './BlowOut' +import { getVolumeRange } from '../utils' import type { PathOption, QuickTransferSummaryState, QuickTransferSummaryAction, + BlowOutLocation, } from '../types' +import { ACTIONS } from '../constants' +import { i18n } from '../../../i18n' +import { InputField } from '../../../atoms/InputField' +import { NumericalKeyboard } from '../../../atoms/SoftwareKeyboard' interface PipettePathProps { onBack: () => void @@ -27,72 +36,195 @@ interface PipettePathProps { export function PipettePath(props: PipettePathProps): JSX.Element { const { onBack, state, dispatch } = props const { t } = useTranslation('quick_transfer') + const keyboardRef = React.useRef(null) + const deckConfig = useNotifyDeckConfigurationQuery().data ?? [] - const allowedPipettePathOptions: PathOption[] = ['single'] - if (state.sourceWells.length === 1 && state.destinationWells.length > 1) { - allowedPipettePathOptions.push('multiDispense') + const [selectedPath, setSelectedPath] = React.useState(state.path) + const [currentStep, setCurrentStep] = React.useState(1) + const [blowOutLocation, setBlowOutLocation] = React.useState< + BlowOutLocation | undefined + >(state.blowOut) + + const [disposalVolume, setDisposalVolume] = React.useState( + state.volume + ) + const volumeLimits = getVolumeRange(state) + + const allowedPipettePathOptions: Array<{ + pathOption: PathOption + description: string + }> = [{ pathOption: 'single', description: t('pipette_path_single') }] + if ( + state.transferType === 'distribute' && + volumeLimits.max >= state.volume * 3 + ) { + // we have the capacity for a multi dispense if we can fit at least 2x the volume per well + // for aspiration plus 1x the volume per well for disposal volume + allowedPipettePathOptions.push({ + pathOption: 'multiDispense', + description: t('pipette_path_multi_dispense'), + }) + // for multi aspirate we only need at least 2x the volume per well } else if ( - state.sourceWells.length > 1 && - state.destinationWells.length === 1 + state.transferType === 'consolidate' && + volumeLimits.max >= state.volume * 2 ) { - allowedPipettePathOptions.push('multiAspirate') + allowedPipettePathOptions.push({ + pathOption: 'multiAspirate', + description: t('pipette_path_multi_aspirate'), + }) } - const [ - selectedPipettePathOption, - setSelectedPipettePathOption, - ] = React.useState(state.path) - function getOptionCopy(option: PathOption): string { - switch (option) { - case 'single': - return t('pipette_path_single') - case 'multiAspirate': - return t('pipette_path_multi_aspirate') - case 'multiDispense': - return t('pipette_path_multi_dispense') - default: - return '' - } + const blowOutLocationItems = useBlowOutLocationOptions( + deckConfig, + state.transferType + ) + + const handleClickBackOrExit = (): void => { + currentStep > 1 ? setCurrentStep(currentStep - 1) : onBack() } - const handleClickSave = (): void => { - if (selectedPipettePathOption !== state.path) { + const handleClickSaveOrContinue = (): void => { + if (currentStep === 1) { + if (selectedPath !== 'multiDispense') { + dispatch({ + type: ACTIONS.SET_PIPETTE_PATH, + path: selectedPath, + }) + onBack() + } else { + setCurrentStep(2) + } + } else if (currentStep === 2) { + setCurrentStep(3) + } else { dispatch({ - type: 'SET_PIPETTE_PATH', - path: selectedPipettePathOption, + type: ACTIONS.SET_PIPETTE_PATH, + path: selectedPath as PathOption, + disposalVolume, + blowOutLocation, }) + onBack() } - onBack() } + + const saveOrContinueButtonText = + selectedPath === 'multiDispense' && currentStep < 3 + ? t('shared:continue') + : t('shared:save') + + const maxVolumeCapacity = volumeLimits.max - state.volume * 2 + const volumeRange = { min: 1, max: maxVolumeCapacity } + + const volumeError = + disposalVolume !== null && + (disposalVolume < volumeRange.min || disposalVolume > volumeRange.max) + ? t(`value_out_of_range`, { + min: volumeRange.min, + max: volumeRange.max, + }) + : null + + let buttonIsDisabled = false + if (currentStep === 2) { + buttonIsDisabled = disposalVolume == null || volumeError != null + } else if (currentStep === 3) { + buttonIsDisabled = blowOutLocation == null + } + return createPortal( - - {allowedPipettePathOptions.map(option => ( - { - setSelectedPipettePathOption(option) - }} - buttonText={getOptionCopy(option)} - /> - ))} - + {currentStep === 1 ? ( + + {allowedPipettePathOptions.map(option => ( + { + setSelectedPath(option.pathOption) + }} + buttonText={option.description} + /> + ))} + + ) : null} + {currentStep === 2 ? ( + + + + + + { + setDisposalVolume(Number(e)) + }} + /> + + + ) : null} + {currentStep === 3 ? ( + + {blowOutLocationItems.map(option => ( + { + setBlowOutLocation(option.location) + }} + buttonText={option.description} + /> + ))} + + ) : null} , getTopPortalEl() ) diff --git a/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/TipPosition.tsx b/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/TipPosition.tsx new file mode 100644 index 00000000000..0c365a6c336 --- /dev/null +++ b/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/TipPosition.tsx @@ -0,0 +1,144 @@ +import * as React from 'react' +import { useTranslation } from 'react-i18next' +import { + Flex, + SPACING, + DIRECTION_COLUMN, + ALIGN_CENTER, + POSITION_FIXED, + COLORS, +} from '@opentrons/components' +import { getTopPortalEl } from '../../../App/portal' +import { ChildNavigation } from '../../ChildNavigation' +import { InputField } from '../../../atoms/InputField' +import { NumericalKeyboard } from '../../../atoms/SoftwareKeyboard' + +import type { + QuickTransferSummaryState, + QuickTransferSummaryAction, + FlowRateKind, +} from '../types' + +import { ACTIONS } from '../constants' +import { createPortal } from 'react-dom' + +interface TipPositionEntryProps { + onBack: () => void + state: QuickTransferSummaryState + dispatch: React.Dispatch + kind: FlowRateKind // TODO: rename flowRateKind to be generic +} + +export function TipPositionEntry(props: TipPositionEntryProps): JSX.Element { + const { onBack, state, dispatch, kind } = props + const { i18n, t } = useTranslation(['quick_transfer', 'shared']) + const keyboardRef = React.useRef(null) + + const [tipPosition, setTipPosition] = React.useState( + kind === 'aspirate' ? state.tipPositionAspirate : state.tipPositionDispense + ) + + let wellHeight = 1 + if (kind === 'aspirate') { + wellHeight = Math.max( + ...state.sourceWells.map(well => + state.source != null ? state.source.wells[well].depth : 0 + ) + ) + } else if (kind === 'dispense') { + const destLabwareDefinition = + state.destination === 'source' ? state.source : state.destination + wellHeight = Math.max( + ...state.destinationWells.map(well => + destLabwareDefinition != null + ? destLabwareDefinition.wells[well].depth + : 0 + ) + ) + } + + // the maxiumum allowed position is 2x the height of the well + const tipPositionRange = { min: 1, max: Math.floor(wellHeight * 2) } // TODO: set this based on range + + const textEntryCopy: string = t('distance_bottom_of_well_mm') + const tipPositionAction = + kind === 'aspirate' + ? ACTIONS.SET_ASPIRATE_TIP_POSITION + : ACTIONS.SET_DISPENSE_TIP_POSITION + + const handleClickSave = (): void => { + // the button will be disabled if this values is null + if (tipPosition != null) { + dispatch({ + type: tipPositionAction, + position: tipPosition, + }) + } + onBack() + } + + const error = + tipPosition != null && + (tipPosition < tipPositionRange.min || tipPosition > tipPositionRange.max) + ? t(`value_out_of_range`, { + min: Math.floor(tipPositionRange.min), + max: Math.floor(tipPositionRange.max), + }) + : null + + return createPortal( + + + + + + + + { + setTipPosition(Number(e)) + }} + /> + + + , + getTopPortalEl() + ) +} diff --git a/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/TouchTip.tsx b/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/TouchTip.tsx new file mode 100644 index 00000000000..5791ac2813c --- /dev/null +++ b/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/TouchTip.tsx @@ -0,0 +1,209 @@ +import * as React from 'react' +import { useTranslation } from 'react-i18next' +import { createPortal } from 'react-dom' +import { + Flex, + SPACING, + DIRECTION_COLUMN, + POSITION_FIXED, + COLORS, + ALIGN_CENTER, +} from '@opentrons/components' +import { getTopPortalEl } from '../../../App/portal' +import { LargeButton } from '../../../atoms/buttons' +import { ChildNavigation } from '../../ChildNavigation' +import { InputField } from '../../../atoms/InputField' +import { ACTIONS } from '../constants' + +import type { + QuickTransferSummaryState, + QuickTransferSummaryAction, + FlowRateKind, +} from '../types' +import { i18n } from '../../../i18n' +import { NumericalKeyboard } from '../../../atoms/SoftwareKeyboard' + +interface TouchTipProps { + onBack: () => void + state: QuickTransferSummaryState + dispatch: React.Dispatch + kind: FlowRateKind +} + +export function TouchTip(props: TouchTipProps): JSX.Element { + const { kind, onBack, state, dispatch } = props + const { t } = useTranslation('quick_transfer') + const keyboardRef = React.useRef(null) + + const [touchTipIsEnabled, setTouchTipIsEnabled] = React.useState( + kind === 'aspirate' + ? state.touchTipAspirate != null + : state.touchTipDispense != null + ) + const [currentStep, setCurrentStep] = React.useState(1) + const [position, setPosition] = React.useState( + kind === 'aspirate' + ? state.touchTipAspirate ?? null + : state.touchTipDispense ?? null + ) + + const touchTipAction = + kind === 'aspirate' + ? ACTIONS.SET_TOUCH_TIP_ASPIRATE + : ACTIONS.SET_TOUCH_TIP_DISPENSE + + const enableTouchTipDisplayItems = [ + { + option: true, + description: t('option_enabled'), + onClick: () => { + setTouchTipIsEnabled(true) + }, + }, + { + option: false, + description: t('option_disabled'), + onClick: () => { + setTouchTipIsEnabled(false) + }, + }, + ] + + const handleClickBackOrExit = (): void => { + currentStep > 1 ? setCurrentStep(currentStep - 1) : onBack() + } + + const handleClickSaveOrContinue = (): void => { + if (currentStep === 1) { + if (!touchTipIsEnabled) { + dispatch({ type: touchTipAction, position: undefined }) + onBack() + } else { + setCurrentStep(2) + } + } else if (currentStep === 2) { + dispatch({ type: touchTipAction, position: position ?? undefined }) + onBack() + } + } + + const setSaveOrContinueButtonText = + touchTipIsEnabled && currentStep < 2 + ? t('shared:continue') + : t('shared:save') + + let wellHeight = 1 + if (kind === 'aspirate') { + wellHeight = Math.max( + ...state.sourceWells.map(well => + state.source !== null ? state.source.wells[well].depth : 0 + ) + ) + } else if (kind === 'dispense') { + const destLabwareDefinition = + state.destination === 'source' ? state.source : state.destination + wellHeight = Math.max( + ...state.destinationWells.map(well => + destLabwareDefinition !== null + ? destLabwareDefinition.wells[well].depth + : 0 + ) + ) + } + + // the allowed range for touch tip is half the height of the well to 1x the height + const positionRange = { min: Math.round(wellHeight / 2), max: wellHeight } + const positionError = + position !== null && + (position < positionRange.min || position > positionRange.max) + ? t(`value_out_of_range`, { + min: positionRange.min, + max: Math.floor(positionRange.max), + }) + : null + + let buttonIsDisabled = false + if (currentStep === 2) { + buttonIsDisabled = position == null || positionError != null + } + + return createPortal( + + + {currentStep === 1 ? ( + + {enableTouchTipDisplayItems.map(displayItem => ( + + ))} + + ) : null} + {currentStep === 2 ? ( + + + + + + { + setPosition(Number(e)) + }} + /> + + + ) : null} + , + getTopPortalEl() + ) +} diff --git a/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/index.tsx b/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/index.tsx index 609ac889b4c..5d79f2a85e5 100644 --- a/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/index.tsx +++ b/app/src/organisms/QuickTransferFlow/QuickTransferAdvancedSettings/index.tsx @@ -1,11 +1,39 @@ import * as React from 'react' -import { Flex, SPACING, DIRECTION_COLUMN } from '@opentrons/components' -import { BaseSettings } from './BaseSettings' +import { useTranslation } from 'react-i18next' +import { + Flex, + SPACING, + DIRECTION_COLUMN, + JUSTIFY_SPACE_BETWEEN, + TYPOGRAPHY, + ALIGN_CENTER, + COLORS, + TEXT_ALIGN_RIGHT, + StyledText, + Icon, + SIZE_2, + TEXT_ALIGN_LEFT, +} from '@opentrons/components' import type { QuickTransferSummaryAction, QuickTransferSummaryState, } from '../types' +import { ACTIONS } from '../constants' +import { useToaster } from '../../../organisms/ToasterOven' +import { ListItem } from '../../../atoms/ListItem' +import { FlowRateEntry } from './FlowRate' +import { PipettePath } from './PipettePath' +import { TipPositionEntry } from './TipPosition' +import { Mix } from './Mix' +import { Delay } from './Delay' +import { TouchTip } from './TouchTip' +import { AirGap } from './AirGap' +import { BlowOut } from './BlowOut' +import { + TRASH_BIN_ADAPTER_FIXTURE, + WASTE_CHUTE_FIXTURES, +} from '@opentrons/shared-data' interface QuickTransferAdvancedSettingsProps { state: QuickTransferSummaryState @@ -16,14 +44,559 @@ export function QuickTransferAdvancedSettings( props: QuickTransferAdvancedSettingsProps ): JSX.Element | null { const { state, dispatch } = props + const { t, i18n } = useTranslation(['quick_transfer', 'shared']) + const [selectedSetting, setSelectedSetting] = React.useState( + null + ) + const { makeSnackbar } = useToaster() + + function getBlowoutValueCopy(): string | undefined { + if (state.blowOut === 'dest_well') { + return t('blow_out_into_destination_well') + } else if (state.blowOut === 'source_well') { + return t('blow_out_into_source_well') + } else if ( + state.blowOut != null && + state.blowOut.cutoutFixtureId === TRASH_BIN_ADAPTER_FIXTURE + ) { + return t('blow_out_into_trash_bin') + } else if ( + state.blowOut != null && + WASTE_CHUTE_FIXTURES.includes(state.blowOut.cutoutFixtureId) + ) { + return t('blow_out_into_waste_chute') + } + } + + let pipettePathValue: string = '' + if (state.path === 'single') { + pipettePathValue = t('pipette_path_single') + } else if (state.path === 'multiAspirate') { + pipettePathValue = t('pipette_path_multi_aspirate') + } else if (state.path === 'multiDispense') { + pipettePathValue = t('pipette_path_multi_dispense', { + volume: state.disposalVolume, + blowOutLocation: getBlowoutValueCopy(), + }) + } + + const destinationLabwareDef = + state.destination === 'source' ? state.source : state.destination + const sourceIsReservoir = + state.source.metadata.displayCategory === 'reservoir' + const destIsReservoir = + destinationLabwareDef.metadata.displayCategory === 'reservoir' + + const baseSettingsItems = [ + { + option: 'aspirate_flow_rate', + copy: t('aspirate_flow_rate'), + value: t('flow_rate_value', { flow_rate: state.aspirateFlowRate }), + enabled: true, + onClick: () => { + setSelectedSetting('aspirate_flow_rate') + }, + }, + { + option: 'dispense_flow_rate', + copy: t('dispense_flow_rate'), + value: t('flow_rate_value', { flow_rate: state.dispenseFlowRate }), + enabled: true, + onClick: () => { + setSelectedSetting('dispense_flow_rate') + }, + }, + { + option: 'pipette_path', + copy: t('pipette_path'), + value: pipettePathValue, + enabled: state.transferType !== 'transfer', + onClick: () => { + if (state.transferType !== 'transfer') { + setSelectedSetting('pipette_path') + } else { + makeSnackbar(t('advanced_setting_disabled') as string) + } + }, + }, + ] + + const aspirateSettingsItems = [ + { + option: 'tip_position', + copy: t('tip_position'), + value: + state.tipPositionAspirate !== null + ? t('tip_position_value', { position: state.tipPositionAspirate }) + : '', + enabled: true, + onClick: () => { + setSelectedSetting('aspirate_tip_position') + }, + }, + { + option: 'pre_wet_tip', + copy: t('pre_wet_tip'), + value: state.preWetTip ? t('option_enabled') : '', + enabled: true, + onClick: () => { + dispatch({ + type: ACTIONS.SET_PRE_WET_TIP, + preWetTip: !state.preWetTip, + }) + }, + }, + { + option: 'aspirate_mix', + copy: t('mix'), + value: + state.mixOnAspirate !== undefined + ? t('mix_value', { + volume: state.mixOnAspirate?.mixVolume, + reps: state.mixOnAspirate?.repititions, + }) + : '', + enabled: state.transferType === 'transfer', + onClick: () => { + if (state.transferType === 'transfer') { + setSelectedSetting('aspirate_mix') + } else { + makeSnackbar(t('advanced_setting_disabled') as string) + } + }, + }, + { + option: 'aspirate_delay', + copy: t('delay'), + value: + state.delayAspirate !== undefined + ? t('delay_value', { + delay: state.delayAspirate.delayDuration, + position: state.delayAspirate.positionFromBottom, + }) + : '', + enabled: true, + onClick: () => { + setSelectedSetting('aspirate_delay') + }, + }, + { + option: 'aspirate_touch_tip', + copy: t('touch_tip'), + value: + state.touchTipAspirate !== undefined + ? t('touch_tip_value', { position: state.touchTipAspirate }) + : '', + enabled: !sourceIsReservoir, + onClick: () => { + // disable for reservoir + if (!sourceIsReservoir) { + setSelectedSetting('aspirate_touch_tip') + } else { + makeSnackbar(t('advanced_setting_disabled') as string) + } + }, + }, + { + option: 'aspirate_air_gap', + copy: t('air_gap'), + value: + state.airGapAspirate !== undefined + ? t('air_gap_value', { volume: state.airGapAspirate }) + : '', + enabled: true, + onClick: () => { + setSelectedSetting('aspirate_air_gap') + }, + }, + ] + + const dispenseSettingsItems = [ + { + option: 'dispense_tip_position', + copy: t('tip_position'), + value: + state.tipPositionDispense !== undefined + ? t('tip_position_value', { position: state.tipPositionDispense }) + : '', + enabled: true, + onClick: () => { + setSelectedSetting('dispense_tip_position') + }, + }, + { + option: 'dispense_mix', + copy: t('mix'), + value: + state.mixOnDispense !== undefined + ? t('mix_value', { + volume: state.mixOnDispense?.mixVolume, + reps: state.mixOnDispense?.repititions, + }) + : '', + enabled: state.transferType === 'transfer', + onClick: () => { + if (state.transferType === 'transfer') { + setSelectedSetting('dispense_mix') + } else { + makeSnackbar(t('advanced_setting_disabled') as string) + } + }, + }, + { + option: 'dispense_delay', + copy: t('delay'), + value: + state.delayDispense !== undefined + ? t('delay_value', { + delay: state.delayDispense.delayDuration, + position: state.delayDispense.positionFromBottom, + }) + : '', + enabled: true, + onClick: () => { + setSelectedSetting('dispense_delay') + }, + }, + { + option: 'dispense_touch_tip', + copy: t('touch_tip'), + value: + state.touchTipDispense !== undefined + ? t('touch_tip_value', { position: state.touchTipDispense }) + : '', + enabled: !destIsReservoir, + onClick: () => { + if (!destIsReservoir) { + setSelectedSetting('dispense_touch_tip') + } else { + makeSnackbar(t('advanced_setting_disabled') as string) + } + }, + }, + { + option: 'dispense_air_gap', + copy: t('air_gap'), + value: + state.airGapDispense !== undefined + ? t('air_gap_value', { volume: state.airGapDispense }) + : '', + enabled: true, + onClick: () => { + setSelectedSetting('dispense_air_gap') + }, + }, + { + option: 'dispense_blow_out', + copy: t('blow_out'), + value: i18n.format(getBlowoutValueCopy(), 'capitalize'), + enabled: state.transferType !== 'distribute', + onClick: () => { + if (state.transferType === 'distribute') { + makeSnackbar(t('advanced_setting_disabled') as string) + } else { + setSelectedSetting('dispense_blow_out') + } + }, + }, + ] return ( - + {/* Base Settings */} + + {selectedSetting == null + ? baseSettingsItems.map(displayItem => ( + + + + {displayItem.copy} + + + + {displayItem.value} + + {displayItem.enabled ? ( + + ) : null} + + + + )) + : null} + {selectedSetting === 'aspirate_flow_rate' ? ( + { + setSelectedSetting(null) + }} + /> + ) : null} + {selectedSetting === 'dispense_flow_rate' ? ( + { + setSelectedSetting(null) + }} + /> + ) : null} + {selectedSetting === 'pipette_path' ? ( + { + setSelectedSetting(null) + }} + /> + ) : null} + + + {/* Aspirate Settings */} + + {selectedSetting === null ? ( + + {t('aspirate_settings')} + + ) : null} + + {selectedSetting === null + ? aspirateSettingsItems.map(displayItem => ( + + + + {displayItem.copy} + + + + {displayItem.value !== '' + ? displayItem.value + : t('option_disabled')} + + + {displayItem.option !== 'pre_wet_tip' ? ( + + ) : null} + + + + )) + : null} + {selectedSetting === 'aspirate_tip_position' ? ( + { + setSelectedSetting(null) + }} + /> + ) : null} + {selectedSetting === 'aspirate_mix' ? ( + { + setSelectedSetting(null) + }} + /> + ) : null} + {selectedSetting === 'aspirate_delay' ? ( + { + setSelectedSetting(null) + }} + /> + ) : null} + {selectedSetting === 'aspirate_touch_tip' ? ( + { + setSelectedSetting(null) + }} + /> + ) : null} + {selectedSetting === 'aspirate_air_gap' ? ( + { + setSelectedSetting(null) + }} + /> + ) : null} + + + + {/* Dispense Settings */} + + {selectedSetting === null ? ( + + {t('dispense_settings')} + + ) : null} + + {selectedSetting === null + ? dispenseSettingsItems.map(displayItem => ( + + + + {displayItem.copy} + + + + {displayItem.value !== '' + ? displayItem.value + : t('option_disabled')} + + + + + + )) + : null} + {selectedSetting === 'dispense_tip_position' ? ( + { + setSelectedSetting(null) + }} + /> + ) : null} + {selectedSetting === 'dispense_mix' ? ( + { + setSelectedSetting(null) + }} + /> + ) : null} + {selectedSetting === 'dispense_delay' ? ( + { + setSelectedSetting(null) + }} + /> + ) : null} + {selectedSetting === 'dispense_touch_tip' ? ( + { + setSelectedSetting(null) + }} + /> + ) : null} + {selectedSetting === 'dispense_air_gap' ? ( + { + setSelectedSetting(null) + }} + /> + ) : null} + {selectedSetting === 'dispense_blow_out' ? ( + { + setSelectedSetting(null) + }} + /> + ) : null} + + ) } diff --git a/app/src/organisms/QuickTransferFlow/__tests__/SummaryAndSettings.test.tsx b/app/src/organisms/QuickTransferFlow/__tests__/SummaryAndSettings.test.tsx index 3240f3af7f9..9635b05eb03 100644 --- a/app/src/organisms/QuickTransferFlow/__tests__/SummaryAndSettings.test.tsx +++ b/app/src/organisms/QuickTransferFlow/__tests__/SummaryAndSettings.test.tsx @@ -76,7 +76,7 @@ describe('SummaryAndSettings', () => { mutateAsync: createProtocol, } as any) vi.mocked(useCreateRunMutation).mockReturnValue({ - createRun: createRun, + createRun, } as any) vi.mocked(createQuickTransferFile).mockReturnValue('' as any) createProtocol.mockResolvedValue({ diff --git a/app/src/organisms/QuickTransferFlow/__tests__/utils/getInitialSummaryState.test.ts b/app/src/organisms/QuickTransferFlow/__tests__/utils/getInitialSummaryState.test.ts index f5eac572400..4937af941d5 100644 --- a/app/src/organisms/QuickTransferFlow/__tests__/utils/getInitialSummaryState.test.ts +++ b/app/src/organisms/QuickTransferFlow/__tests__/utils/getInitialSummaryState.test.ts @@ -83,7 +83,7 @@ describe('getInitialSummaryState', () => { transferType: 'consolidate', aspirateFlowRate: 50, dispenseFlowRate: 75, - path: 'multiDispense', + path: 'multiAspirate', tipPositionAspirate: 1, preWetTip: false, tipPositionDispense: 1, @@ -133,7 +133,7 @@ describe('getInitialSummaryState', () => { transferType: 'distribute', aspirateFlowRate: 50, dispenseFlowRate: 75, - path: 'multiAspirate', + path: 'multiDispense', tipPositionAspirate: 1, preWetTip: false, tipPositionDispense: 1, @@ -142,6 +142,8 @@ describe('getInitialSummaryState', () => { cutoutId: 'cutoutA3', cutoutFixtureId: 'trashBinAdapter', }, + disposalVolume: props.state.volume, + blowOut: { cutoutId: 'cutoutA3', cutoutFixtureId: 'trashBinAdapter' }, }) }) it('generates the summary state with correct default value for 1 to n transfer with too high of volume for multiDispense', () => { diff --git a/app/src/organisms/QuickTransferFlow/reducers.ts b/app/src/organisms/QuickTransferFlow/reducers.ts index 368081f4362..81e2f56ee9b 100644 --- a/app/src/organisms/QuickTransferFlow/reducers.ts +++ b/app/src/organisms/QuickTransferFlow/reducers.ts @@ -58,12 +58,12 @@ export function quickTransferWizardReducer( state.sourceWells != null && state.sourceWells.length > action.wells.length ) { - transferType = DISTRIBUTE + transferType = CONSOLIDATE } else if ( state.sourceWells != null && state.sourceWells.length < action.wells.length ) { - transferType = CONSOLIDATE + transferType = DISTRIBUTE } return { pipette: state.pipette, @@ -73,7 +73,7 @@ export function quickTransferWizardReducer( sourceWells: state.sourceWells, destination: state.destination, destinationWells: action.wells, - transferType: transferType, + transferType, } } case 'SET_VOLUME': { @@ -110,9 +110,19 @@ export function quickTransferSummaryReducer( } } case 'SET_PIPETTE_PATH': { - return { - ...state, - path: action.path, + if (action.path === 'multiDispense') { + return { + ...state, + path: action.path, + disposalVolume: action.disposalVolume, + blowOut: action.blowOutLocation, + } + } else { + return { + ...state, + path: action.path, + disposalVolume: undefined, + } } } case 'SET_ASPIRATE_TIP_POSITION': { diff --git a/app/src/organisms/QuickTransferFlow/types.ts b/app/src/organisms/QuickTransferFlow/types.ts index f859d699dac..d4e83558a21 100644 --- a/app/src/organisms/QuickTransferFlow/types.ts +++ b/app/src/organisms/QuickTransferFlow/types.ts @@ -25,6 +25,7 @@ export type ChangeTipOptions = | 'perDest' | 'perSource' export type FlowRateKind = 'aspirate' | 'dispense' | 'blowout' +export type BlowOutLocation = 'source_well' | 'dest_well' | CutoutConfig export interface QuickTransferSummaryState { pipette: PipetteV2Specs @@ -61,7 +62,8 @@ export interface QuickTransferSummaryState { positionFromBottom: number } touchTipDispense?: number - blowOut?: string // trashBin or wasteChute or 'SOURCE_WELL' or 'DEST_WELL' + disposalVolume?: number + blowOut?: BlowOutLocation airGapDispense?: number changeTip: ChangeTipOptions dropTipLocation: CutoutConfig @@ -111,6 +113,8 @@ interface SetDispenseFlowRateAction { interface SetPipettePath { type: typeof ACTIONS.SET_PIPETTE_PATH path: PathOption + disposalVolume?: number + blowOutLocation?: BlowOutLocation } interface SetAspirateTipPosition { type: typeof ACTIONS.SET_ASPIRATE_TIP_POSITION @@ -160,7 +164,7 @@ interface SetTouchTipDispense { } interface SetBlowOut { type: typeof ACTIONS.SET_BLOW_OUT - location?: 'source_well' | 'dest_well' | 'trashBin' | 'wasteChute' + location?: BlowOutLocation } interface SetAirGapDispense { type: typeof ACTIONS.SET_AIR_GAP_DISPENSE diff --git a/app/src/organisms/QuickTransferFlow/utils/createQuickTransferFile.ts b/app/src/organisms/QuickTransferFlow/utils/createQuickTransferFile.ts index 5bed7582291..a4114c0a4d4 100644 --- a/app/src/organisms/QuickTransferFlow/utils/createQuickTransferFile.ts +++ b/app/src/organisms/QuickTransferFlow/utils/createQuickTransferFile.ts @@ -218,7 +218,7 @@ export function createQuickTransferFile( const labwareDefinitions = Object.values( invariantContext.labwareEntities - ).reduce<{ [x: string]: LabwareDefinition2 }>((acc, entity) => { + ).reduce>((acc, entity) => { return { ...acc, [entity.labwareDefURI]: entity.def } }, {}) diff --git a/app/src/organisms/QuickTransferFlow/utils/generateQuickTransferArgs.ts b/app/src/organisms/QuickTransferFlow/utils/generateQuickTransferArgs.ts index af8807261e3..4f571665b10 100644 --- a/app/src/organisms/QuickTransferFlow/utils/generateQuickTransferArgs.ts +++ b/app/src/organisms/QuickTransferFlow/utils/generateQuickTransferArgs.ts @@ -14,13 +14,14 @@ import { DEFAULT_MM_BLOWOUT_OFFSET_FROM_TOP, DEFAULT_MM_TOUCH_TIP_OFFSET_FROM_TOP, } from '../constants' -import type { QuickTransferSummaryState } from '../types' import type { + CutoutConfig, LabwareDefinition2, DeckConfiguration, PipetteName, NozzleConfigurationStyle, } from '@opentrons/shared-data' +import type { QuickTransferSummaryState } from '../types' import type { ConsolidateArgs, DistributeArgs, @@ -46,8 +47,7 @@ function getOrderedWells( } function getInvariantContextAndRobotState( - quickTransferState: QuickTransferSummaryState, - deckConfig: DeckConfiguration + quickTransferState: QuickTransferSummaryState ): { invariantContext: InvariantContext; robotState: RobotState } { const tipRackDefURI = getLabwareDefURI(quickTransferState.tipRack) let pipetteName = quickTransferState.pipette.model @@ -114,7 +114,7 @@ function getInvariantContextAndRobotState( labwareLocations = { ...labwareLocations, [tipRackId]: { - slot: adapterId != null ? adapterId : 'B2', + slot: adapterId ?? 'B2', }, [sourceLabwareId]: { slot: 'C2', @@ -140,7 +140,7 @@ function getInvariantContextAndRobotState( } } let additionalEquipmentEntities: AdditionalEquipmentEntities = {} - // TODO add check for blowout location here + if ( quickTransferState.dropTipLocation.cutoutFixtureId === TRASH_BIN_ADAPTER_FIXTURE @@ -155,7 +155,29 @@ function getInvariantContextAndRobotState( }, } } - // TODO add check for blowout location here + if ( + quickTransferState.blowOut != null && + quickTransferState.blowOut !== 'source_well' && + quickTransferState.blowOut !== 'dest_well' && + quickTransferState.blowOut?.cutoutFixtureId === TRASH_BIN_ADAPTER_FIXTURE + ) { + const trashLocation = quickTransferState.blowOut.cutoutId + const isSameTrash = Object.values(additionalEquipmentEntities).some( + entity => entity.location === trashLocation + ) + if (!isSameTrash) { + const trashId = `${uuid()}_trashBin` + additionalEquipmentEntities = { + ...additionalEquipmentEntities, + [trashId]: { + name: 'trashBin', + id: trashId, + location: trashLocation, + }, + } + } + } + if ( WASTE_CHUTE_FIXTURES.includes( quickTransferState.dropTipLocation.cutoutFixtureId @@ -172,12 +194,33 @@ function getInvariantContextAndRobotState( }, } } - + if ( + quickTransferState.blowOut != null && + quickTransferState.blowOut !== 'source_well' && + quickTransferState.blowOut !== 'dest_well' && + WASTE_CHUTE_FIXTURES.includes(quickTransferState.blowOut.cutoutFixtureId) + ) { + const wasteChuteLocation = quickTransferState.dropTipLocation.cutoutId + const isSameChute = Object.values(additionalEquipmentEntities).some( + entity => entity.location === wasteChuteLocation + ) + if (!isSameChute) { + const wasteChuteId = `${uuid()}_wasteChute` + additionalEquipmentEntities = { + ...additionalEquipmentEntities, + [wasteChuteId]: { + name: 'wasteChute', + id: wasteChuteId, + location: wasteChuteLocation, + }, + } + } + } const invariantContext = { labwareEntities, moduleEntities: {}, pipetteEntities, - additionalEquipmentEntities: additionalEquipmentEntities, + additionalEquipmentEntities, config: { OT_PD_DISABLE_MODULE_RESTRICTIONS: false }, } const moduleLocations = {} @@ -224,22 +267,25 @@ export function generateQuickTransferArgs( } } const { invariantContext, robotState } = getInvariantContextAndRobotState( - quickTransferState, - deckConfig + quickTransferState ) - // this cannot be 'dest_well' for multiDispense - let blowoutLocation = quickTransferState.blowOut - if (quickTransferState.blowOut === 'trashBin') { - const trashBinEntity = Object.values( - invariantContext.additionalEquipmentEntities - ).find(entity => entity.name === 'trashBin') - blowoutLocation = trashBinEntity?.id - } else if (quickTransferState.blowOut === 'wasteChute') { - const wasteChuteEntity = Object.values( + let blowoutLocation: string | undefined + if ( + quickTransferState?.blowOut != null && + quickTransferState.blowOut !== 'source_well' && + quickTransferState.blowOut !== 'dest_well' && + 'cutoutId' in quickTransferState.blowOut + ) { + const entity = Object.values( invariantContext.additionalEquipmentEntities - ).find(entity => entity.name === 'wasteChute') - blowoutLocation = wasteChuteEntity?.id + ).find(entity => { + const blowoutObject = quickTransferState.blowOut as CutoutConfig + return entity.location === blowoutObject.cutoutId + }) + blowoutLocation = entity?.id + } else { + blowoutLocation = quickTransferState.blowOut } const dropTipLocationEntity = Object.values( @@ -305,20 +351,18 @@ export function generateQuickTransferArgs( dispenseAirGapVolume: quickTransferState.airGapDispense ?? null, touchTipAfterAspirate: quickTransferState.touchTipAspirate != null, touchTipAfterAspirateOffsetMmFromBottom: - quickTransferState.touchTipAspirate != null - ? quickTransferState.touchTipAspirate - : getWellsDepth(quickTransferState.source, sourceWells) + - DEFAULT_MM_TOUCH_TIP_OFFSET_FROM_TOP, + quickTransferState.touchTipAspirate ?? + getWellsDepth(quickTransferState.source, sourceWells) + + DEFAULT_MM_TOUCH_TIP_OFFSET_FROM_TOP, touchTipAfterDispense: quickTransferState.touchTipDispense != null, touchTipAfterDispenseOffsetMmFromBottom: - quickTransferState.touchTipDispense != null - ? quickTransferState.touchTipDispense - : getWellsDepth( - quickTransferState.destination === 'source' - ? quickTransferState.source - : quickTransferState.destination, - destWells - ) + DEFAULT_MM_TOUCH_TIP_OFFSET_FROM_TOP, + quickTransferState.touchTipDispense ?? + getWellsDepth( + quickTransferState.destination === 'source' + ? quickTransferState.source + : quickTransferState.destination, + destWells + ) + DEFAULT_MM_TOUCH_TIP_OFFSET_FROM_TOP, dropTipLocation, aspirateXOffset: 0, aspirateYOffset: 0, @@ -390,7 +434,7 @@ export function generateQuickTransferArgs( const distributeStepArguments: DistributeArgs = { ...commonFields, commandCreatorFnName: 'distribute', - disposalVolume: quickTransferState.volume, + disposalVolume: quickTransferState.disposalVolume, mixBeforeAspirate: quickTransferState.mixOnAspirate != null ? { @@ -399,7 +443,7 @@ export function generateQuickTransferArgs( } : null, sourceWell: sourceWells[0], - destWells: destWells, + destWells, } return { stepArgs: distributeStepArguments, diff --git a/app/src/organisms/QuickTransferFlow/utils/getInitialSummaryState.ts b/app/src/organisms/QuickTransferFlow/utils/getInitialSummaryState.ts index ff582c53e01..d073dd13894 100644 --- a/app/src/organisms/QuickTransferFlow/utils/getInitialSummaryState.ts +++ b/app/src/organisms/QuickTransferFlow/utils/getInitialSummaryState.ts @@ -49,13 +49,13 @@ export function getInitialSummaryState( // for multiDispense the volume capacity must be at least 3x the volume per well // to account for the 1x volume per well disposal volume default if ( - state.transferType === 'consolidate' && + state.transferType === 'distribute' && volumeLimits.max >= state.volume * 3 ) { path = 'multiDispense' // for multiAspirate the volume capacity must be at least 2x the volume per well } else if ( - state.transferType === 'distribute' && + state.transferType === 'consolidate' && volumeLimits.max >= state.volume * 2 ) { path = 'multiAspirate' @@ -89,6 +89,8 @@ export function getInitialSummaryState( aspirateFlowRate: flowRatesForSupportedTip.defaultAspirateFlowRate.default, dispenseFlowRate: flowRatesForSupportedTip.defaultDispenseFlowRate.default, path, + disposalVolume: path === 'multiDispense' ? state.volume : undefined, + blowOut: path === 'multiDispense' ? trashConfigCutout : undefined, tipPositionAspirate: 1, preWetTip: false, tipPositionDispense: 1, From 9db2511bb814bb8b2e4cd0cb8969287c6adde580 Mon Sep 17 00:00:00 2001 From: Sarah Breen Date: Thu, 11 Jul 2024 16:23:11 -0400 Subject: [PATCH 02/78] feat(app): add introductory and error modals for quick transfer (#15626) fix PLAT-234, PLAT-235, PLAT-236 --- .../on-device-display/odd-abstract-6.png | Bin 0 -> 22829 bytes app/src/assets/localization/en/anonymous.json | 3 + app/src/assets/localization/en/branded.json | 3 + .../localization/en/quick_transfer.json | 5 ++ app/src/molecules/BackgroundOverlay/index.tsx | 2 +- .../IntroductoryModal.tsx | 56 +++++++++++++++ .../PipetteNotAttachedErrorModal.tsx | 56 +++++++++++++++ .../StorageLimitReachedErrorModal.tsx | 49 +++++++++++++ .../pages/QuickTransferDashboard/index.tsx | 67 ++++++++++++++++-- 9 files changed, 236 insertions(+), 5 deletions(-) create mode 100644 app/src/assets/images/on-device-display/odd-abstract-6.png create mode 100644 app/src/pages/QuickTransferDashboard/IntroductoryModal.tsx create mode 100644 app/src/pages/QuickTransferDashboard/PipetteNotAttachedErrorModal.tsx create mode 100644 app/src/pages/QuickTransferDashboard/StorageLimitReachedErrorModal.tsx diff --git a/app/src/assets/images/on-device-display/odd-abstract-6.png b/app/src/assets/images/on-device-display/odd-abstract-6.png new file mode 100644 index 0000000000000000000000000000000000000000..d145688583636f09d259fab105f7ef5c7b9b4db4 GIT binary patch literal 22829 zcmce-hd12a_XaA3yd+34B5L$bbVf^*VD!OgLG<2xPeh5)d+$aU-RPn-TJ$nXwCKI} zJACh2_xBfEYcb0#=CjW}d++n?=h-J%NkIx5ix>+H4GmjHS^|uQ_AC?)?Frg{7^rtH z=r0&hFPL`HnvQ5_gtQOePh`L}2dFonID)0b(0&b(ZlZp9Zu&|76B=4Y#LF9fbTl-L zb{UCJs%}qq=P|z$Ox(UWSoGxbr16h@{vRLBoCEZ47}mn9$J?SBUr9+NHgxuKttpSc zr2xP|?PSr{nu$pZZFF1JlAmur?Oo&4S4}l7-t#qLVgy89-}pFHR#kCTRi#Asx%%2J zP(MXI`~T0cZ^^m;p`m3aX};FjrWf2-GT5az_9Kwj<>ZWfM}snCI!GS%b1;lOznN~h z)rc)%TFZdJGfpgoCH>WV2^}iCcoS#11E3`Sb#+gWupA6r_?CViVf-2V3Du_#9$%pTCX*f z3J2CPK9`0V4l2+8kuGIY*T1>Qitv>ABQ)#nBfn&4U%tWm@La#g=PGz}ALoptyRz@f zPW-*7T8t?__ZBKgzowz#7iX$@hOmc+sJ)%{R;`sOS|iZguz8EreS3dCfQ%@bv?JbM zJe1EU(hWhmS1HBL;VIfTVAbCRTOdi?RVd_Y4R>E_@_Uil>Ufj@-t-+gu2;n-RuK)n zlIW#(gG1T^ON*&4vv$zX&j2-m(afDpQ6M^3LYmdgq!e{iYs2o~?`4SxJ3l_!Ib_yz z#l?|EUlkP1>w9^z5TOkQFf!l7vtfreY?PGz7P1ZCJ6l&uDwl~X{J@kFF{&!Wf7jH- zoRhcVU6hUfy^M>+_+PJ;Hf&>(p!MG4|9mhJROCG$rE7ZdX9;@C;ip#um#d*6n!1BW{zRa zSeG=}T3S$-AW8drfcbwO5}L?ykmq{;q#qa4!r;_olmTQEQaGSV2&hSQxAj}bA@aDTT4Kyvc& zV&tgx(G@Bb`&X^GkFHFKU?kL3zifqpuAH{=F&n*|GW?OFP_yqjW>>x180=JNXx}g} zzN4~zYkXtmS$j+7zKy{hZkJXback{m#knaPF(@v!AZNQ8$47VciM(t-zEH7&cKoa2 ztQ9z5O*tz>HeI&e_8?4GCC|SgI`!--cW- zQAxS!E5;!(LDVV%=(xDIKV$B4*d}f!f@C@ubkU^xQh;#P>Cn7?zq#Tm+2)RQH*qmH zq_Z-pY&X++qb@q!A$#??UM}eBWZ^u#oZ;w2shEhQvP}QKxMkM6q=!`fNcoVe%|zrI zja`X<%w&V$vfrp+;>}iNrwMRdPr-W5>x#mQ47|TC4IPFWkV8h7{;&H#tXCq!+BE9m zG8Q28n0%xGa~j|J~PmtJ=X&8${4hm^cPuUzOpk=x1dCBO^>{b@I(jSrNZL^tblaDv(| z-4G(&M9H;ZJx@mQ2-z8}TZ+=Y1|;V+7>d;rMcUWHs8`~!`Z4)fCWa4K#q+{ELLNMa z`B?a0V+_{0DMt0Re%|iQb{iYCGc}e>*EziyPY2BCw$D`&0;CK(j0={l9MXx>J$}dC z3g8ii7@%tIdkx)uK0!fJ&rnssb;EUys-2W>C=z>5}E^DpY!LGNOOifw2qtj`xL~D;m`e(_lkY>t5TGj!C zme8=MX~qpqa^Fnn+X}kELj_wF=SWK;=LG3P) zOz=wStn*iIZ@*h8$+zsIR{G*G9@V;^OvWn?-hch)3cp=X?LgL$D=~eUrQX5_N@K=NbLlrJdO|TDGjaN(vDH)Udi)fXj7t5FOGk(( zXt*%qXY75JV!uvcPrMwQMj;DcHI>D_^QJPjB{i*poI^CC2BTa2R-covNcXw}Z0)zE z+7$@>gV3fPty+;k4G1EzmC@dBR*ButgAeX^pZ4ym+3vJBtq9``*D{8s&#v05Sh3DS z)!D_ht!z(ro=jozD)D%8JDP6=of`@F$}J+kN6`Hv5vg0*)_t%nFkPi%8b zc{815?NN?4vM)5d zz`Ch$ec!b!Q!dZQ;jEQCeQH>?J2d{*Y%TnaOxxuqegc5>Z=w08um1B?iyn~-UM60* zb}b!CmW_I+fB00fL!O1;zg@W9ofx>1b>AJMS)l8oP*yhf3`gPEZ(9Bj1x1bO`^aj5 zxa+&ow}|;NM%|(++Cl6D|5L;Zu2y&bky|}%2hW!R%vHUpM$ zUe>?DAD~T;&@NHBmKRc`5o4Yr)jD--%im&)wHC424 zsk*b!>ISctjn-Og_+--0iFo#+6H3}dK?_)W`oP#}DI7~R`BD+-`$_;WmmX0Z8AZ8t zn3abtRxo`eQc*GVN-Nxb=aBDQ)OMBinaJmeVpJF)AH!f)p;6y%>F2v=d;gJ$h>5L? zMVI@5BxMSvqeq8s$tzoLZtmP$?Nchiw5LS(i(g7pk86;QMaXg0AfS8ovE1I+?dTcY z2LZ6;lP*0@i>AwGjjR2<>)Hd82jjl3r-7!aG=y`Wr~`h^Uniw0p6%{dh!6ML(K*%InzhH>QsYu8{h;xQFC{80fMA%3 z7|Q2smw(gm$!s_OdK02Y{3+7t@}Qp3ho6t$dpI5LmaXmFj9ZUtC3)NU9vv960Pf(= zD+%|M01_ipeDv@-GvAIc1OuBMAnxqNs7M$LSjUgYBvi30-*_ZTv3P9vInbflIhuV< zuFgM6Do;TN4j-!99vPsvT6Q&zuteY%w})@yyWZ_wbqQA)i|TqL%#0x;gI3R8;-96* zh#dA?t72+Kh^7-@jo1y!IKJ|16xc}+PYCu7z@fjP{*wEP!u?t~1?r!SPRI6!sS5?q5{-ThF_BvvR>lVQ-I!4d{F0^X_hkJT2 zU#3;Qr1)=Vow&;`{5D|x*;H3iPzlbMONe($dHn7P-PQ4WWezuRl5}EzVafIUEg|0v zAdN-H9nJP>{L~tRu!>cXvt?BOaOrmz0h`x+&OqyGjWcuM0D?d#mnXx0M_x+rXZgdi zj{X3UEF{YqO#{OlHc#I26jJj@?kN+n#`a-{1xJr|dnUEs=}(5Wrtn$Bszu#0N~XH3 z9VPGx;KrLXd7ScCH{PA)Z|BMD#aNP_s&gW+LuXCA4~JdM5OT&=>Bvvv5emU5jETQ? z8{V8aK6Fk%LQdWaiH4HlL?|>{&EOSf?o*^pdvqC&WmIzezJGENd*MmFsV2!Q{o&4f znj@dw{#J$p4-M_3uvm;L3L4<+4X38*Uy8eG120}m?w$JVT|g_$8H@jtWu>9p;`}#& zt8S#{`OnEynJM&bj9_PA*6S)Rpz~2TU;nQdUt731sjO-JAULUTnhDEA9>;1Pfb=o> z99TI%b)*5E(`oM#;^cX48=dU!I%Qa6%^Dk_YCUree`Bp+Dd>DVIeN=R&Nn{h_;(Mv zcKEKMsvus&e8g5;r+#k@N_q`_!Dg{?OdsmVzl0HV!x-poG|e8vo>1S_OxbfhUcTM4 z|AnnP!1+e5VNU$$ve}gBHmuQ^{!ke59h(0R?;s~-ZNHuaJV4} z9fiQvVH&Q>mX1qW?(!*f?$i2qYe-Qd9{-3dLPU^pm-3Vz@VT^9)|dpTV?%mZXQkof zyx(~Jl-B~hSxX9g8{PBEGl1#q3jTU}RHWI00iZcVm75l#wrZxqR9;rW=W+A9Fvf(6 z1qCm~ACFgOlaKFTlk=}l`kwbqN-?i}`Q_{s2H89HebWldUcYKKIjpUxADTVch+5NK zoIcu`5VXqcht995c9a+3E(=JQ;U>_gEYIrVQsaYD{?9At8SirZ3`BTG@Ap@X zrz0B|ykE>n?i^>VT^7f7iR8C5F|%1^1Ceg5UDN7~gT}Z{CE$5gWCp*(pLlx3tCYzo zcpQ`*rZ(REFKW%%mu*bbyR6=D{5NK+HVUSqVL!H&;{t&HBs>*;F6w{ok}|5*2Ej%X z@f&?2sA}b6nnaFAe$l^>8*eN#ce;&OGmw=up8XPBi`IR>tlet-Sr_o<}kQd zi(l_93J44zY+-i%v(N)#_BVcNSeuPMtyFwrxK$kA)=wBG67MJlm$TM9UajzVn*g&q zFx+GD7^`i3?5VSMi8$HG#}F>Dx)XC;O}%i2+N_I?4Cb~cSSv0HR8GYP-9L~O*aQ!! z%Xo^&O2%9q$KmkUD*$#}5t4)nVjZgl%XtU(_Db|RYjw!jJ9`NS!CQA#@=NY#mAMs| zd_M7Y%19ns`EnoDj`nB^6iJph< z*SoKr_F3DG6QeCjLb553aa|`$K8lu+B1kn|+)66V%o_Dg4Q6XBd2dcM<|swZ2DXH# z&U0-tEFrFaho*H?76?nm_}4e=xJlxII=4f+^JU(Sf)AbM(+yex%KBL0VY1|Qg@dVh z9Q5g>Gg^SK>ug_VnFMrvev|JP(zHZ8J~Mu0`%tRO1S}I!{+pD&ao@Y;le#bA z`XHIHiJ#9$1Xb?%P68_R?bmhYfvZBX_7NXXe%lc(`Q)eHJ9Z6F4b`qOFS<2QsT z#)m`ffj`69Y8FgzI&v?;i883!eCA;-mAN|GGW!pLb3Dcs!DIAvwcrh2_nb2ByiyP1 zPfzy-@=l}a>c_|TBK+NZf}Tzz;m7Qn`MyFohxer6>DG*m30AG|ebT7CZMD5x3{OlC zv8dUj0Qpgs3zS{D0$l(4s$LlwUd0$U7je^f^=PZI2OPcZf%|^-%)0*a@~IK7OmE#6 z^M=5ht+!l;(b=(toNQAwDPgwnWrj&O zmQa3cSqg zy`=>m3d(sjctT?D^}^jQU64`GtUQ|v<77f@t@~`_ug)y~YYR#4&oys8FgyE~7$)$F z#%+rC@Q!IKYTdt^pyDHqF0&4lD%1_!R1)Z>UahGY{$1;!7sI=SSh3l=Cq>1%D*i(` ziDgDnF7s6vL5UXckcVLr;s~a!vA#8`s}Ph8QOgWFu!zI!*1hd1F}Ipl7U zky!wd?K{4T^$Yba_}$Jh@eTd^xB1b1TYXPK8p)Z(lrU3`aT%f1I7Kl71B#Bj=X93$ z`}oHtZ}@OiAE4pHW9P7jq6@dDTKu$EW9_Tw$XHaiD2{m|O0)*%D;F-jMzo6f;G-zmkMn`ba>uV25c>_K1 zyI{S=>Nsh9slZvt!1aBzh|3SLWXfWdpm-`LBBcfv0 z=Hb3$c+lm|glXy*Z0Isfyde22puH|ICvBaLsCXGt@~3{Rn1zZ;Ic>y#M8THdfQ)NHKu zEELX>!u?ML-)TLV-RUTBH)b{Fd)v{{NA=6VzVjcn6wK#ZBNIlc_jA=7(fcUA*5dJy zSAF?HgI(@RV*2l;?_m9%{L-(!m_L)iYC9$Mbw38+&;V&N+z7(H(mM_%CN&KQk}sCU z%WBCTi(D%4Ya4M{W@afPBl0MiPe^)kDK#x%aVxnBA;BhV;Nu4}Ap$cd0u{c3y+Oa+ z=Uka!KD;va(F;*Oc)&73@?ybc+HB`Arz=~`e%#!-+JDVQ3kkxwrn%GPV|`{5YE&C& z?hPVx>EfZF4Ysy&@1%pbFM~uOuVw!+5@I>hezb`?g-E9P8?(R>78Rq0n=tXb@DkPE zHYIz)XG!5;i54YBpkfP$6oN`optx~y!uhDwL&SpW*ye7hat+nmPds8X9-G6tdU9l< z6M%7R8KxwK34I0toYoe&MXCZCnbl%7YM*Lqr8%WkGr?aI;d<*Q&Ho}>pxU%9HPFh+ zvC#B4bGM)-b%#r}=;p z2gB=EB~Y*UCraq)H|&CCHNW04F!yntThVVGIbCF`Izn0Lt!Hdcftj$oDp?b3ZL3^@ zSxAW)oLB9as6_YS>NpWUfrwt;IQFL4xY>AC=F18Vy)F3%7b{*KF2|su)76XHR{ctw zSX)`qBJLWk7TjUhyNmtvf&5F92J2BMagek0`H1fPa3paNE8LNEqFJsy)~>gD_%`~t z^emR(>aYu`Z+I3V^Ylf$U~C;f@u5!~hx?tDU*a{6?=N9b;fUUyvcfA7CKsxN=Y%@7 z;iyh<^sx_LIBfD+NS)8iSqpLERxtYJGX9~E2`5%*T3KK`J?!XidYs#*-}tGK*SjEN zpAi54K|HF~7~@ic;$x#EzB4z;c7wv`NcvCqDXN9KW65J0+by~ERsL$o zOA*JQzQgHinX`&u0Q)4bw{fVe<{yRGxuv7B;)zW~6+)sbv8m^+K57eXIR$~eaG$VA zr@kZ;0<7XaylD)xoEH@xK~WvIv=UmL%#qjrQl~N3 zwD36PT7+9BB1^3@odk#H1+7HGih$3r*ehO(5nP|iZ7L?!#8f_BDfFjt!hR-C_j$)(284&nD1ceR#`~9c$xuSwAX84cTr4~HA>waq~7uC1M z3~BS0gucXd%-i&F;)OJUGs~rWN+b?vUntMj%hk&&osA^Hb=SB&Ja-0;0HQbLw@c*g zpua7V7Qw3NqOUxt*Pj#WPO<=BJw+?P>eGgd;^a7H&Yf(H*OPJHfa zQoWQoVJUoZmb;}?QE*wR@1eu!kr6r@xwYM}w!ZXQvUuxA$}@w~_S~#U@<>={lA?lM z8r?CEN-gO#0;M?;GCk;i8=Zlpna(nSyENHmrerXY_BWGe_KM=LLpFNh$` zX`B8Y?uUl6CxEXC!)y2Vj=vd8^j>xx>vl>xtNBQ0qYl+mkV1ED5JsQT+ z0Kw#2k>=d^Gas!pkFJ7fekr8!%x8D2ax02`+8Ry}usI64%v;D{6Gt#BzDG|{H%2n+ zSxRlm6m5OhTY1`@-8IV@5(aCX2MlPBS%O^1+lJ>n2SQ%135$H+`l+vlck~M=RQ}=H zw!#xMoTrc7v!mrR>Khqhufn_!slU_?7{G0m@mM(H!0CSewp!Q{JA@(ByZSOFIx89- z8!X0nE(eHfS9$u9ian*w?P(N6#KAExa&10PHLnVVLUNgBi_g~^L5RWP|In`2mGh|#Wd`QJ)OafChxKfusNi@)ymUeiamon*0D5i2q|K5T( zc=5xf2z|`O>0Ut}+sW!GHp#u3e{$CEpX+nFb`d#18d1LanD*xIs2N<8PZ0qRv0t-w z++$L$;37v60oaoSP9v9>TL=NCScHVGmA$zYxdO6unEDk21#>OZpr=S(sADX6QRctv zPkXnSWT!ggkZlG7-aL%SZjUCT8UFLh0TnC2O^AQTigA#~z7HrL%B`j*nu3A&wSe{F zlC0;T)Ouh0T6gF2DfNy)4NS+MI&GRatp@yzCW)M?XIe3QYS)V1KI67)--u!KT{MLv zCms!Up;>fXHP487mG!RSGYA)LsRP53HvN`P3s28=NAJh?(( z^?ypBw%*^pbR((YmUHjzl`Ep2LKEBuRJnNm$SEWIerx%TCoGPH7yUv&nppu``Dxkd zZ^(8m8HcK?NB2$xY5D+5=i1M?BV!>Zhmud*!*PLK z7rAS#G)Z*PupwncXteN!vHJ^w8^PP!`{-08mrrXj{vy$94MMhgNtwh!_N2KA-UVjw z(Dl<}lehk!mBy#b1;kdpTh#RxDGsXiebu{bw+AXL6WwA)MNNu@l`P_UEA!%<(HztR zlrf3blN~KEih&yoS{WYjLM!&0U8@EBFSXWAU!4+(NhH$>$ua8wK#?e@OZv!%#P(zV zoYi(K8r_oS_2MsV7tzJ!Ud@uY6kmj{m$}U@Hw(ANT926r@8xRBq6Bgd;p?e#x>+sRrO;KDwp9!RCkl8qBei?X|bsvh5ELOUQ5$dFZ^XcFnw zX8+d*)-2D7oZV|(|XaB9Xu#aN{zH&t0tw80C5`3cM`TT;_VSid_ zc7)4(lDl|{_N0$}xgjLSu+2fU@5I$L30dN-;t;Z$xtQxFgB@G$R^1fzgI4K0Ds!53 zAx&ZPn#fbnyAz^UzgtZ6t%}A`v*VB$VZFZJdi^6wQ+AE0^^dZa+YRe891l5EtMlT= zLw#5~qVX3BoWRB#2;6tn(Kp|42ul0|kr0z>@lW&Ws8-nXm73KeHn{HR$?!TI+_*eG z*W*Z4s_*`3^P5RdbCI1m>pE0CUq&btyLB(ziL$45y7u%Wcd<-2k!)k8?`Sj#S`TXs z7rOdUdlGU)yvO2>UA%SyE23W-DSh5BA(7492i=7kxV|&@k(jik9s6r1Eu5hBHBGiu z9h>=s@UKvdV^7Tmj)mIzaJcY|`H*)KuGDTXwRN8L?3bjeoTo46O<*&$W5i6D(Z6i> zT3ZAB+p~-`J4kb`WD|GjRn!H#Az&N1P@R)^o+<_bbKEi!<_={EO%!j-Vd*e8t{DtP zN7!{aI!YV(U*iX?ty5fju}~6+ryAHp$Z(G9RihG(pp*AV^6!+y!)vXPE@Nf`&GB*` zglN^*vh9Dzu|C>y=Bvt~L+I>B1n|{F_06SOaHd1xtf6~>~W8>U!50rn|5 zCC5A|^oEpEyWVk^0`9_73EX;v@t5qYS$g%u)NvgP4!&6C$5QnvRDhHlaI90r9rInyetvs}*M$M^N@@!*HijzF{d|9zjWok`{2fJk{Pz>sdsDvBF zOGdP$SphtO5^;M>fU&IP@z;GF&GRN{cm!k9WaHFl(HGhlGz8mWC0i}>OjQe#XlQmy zL_rT^g+<$dt%CaMyN$@>F#lK{9Bt2b8JD&s6_0>|cw_WlBuriDa~RZ`{!pJd<#4cRMe+6Z5m>Y7lcRbzOvV;Jn7 z)-tyjBuhgu6pL6cwy6gf#aXJ_4J%nhaY`!0z!UsI?tLC$t*?Wr)5p)BdPJ-TJc*Zg zm6i}>o|NO^CCovp7hfUF9JOb2Pi{4C@^~ux1hPxyz7?hz*6lDZSizDtMy*GUlK_wUv!|^Eo}Kv7-+XIeA}oayjRgh~nuE2}kzB zB&_@a7~?CBO9g?8tK#}8mm|ei>gRI3?*R$u zYr9vmTajLi5U&xQB|((6KOQGnFbpSlK5aH;0C#vJU@azp#dU3hFr8?)-f8qx=eecMQEScThFFh-n~KgG9}H~ zy&Gql=ygAGD==zythzqkiS#=DH=mrX&Ln&L8d(O}wTkfJxlZ5H=@;?sXDoK9Z7{iA zVWI>|_^o$Tdg}{u7jZKJbt%k~Dy^neI_OM<7s`f;%)$mJ3fbNr=U*O68(RqwlPyW!>XdqH{v+e|JzCQzg#4lc+5CT)ijKF z;Q?3H!Vck+TBDL0X+5*~;!-CK#kFsP-l+m(a|be18Z7!rAf}SsVR6Qihr8p2XAH4!@XCF@_PCmC{TrdL zCBE)>%k=ugH8P!IchfOiXc+X9gdErXi`C4(?CQnRd1O?ujFUn%)KHN&PQtpdtfudt z>FisCPss-@(LEK}my%8dC>z9df(jO*XfF-c#I$=a5%C_f@C6Gw!g6x{igO zJWkX0*mopn(%-%);eAB9c`Z01_E=$NgDjILs!fPqa)IBCjuiVY9C6}dx`Z~qYuSxy z((TGtLXNozBE~{qx_6Z}j7PaXwaXEF5&g+;55QdNb8XizB)6Q4(2rD(to38N>(X{! zqj+GVM_DPj>wA5$EQODooNUZNq3Vo}ZSNR_$=U`E698-8ICk<=3))hg1=aF-NBfq0 zJ6lXySEjbnbuxPsm=wyvoXE}+Dz~j#nXAa+R4L|*GEJIojSJ=S`HTg$geh=CE8~KO z{?(J=$x^61;iUVdNFQJ*baU$I zL2@*xtbM!B;{G>kiKQP=XY_wgSgi_1x^~+aK&VZDBGFGrFSq?d%mYSTVDWu zfx6!&p4UhI*KR?vQQH2>6tIZ5`Sg%6gP}-@E1gz1+%lOyVv}QmCp% z++6th>~RJRSNQr*K3B$`%;0$SaJR`@*OM=7_J@?0caUOY22t{Gugn80Zj0Nac*UMy z^r-BwHo0a*Re|xA;r%;X%iVrYX_XDHQZboiD6M}$E)&sgrqj~kZRKe(UN)0Zf%gKJ z_Z2^ZhvK^}37cx}Ch6@1&At`*9#i9jAKHvKxRE`&C#b=hiH!qHdO7L=hwOOYWQCdn3j0C8E_bT6*&2Lb50)84W1P`$FDiCAM#3*~SLGjw zL|Tt?2OA6hAR->x4$~S_QAhGz-5IOp5sCP{`u8}zEuL6VtwHj4L^D8Ve-PK}g`L#Uv!P zZqw?BQks7ah`>bzb4!OM7S#xdvsF9gzl#<$^^TP0StIT?8w4C1aC6G-gDj*Y8@H(f zBg`MQ_?~kimhVxtAHXtSbG@G#?sd!$MH*eZ_9c@~HeHHR{MzUB5V>V>G8*0menDO> zf=1da2%n)5CYI}$ z`$jnF3A`>H^w-*=`-e7DvUYWgrEE;gJzDa*qV+o~6~7A@$cbd0Qjdvzx9?fGjrRI- z62Kx#y7@m_~%SoB|DuO?^uoqSFjz?9`WQ8_! zfktS-Njis(ic;3*LrcVHnR$<*M3~!sxa}3Hrzp6A`LINpFYyXH(}(zl@C5x56iU1( z#8Pcp?szh)J)7o`0qdA5)X)f(#(AYX?Q&f9Z+}0zCzZY-50rSpS3yjtEhx8@JC3<6 z!qBL|@?jPww>vHVCR$YdcUyUTS|JF%k9Ih{x;+a-9DmkqbcBc?5L}cHV)-lUJLsa^!6-61Z$C`iXQEvO0QRnn|s@E|kz~&4KH5oBP%Gu(qU3V)k~d0^(H^+dl4V@+(D;1J}rw#3B%3^aBG^NrDm{B z(S+RWc1K`-XdHCQwa`OX#=*2-UVZ54(EqE-wD$RDaZz3w`N-dHGcDikr^eDE4*1Wc zz{V#aC8X75&W{-O`TNu0lyw~WYPIEz+y=K_9OQ+sJYJF-W{g-blTfGtys!S`o6{M{ zC;WDMS2dA0ATF3Vp+m}+%ExKf$%_iF0?9`$yU71piD;eRHhA}#CGyXj3l=_ZNQob4 zWG53EDrC~FJPe^{`Q?5ys*>b&E(05+hO>OyVK2QOJ8U_sjY2HFgBh};W`1tCS@vN4 za@R=3DBpTjrq-Xh8Cj$9y`FevAM+3O02QFRHjb7qhtz1bW*G@g#ZqrCRnMYq!#uJW z;hQ{aTPM+rs}dgsA%QT~|ZUfJrQiy`YEtSR~i# z#pW}*R8H?{jH1Aa2u1$X>FZGcu1<{?A9n8F3nmL?HCaC66PafZ>vg4L4(SEcVl>7NZw4G8}o+ z;iG;s_UTy;bBU0x?1Wu$6gKG*S-R)dtc_~;FZI@_HHllpB=5$gW*ajhRZne^bijXZ zg~XIx@G z)+VALU1D)D7eo&LW{?oV(ZkM927-_Kn{|hGja8YV-Xd~otPn22!=!H3t*Y31XnN~gb1Vk!2M_-Anp`oajCArkT>cfB0YjMYppgt(bvNiB zg=AWBzr^@7QRtezpQ8EDPGC@X8$z7a1lRq8m<9aA71L`eB%w`nIqUo6!5Fp5OVc;D zMc^st80g96px)Pj-_dhhm$kskD3D<_l=U-4R>YSBuqxU67E8qgNrlBAj&f(*BYA-R zbU4M-yOHr4EHy&F`ldgUbKuJL_E+$lnE^y(E8&^{3>x!HS+7Yvgf7WG0c)gR0G6BQ7?Z>o9>dyj+e zg-<#wJM!dz;vRBjWePLu*;cAy2&6LhL@@@9#!c}CGXb(dlV^48cgN9`ocf$zU7<8x5uL`2r%M=-l~VwL-u z06TwfzBDW%);diR@L#BKx8ksl@Oe6>fFvuYaBc+MSv98M-TJj2+cNmB!U{K z6&Z|cF$6B)NALrc@wydbI~fQIo9pgR88O8|$sN7WQm z*I}dDzLBW`SJ7{us)KwN-b2iIP_*E{3{Wx+CHQY``Nvm!^R0Q~E&8B3I;z$VVtB})I5Lmp z&2dHiX^=-7(PHNGbifYsuY^!OcGDSR7ck$m?AcvoK!4~!&wKOvsTAW9?1we}tA-UI zT{ry-Jwk@j^;TgegT(;kwXn;omM%V&CX-`UNqVq)@D&q^i`WH?S|p`HD56i?N>ul> z`l{hNo6BE;^Gz!Zg{s}U)M>GivQxPb%S1#&+iD!zj7%Pf!@97=+Zp*7s{5!ZFGiiN z%?b-9lmusfA&H9`JEcMmsf6Xh6RoE`pQ~79UX$Y2xS`{bGh&e4`eY_^*}7CbjAqs7s$Jn zWtS5p8wyvU9u1spV*_%X#8~+T%E@=Q1H0eWjob%^W616TT>=-g`$lBVENqBT+a2?` z-Ok}j6MN5je)KhTk`*fz{h&15+1&RhY*wDREtjdA&U#Mv(l_=1RK{3!gj1{YfshEl zuapRwYIi{2KvU+tsCIs)Nxp;FJ2pG26<{1)jB=PVN^lB-1n`qm(=N7sh1WTfjJZe zHK7!08>{0t^@g13Y1PoXs73XE%nLc@i9EuwV*w4bB%7~oe`=YDtJCf0mMY`2MqCsq zLwU4e;zoO~>zf0kfvuA2QW=yPrnyCiFRK`|ATT9ss%0?f#_UF%WCcb}tr zx49WlNQk>mXUSMlrx7ly$^9tY-<<78?0EWO!SdBYqoXEY$c1E*^~yrR+vaNi?`#wT z&eCyA%6MnL>;xO_Q{sT(wtF07d1?%8=zUVkd`8{C3P$r!0)E!X1Hxto2OQulZJB`9 zgl9Y8+T&y^`X~{$tv8-6Mx`5_geiqgypqk_hOSZwzQZv)J=;hdc_c_=qTrV)O9bow ztn?*LtHf@$r5X5w^QzUF^}%6esJi#fPi?287vqim*3!7sY zMk)XnpNldZ<3nzE0|P)bN4{N;sij7*HaJK!9wp~mw{CrYMui&_Y$T(R%AmjgZEC4m zFY(>W(7oDLvx+#pV*Zv}+&7}au_+3dv}9rA|CqBEY1B4L&HmQKH1!QDIn08SSHDQ& zyU?KU1UK;m>^)`h{BjOS9@iY$+9bp0#DV202t**%;VkNzeK-H=-g!?d0R%<}-x3`m zZR}z!ma^OUzEtM4le|y?iRnEp#jM%C)U=168*Szu`56>OgaAkL?6jT!Onw@Z58Edu+ap(;8)eTk)NR0K&)Y`U(8CN{5R-SeOrlECZtMEw z-GA`%`9B&v)zSu|rOXYxY(0uELx4R`srwx7E4vhmm@YHF>9R}18qce9`WhSl{4;|A zb*w|@S_Ls8!DT8QtBc$f7c|81y1rI3v~S(7P>?H=+`P+F)++R#&4f6w{hyD;vO-;fW*i`l0uK`4}OU_7>~UJ5G?5 z{RG8&>vCK6)Sv2O)7-*4?jQc$#3(@Wu^r)bTE<@0O6f6uwO()PmxdUrT>-1_%-=zb^DNh;9vU#G`9l|`@`G8Mi?7&S@6 z?WB(P^6d?_JGuL~n!O59knN=}z&Kz0&Y`H{QTEmxRF1mV`SHhhly0S@7`q&Td!R&8 zIY81#?_@PP#RZv=zDJkplaII%uKA3-)FZW9v!8>yNTo+fT!j0#`@ljp6?_kWC%(3f z){0Y>7hzJEZJZyudulPk-K|_AS`ltPZsu^(!_9Hyi#qbKpFU=PZ_-><(2cv7mO!zS z3gX;ivcShI`&{L-d~S3IP8`^m#AoavBjJ3GoS$AYoL29`Q^wiQs810HQLZT8NbF?d zk#V*T^)Qzof19FMONtp5@SNwPa)X?2^q&jUhE#2oBstFfxI=%5Oc_RoQ*z>I*Wz;o z;V&23)EIqvlqnW&TWfPCenvNTSEQNx)JBtV&PtZW0GF>ji#ooa0W3#}Eho2JcvUXp z{JqHwY&Z5qp@uXcu2gA}wPNl*;=xPo8%4drs0E!qTvW{rKdj3lm)$&Ou4X&t@uWMu zq5YJ)Cj5z0^E%p7FTBa;&yhE|YniX*nJ&s0(xbU9z|HtHCAB^8D#;b z4Za%KyKi~Q+`$bm%`qVJV=kepJ70^P|xhyK}<;T0w!?nYDYI_G4B7fhDN znBiGSN_x?#qv_c`y!ZB5Ul`#@*?d5?g_OosoeJg#O;2XA;c9I~8N{EW@`ztE69HI6 z8Ckq|B)t;s+4Oj#v?yzx`BAFJcvfi)XCvrLFqw(jM|djEJ;viE`TY>t>U?<9d&Zk| zgukTONkeqyy#l`YI;v=n10VJu7%S~jJy)GwbjW#Jl4@Us>c>%?$gf!eoac=}0r zJSHcLIXMd$Q+nqaDD|@juxY+M8_ONXjwMg~{s;BRj*s>{)o!H%^V|!nkDz71{tb{w z${*vd_Sdl%_V)Xi{|BJ{f4aH$f2RBQKcWzlNl8v&%<;ZwsX0W>r)lP_kn=1_gd2rh z&N-iAp*f7rF~<-pF=q`^IVYzOY8Xm<-t+l>zkkGcKffP)T*v2iU9anKMBY1-A>6{z zRto5hlc58<#x3%7`FpVQ=JIC6rm6__EeIA1`EF3iKV0foMVq(M+FEp&FW=UN_rA*} z03ri1JSr>bY2G2t+N&}tk7zRp;Kd(TtPHs(Mtc7&i9Zhs%3PMb=RQ34P6>$DWsF@Y zd{)hTZl{iL88SYthw@ZaadRXU$Lx4b)6P*3WZ6ZHt`9cSauEUgXENGA1B$2{G#~uR ziJ)gDl!BU=ncr1=cEzYnHgUX|(%iYK*MZ{l@nOeyxfQ| z@jhuTTXmPBcR$Si}C<r2~;YM;o40D@I<1`^STwE#(quh0m5AJ4`{2JwMa0nY3e4@BA2w>JDCClBZ3QCD zd-$m&l?zDr3~>@7Xn57#M*wKCGjLu$R%ON|4*3VJKmAO(cyrR8+vs+WWFD)nXq9w_ZXViRfZP?#9>xN?Q*p#0}6e<=*i@v*R-%ghZ5AOOzDW+%we}00|VI|(!(n}@C zw}fAmAe<}L_PV76Ka?Gt_YnvE{+e1pxqpQT^fvUEdI_+)VS8iY$ppilpE`Sn zSy<_@D`PY*_DZRXz40iqynV?N-L~`$>S6lOFY7?YW>+_93pqFPD zwsY~ViP72Q0p(U%;^W3V4mxYEgrrbREq;$d|@$?59Paa0Q61=g+tFW0}C5UFSL3FG z!$A(FxOcbXek>NHO&d<#|q#eVw?*S>?`H&fSTh{``BFE z1ZIM>uflPt@S*!4{P?j_Bi1K_o7%#-3GO!tFVSbb+Ewa4+^`nv;upZ8+sIl-)64*=YKyw)`4-{{;J8R!RnD>ojiQIzVgmF|C?4%?UUm z|JF*oncEvX2{K=m?U7>PB|s9)t-QXL2~3Y32Zc0cyd0`gBdrB zEKsJAzy4Oj6|c+QfbKlHzx5siq&g>qN8WyA*jfU^gDp~Oi%NT}AFk5Cycd2?xoliY z*DN^dwT}V*k1rkrZ#x<8ejFp2ml7{O{d^Jm(ouXRyP$(@>=b%I_ zLpQzy;Op}*IG`Fm+}br~fGrrT@;47;L*GZ!1g@@1-FjL-UVbDQc%HYe5Zaa3L)$Kn z^^=A^E@tVSb8SkGG`OvOCJ{Ueti(|gzC?0?P)y_Cmf3Q?X#OF1_9efdiCq6`lz*a=hNS+PB59$U z{LMm1oGCDO+2$P{&Bv(KGw^;tK|qx!7zVQV#msfx^I5Z5&8jBKR<%tpZ`hf>T1|u? zztCOvL((3cJX`LvU=dJ}JJ`Y-d>E}#bZD3y6lw^(6f!Q6Xd-H>R}YQdhHtT0b{f+X z{G4Bn3(`^d*j|=gGAh9|JKz9Rq383LE(}Ke=VsNi*w#UmOVpSS8U_K)3_`3qVv6$%Z1(Qk#ub3+DLO$vn z`Kaj)g}z@fprmqJSTaGcr@8M_esY1}K@3TpO1X;IlIteEIa`%j&OYu$81mlzyn2%; z7VBROzIekcPmakqJC{4e5QGsI8!-l*dQW?SmKt%%j{>Z!b8gfUh|xaT3X+jsxU?m= ztx|hy|Lg68xya|FcJ=BA+D=|t+&2~Wr*j53o&ETrYY*9zANN$s6>+fto_7%JFHvV` zgyCG@yNB++_Wz+%aX*_Ve`C;WdpTEmsc+zIi|7@Lf%p=%EPILo@A;V2Vg23?CD&C0 z5aF{4q5DrV4@eUna}$_qr@!a8C1vELX;a}DtzbG{@Ud@d6G#C1+J<2IC6bb(qd#YC zp4j&e>!+J_r&^sT{`J`yeWJ`{FtgE)fd}CL!n_nQ8fIm=qNqF&)#CoiQk}msr|#Ed ztQ>P+&nJJ$(SCR)n0RgKi5k^9VjhM5^$*E}JFDA{2Y!Y-c$zhQi3y2j0);v+7b^(e zqf?Lk1CWy^!6V&`p}}Jitc+gGvkBH@u}1y0uSJV|lQ+F5=Tv$i@hiH)Qz7ht6do|N zp^>Tt-QsZHf#0|n@?1P5G@ZVXe;XTL_4Of*3te;?hQlYpl}E$qrQSAz{Ia~IOoACmicc_u1yu| zTPYMQNz4eY@#i=^MKaMxhI|y!Q%`zSv?At*X*C;8of%pbo^e-#Q?3;VLu2e`NV|OK z+z{y<^oeNc+IE9OH;Ws0*ZZVm{ZxlnZck-t1&eyF894i~K-Fz#CR- zcehChF_c<780Y%Ubw6h#BIzNP6@5P!*TtWWLU$UpU5S zbe7A$#RWvRq}Tp73Rlas5t6;{Q=&E!k8f!&dC5YSvK+mn&j-S@;2eO zv5y4;@-^U%8b3b&x4>bp#GlLqrs-t-zjyS@oUVG!tb{#!d*JqsrBwNUah6L8sox(m zYksKGlM!}Co{c+UUr1)kS6KZ8a8dBTHi4o09?-?oZvOhBR3EKlOFO^69YBiO_?&Q> z4f|oMk4x=EmWd9yNqu}cpLX9lB{G0?y$rb8qh}q&;=;Y5y*TVAAQqet5C#V_1q@xT ze?Jp^TAG5F&h!qdupKvVd67f^hT~%KpJ#7CwzrISZ?t``1?rBTA;9^ zV}|>*U^#4`rqp+gTk{V=n2$gRUd!0txaF#xhj&762iHsAH<|PZTm(}!c2#=vsBsqc z)sf^;x8AeGmiKe+YY%eFYA5fzA43j@I0$}MeA1OuDzYm&aUUM$Oe5*MJ~TodG54*< zVjlNyDl#*|%ib8>4Qzzq|Fn~z?VRieZ&xg)5hv{G|C zCP!?CFmjM-wSA8^;(d4c7WRZPux-VAPd?{*#!7+BIbVI(*he?@4SduLv$(Q0D}TQ; z8Exi9lk5SeIPr!uAo#7QdF~Y+I?J0($EZs7RlBOz&Dp<41AQTOo_{|!=B`%owrwK( zIWc*f9hbG%#KkJE?ppQ6mF!}eBfju^#`-!0(<*XX)}LdLcJS=~j6sIZFXAbX7o9wW zjVmOwL_BI9Ob;oa4#x=WAT73ht*~v4g{+>*75!qEU&0BN^HVE+&Y?*qc9qhXdQ;wL zMfZN_ae@iSC%g4zm?Rc(^zvmW?v1mCyE6f0AWo9xMau2{_1%5C-zCw!nda?n)!CnD zHY*F0%^2>d*ZZ~ib5^{!@#i_eX-nn0Wo!g_B`$~i>dt7zr2cHmMW@~;@Lp>H~?D0-Ju~`%ML*3ZIys0~ zdh7`z*dKW&{EE-4%Q&Eu>o2Ls&`zBr?QaP&V@x1^um?@z0{(oq-r);%3m=HkIKHuBRyF*{BRCo z>V-$3ox;7%|^urra2eMu@<@$$ct22uIknknBw+<5m8@ll!Jhr@|`lVcB(?W3?#V>=4b zd|7>S?j+9&7dL)rZ95fLmn*t%5aNqeVA32LwBBfFd1?$P6l|7^>d5I_WrM9gB)wKMGt|Toz~NJ#~hD1EeJ!lO}iz;vQfu+K{xGCzImXE+~0a z>1$JYXRu$b;@PW#e#bJ0DF*{!TmyKeU<4JfJ)1noGa~8DpZKcMJfoZ&2Ab3}gSUO{ ziE|=gV(CnNqGJqJ%bk>wEYf7<*8|?%0O!&wy$Yf%tY=dcd$qenj3j*w z5as7&2^`tul{}$t24R<9%@RC+nGV`zG8P0tgFFlt7oZ$M3XUhA+45Qrl-rtT!LZF@ zJ8d7`qs|ey4C_99@B9IrEdhZT$rWW`9H$gJ)^E~kQuyUn7TBT6Ysj0#%RvQ~YyGZJ ii(!2C|Kf*_{-d$`aIVp(nxM{L$WYf*r%LNi#D4%7deC$L literal 0 HcmV?d00001 diff --git a/app/src/assets/localization/en/anonymous.json b/app/src/assets/localization/en/anonymous.json index 6d4a6d9b563..12e57d595fa 100644 --- a/app/src/assets/localization/en/anonymous.json +++ b/app/src/assets/localization/en/anonymous.json @@ -2,6 +2,8 @@ "a_robot_software_update_is_available": "A robot software update is required to run protocols with this version of the desktop app. Go to Robot", "about_flex_gripper": "About Gripper", "alternative_security_types_description": "The robot supports connecting to various enterprise access points. Connect via USB and finish setup in the desktop app.", + "attach_a_pipette": "Attach a pipette to your robot", + "attach_a_pipette_for_quick_transfer": "To create a quick transfer, you need to attach a pipette to your robot.", "calibration_block_description": "This block is a specially made tool that fits perfectly on your deck and helps with calibration.If you do not have a Calibration Block, please email support so we can send you one. In your message, be sure to include your name, company or institution name, and shipping address. While you wait for the block to arrive, you can use the flat surface on the trash bin of your robot instead.", "calibration_on_opentrons_tips_is_important": "It’s extremely important to perform this calibration using the tips and tip racks specified above, as the robot determines accuracy based on the known measurements of these tips.", "choose_what_data_to_share": "Choose what robot data to share.", @@ -63,6 +65,7 @@ "share_logs_with_opentrons_description": "Help improve this product by automatically sending anonymous robot logs. These logs are used to troubleshoot robot issues and spot error trends.", "show_labware_offset_snippets_description": "Only for users who need to apply labware offset data outside of the app. When enabled, code snippets for Jupyter Notebook and SSH are available during protocol setup.", "something_seems_wrong": "There may be a problem with your pipette. Exit setup and contact support for assistance.", + "storage_limit_reached_description": "Your robot has reached the limit of quick transfers that it can store. You must delete an existing quick transfer before creating a new one.", "these_are_advanced_settings": "These are advanced settings. Please do not attempt to adjust without assistance from support. Changing these settings may affect the lifespan of your pipette.These settings do not override any pipette settings defined in protocols.", "update_requires_restarting_app": "Updating requires restarting the app.", "update_robot_software_description": "Bypass the auto-update process and update the robot software manually.", diff --git a/app/src/assets/localization/en/branded.json b/app/src/assets/localization/en/branded.json index 334671e69e9..6a65184183f 100644 --- a/app/src/assets/localization/en/branded.json +++ b/app/src/assets/localization/en/branded.json @@ -1,5 +1,7 @@ { "a_robot_software_update_is_available": "A robot software update is required to run protocols with this version of the Opentrons App. Go to Robot", + "attach_a_pipette": "Attach a pipette to your Flex", + "attach_a_pipette_for_quick_transfer": "To create a quick transfer, you need to attach a pipette to your Opentrons Flex.", "about_flex_gripper": "About Flex Gripper", "alternative_security_types_description": "The Opentrons App supports connecting Flex to various enterprise access points. Connect via USB and finish setup in the app.", "calibration_block_description": "This block is a specially made tool that fits perfectly on your deck and helps with calibration.If you do not have a Calibration Block, please email support@opentrons.com so we can send you one. In your message, be sure to include your name, company or institution name, and shipping address. While you wait for the block to arrive, you can use the flat surface on the trash bin of your robot instead.", @@ -63,6 +65,7 @@ "share_logs_with_opentrons_description": "Help Opentrons improve its products and services by automatically sending anonymous robot logs. Opentrons uses these logs to troubleshoot robot issues and spot error trends.", "show_labware_offset_snippets_description": "Only for users who need to apply labware offset data outside of the Opentrons App. When enabled, code snippets for Jupyter Notebook and SSH are available during protocol setup.", "something_seems_wrong": "There may be a problem with your pipette. Exit setup and contact Opentrons Support for assistance.", + "storage_limit_reached_description": "Your Opentrons Flex has reached the limit of quick transfers that it can store. You must delete an existing quick transfer before creating a new one.", "these_are_advanced_settings": "These are advanced settings. Please do not attempt to adjust without assistance from Opentrons Support. Changing these settings may affect the lifespan of your pipette.These settings do not override any pipette settings defined in protocols.", "update_requires_restarting_app": "Updating requires restarting the Opentrons App.", "update_robot_software_description": "Bypass the Opentrons App auto-update process and update the robot software manually.", diff --git a/app/src/assets/localization/en/quick_transfer.json b/app/src/assets/localization/en/quick_transfer.json index 3a0d81ffe79..f40298d6ae1 100644 --- a/app/src/assets/localization/en/quick_transfer.json +++ b/app/src/assets/localization/en/quick_transfer.json @@ -1,4 +1,5 @@ { + "a_way_to_move_liquid": "A way to move a single liquid from one labware to another.", "add_or_remove": "add or remove", "add_or_remove_columns": "add or remove columns", "advanced_setting_disabled": "Advanced setting disabled for this transfer", @@ -16,6 +17,7 @@ "aspirate_tip_position": "Aspirate tip position", "aspirate_volume": "Aspirate volume per well", "aspirate_volume_µL": "Aspirate volume per well (µL)", + "attach_pipette": "Attach pipette", "blow_out": "Blowout", "blow_out_after_dispensing": "Blowout after dispensing", "blow_out_destination_well": "Destination well", @@ -58,6 +60,7 @@ "exit_quick_transfer": "Exit quick transfer?", "flow_rate_value": "{{flow_rate}} µL/s", "failed_analysis": "failed analysis", + "got_it": "Got it", "grid": "grid", "grids": "grids", "labware": "Labware", @@ -114,6 +117,7 @@ "source_labware": "Source labware", "source_labware_d2": "Source labware in D2", "starting_well": "starting well", + "storage_limit_reached": "Storage limit reached", "tip_drop_location": "Tip drop location", "tip_management": "Tip management", "tip_position": "Tip position", @@ -139,6 +143,7 @@ "volume_per_well_µL": "Volume per well (µL)", "wasteChute": "Waste chute", "wasteChute_location": "Waste chute in {{slotName}}", + "welcome_to_quick_transfer": "Welcome to quick transfer!", "well": "well", "wellPlate": "Well plates", "well_ratio": "Quick transfers with multiple source wells can either be one-to-one (select {{wells}} for this transfer) or consolidate (select 1 destination well).", diff --git a/app/src/molecules/BackgroundOverlay/index.tsx b/app/src/molecules/BackgroundOverlay/index.tsx index fcc8956e423..ccfdb273fc4 100644 --- a/app/src/molecules/BackgroundOverlay/index.tsx +++ b/app/src/molecules/BackgroundOverlay/index.tsx @@ -6,7 +6,7 @@ import { COLORS, Flex, POSITION_FIXED } from '@opentrons/components' const BACKGROUND_OVERLAY_STYLE = css` position: ${POSITION_FIXED}; inset: 0; - z-index: 3; + z-index: 4; background-color: ${COLORS.black90}${COLORS.opacity60HexCode}; ` diff --git a/app/src/pages/QuickTransferDashboard/IntroductoryModal.tsx b/app/src/pages/QuickTransferDashboard/IntroductoryModal.tsx new file mode 100644 index 00000000000..560ceac280a --- /dev/null +++ b/app/src/pages/QuickTransferDashboard/IntroductoryModal.tsx @@ -0,0 +1,56 @@ +import * as React from 'react' +import { useTranslation } from 'react-i18next' +import { + SPACING, + StyledText, + Flex, + DIRECTION_COLUMN, + ALIGN_CENTER, + TEXT_ALIGN_CENTER, +} from '@opentrons/components' +import { Modal } from '../../molecules/Modal' +import { SmallButton } from '../../atoms/buttons' + +import imgSrc from '../../assets/images/on-device-display/odd-abstract-6.png' + +interface IntroductoryModalProps { + onClose: () => void +} + +export const IntroductoryModal = ( + props: IntroductoryModalProps +): JSX.Element => { + const { t } = useTranslation(['quick_transfer', 'shared']) + + return ( + + + {t('welcome_to_quick_transfer')} + + {t('welcome_to_quick_transfer')} + + + {t('a_way_to_move_liquid')} + + + + + + + ) +} diff --git a/app/src/pages/QuickTransferDashboard/PipetteNotAttachedErrorModal.tsx b/app/src/pages/QuickTransferDashboard/PipetteNotAttachedErrorModal.tsx new file mode 100644 index 00000000000..a738a728139 --- /dev/null +++ b/app/src/pages/QuickTransferDashboard/PipetteNotAttachedErrorModal.tsx @@ -0,0 +1,56 @@ +import * as React from 'react' +import { useTranslation } from 'react-i18next' +import { + SPACING, + COLORS, + LegacyStyledText, + Flex, + DIRECTION_COLUMN, + TYPOGRAPHY, +} from '@opentrons/components' +import { Modal } from '../../molecules/Modal' +import { SmallButton } from '../../atoms/buttons' + +interface PipetteNotAttachedErrorModalProps { + onExit: () => void + onAttach: () => void +} + +export const PipetteNotAttachedErrorModal = ( + props: PipetteNotAttachedErrorModalProps +): JSX.Element => { + const { i18n, t } = useTranslation(['quick_transfer', 'shared', 'branded']) + + return ( + + + + {t('branded:attach_a_pipette_for_quick_transfer')} + + + + + + + + ) +} diff --git a/app/src/pages/QuickTransferDashboard/StorageLimitReachedErrorModal.tsx b/app/src/pages/QuickTransferDashboard/StorageLimitReachedErrorModal.tsx new file mode 100644 index 00000000000..d4c8a562306 --- /dev/null +++ b/app/src/pages/QuickTransferDashboard/StorageLimitReachedErrorModal.tsx @@ -0,0 +1,49 @@ +import * as React from 'react' +import { useTranslation } from 'react-i18next' +import { + SPACING, + COLORS, + LegacyStyledText, + Flex, + DIRECTION_COLUMN, + TYPOGRAPHY, +} from '@opentrons/components' +import { Modal } from '../../molecules/Modal' +import { SmallButton } from '../../atoms/buttons' + +interface StorageLimitReachedErrorModalProps { + onExit: () => void +} + +export const StorageLimitReachedErrorModal = ( + props: StorageLimitReachedErrorModalProps +): JSX.Element => { + const { i18n, t } = useTranslation(['quick_transfer', 'shared', 'branded']) + + return ( + + + + {t('branded:storage_limit_reached_description')} + + + + + + + ) +} diff --git a/app/src/pages/QuickTransferDashboard/index.tsx b/app/src/pages/QuickTransferDashboard/index.tsx index 7d75986e757..ed35db7fedc 100644 --- a/app/src/pages/QuickTransferDashboard/index.tsx +++ b/app/src/pages/QuickTransferDashboard/index.tsx @@ -16,27 +16,36 @@ import { SPACING, LegacyStyledText, } from '@opentrons/components' -import { useAllProtocolsQuery } from '@opentrons/react-api-client' +import { + useAllProtocolsQuery, + useInstrumentsQuery, +} from '@opentrons/react-api-client' import { SmallButton, FloatingActionButton } from '../../atoms/buttons' import { Navigation } from '../../organisms/Navigation' import { getPinnedQuickTransferIds, getQuickTransfersOnDeviceSortKey, + getHasDismissedQuickTransferIntro, updateConfigValue, } from '../../redux/config' import { PinnedTransferCarousel } from './PinnedTransferCarousel' import { sortQuickTransfers } from './utils' import { QuickTransferCard } from './QuickTransferCard' import { NoQuickTransfers } from './NoQuickTransfers' +import { PipetteNotAttachedErrorModal } from './PipetteNotAttachedErrorModal' +import { StorageLimitReachedErrorModal } from './StorageLimitReachedErrorModal' +import { IntroductoryModal } from './IntroductoryModal' import { DeleteTransferConfirmationModal } from './DeleteTransferConfirmationModal' import type { ProtocolResource } from '@opentrons/shared-data' +import type { PipetteData } from '@opentrons/api-client' import type { Dispatch } from '../../redux/types' import type { QuickTransfersOnDeviceSortKey } from '../../redux/config/types' export function QuickTransferDashboard(): JSX.Element { const protocols = useAllProtocolsQuery() + const { data: attachedInstruments } = useInstrumentsQuery() const history = useHistory() const { t } = useTranslation(['quick_transfer', 'protocol_info']) const dispatch = useDispatch() @@ -49,8 +58,21 @@ export function QuickTransferDashboard(): JSX.Element { showDeleteConfirmationModal, setShowDeleteConfirmationModal, ] = React.useState(false) + const [ + showPipetteNotAttachedModal, + setShowPipetteNotAttaachedModal, + ] = React.useState(false) + const [ + showStorageLimitReachedModal, + setShowStorageLimitReachedModal, + ] = React.useState(false) const [targetTransferId, setTargetTransferId] = React.useState('') const sortBy = useSelector(getQuickTransfersOnDeviceSortKey) ?? 'alphabetical' + const hasDismissedIntro = useSelector(getHasDismissedQuickTransferIntro) + + const pipetteIsAttached = attachedInstruments?.data.some( + (i): i is PipetteData => i.ok && i.instrumentType === 'pipette' + ) const quickTransfersData = protocols.data?.data.filter(protocol => { return protocol.protocolKind === 'quick-transfer' @@ -123,14 +145,53 @@ export function QuickTransferDashboard(): JSX.Element { } } + const handleCreateNewQuickTransfer = (): void => { + if (!pipetteIsAttached) { + setShowPipetteNotAttaachedModal(true) + } else if (quickTransfersData.length >= 20) { + setShowStorageLimitReachedModal(true) + } else { + history.push('/quick-transfer/new') + } + } + return ( <> + {!hasDismissedIntro ? ( + + dispatch( + updateConfigValue( + 'protocols.hasDismissedQuickTransferIntro', + true + ) + ) + } + /> + ) : null} {showDeleteConfirmationModal ? ( ) : null} + {showPipetteNotAttachedModal ? ( + { + setShowPipetteNotAttaachedModal(false) + }} + onAttach={() => { + history.push('/instruments') + }} + /> + ) : null} + {showStorageLimitReachedModal ? ( + { + setShowStorageLimitReachedModal(false) + }} + /> + ) : null} { - history.push('/quick-transfer/new') - }} + onClick={handleCreateNewQuickTransfer} /> ) From a03199b31f82e8d18d0b4589d42f4720204e620b Mon Sep 17 00:00:00 2001 From: koji Date: Thu, 11 Jul 2024 17:12:58 -0400 Subject: [PATCH 03/78] fix(components): fix location icon font size for desktop app (#15616) * fix(components): fix location icon font size for desktop app --- .../LocationIcon/__tests__/LocationIcon.test.tsx | 7 +++++-- components/src/molecules/LocationIcon/index.tsx | 10 ++++++++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/components/src/molecules/LocationIcon/__tests__/LocationIcon.test.tsx b/components/src/molecules/LocationIcon/__tests__/LocationIcon.test.tsx index 120a05096dd..1750d594d1d 100644 --- a/components/src/molecules/LocationIcon/__tests__/LocationIcon.test.tsx +++ b/components/src/molecules/LocationIcon/__tests__/LocationIcon.test.tsx @@ -2,7 +2,7 @@ import * as React from 'react' import { describe, it, beforeEach, expect } from 'vitest' import { renderWithProviders } from '../../../testing/utils' import { screen } from '@testing-library/react' -import { SPACING } from '../../../ui-style-constants' +import { SPACING, TYPOGRAPHY } from '../../../ui-style-constants' import { BORDERS, COLORS } from '../../../helix-design-system' import { LocationIcon } from '..' @@ -36,7 +36,10 @@ describe('LocationIcon', () => { it('should render slot name', () => { render(props) - screen.getByText('A1') + const text = screen.getByText('A1') + expect(text).toHaveStyle(`font-size: ${TYPOGRAPHY.fontSizeCaption}`) + expect(text).toHaveStyle('line-height: normal') + expect(text).toHaveStyle(` font-weight: ${TYPOGRAPHY.fontWeightBold}`) }) it('should render an icon', () => { diff --git a/components/src/molecules/LocationIcon/index.tsx b/components/src/molecules/LocationIcon/index.tsx index 773efbdbbef..95be8e7b8f9 100644 --- a/components/src/molecules/LocationIcon/index.tsx +++ b/components/src/molecules/LocationIcon/index.tsx @@ -37,7 +37,7 @@ const LOCATION_ICON_STYLE = css<{ padding: ${SPACING.spacing2} ${SPACING.spacing4}; border-radius: ${BORDERS.borderRadius4}; justify-content: ${JUSTIFY_CENTER}; - height: max-content; + height: max-content; // prevents the icon from being squished @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { border: 2px solid ${props => props.color ?? COLORS.black90}; @@ -49,7 +49,13 @@ const LOCATION_ICON_STYLE = css<{ ` const SLOT_NAME_TEXT_STYLE = css` - ${TYPOGRAPHY.smallBodyTextBold} + font-size: ${TYPOGRAPHY.fontSizeCaption}; + line-height: normal; + font-weight: ${TYPOGRAPHY.fontWeightBold}; + + @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { + ${TYPOGRAPHY.smallBodyTextBold} + } ` export function LocationIcon({ From ec3912ac19f84a7f41ee75baa1359b59ec4f75af Mon Sep 17 00:00:00 2001 From: Caila Marashaj <98041399+caila-marashaj@users.noreply.github.com> Date: Thu, 11 Jul 2024 17:14:33 -0400 Subject: [PATCH 04/78] fix(hardware): gripper calibration fix (#15630) --- .../backends/ot3controller.py | 1 - .../hardware_control/tool_sensors.py | 23 +++++++++++++------ .../hardware_control/test_tool_sensors.py | 2 +- 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/api/src/opentrons/hardware_control/backends/ot3controller.py b/api/src/opentrons/hardware_control/backends/ot3controller.py index fd901955022..766125ea1ea 100644 --- a/api/src/opentrons/hardware_control/backends/ot3controller.py +++ b/api/src/opentrons/hardware_control/backends/ot3controller.py @@ -1461,7 +1461,6 @@ async def capacitive_probe( tool=sensor_node_for_mount(mount), mover=axis_to_node(moving), distance=distance_mm, - plunger_speed=speed_mm_per_s, mount_speed=speed_mm_per_s, csv_output=csv_output, sync_buffer_output=sync_buffer_output, diff --git a/hardware/opentrons_hardware/hardware_control/tool_sensors.py b/hardware/opentrons_hardware/hardware_control/tool_sensors.py index c4ea96fccff..6b762ef7c30 100644 --- a/hardware/opentrons_hardware/hardware_control/tool_sensors.py +++ b/hardware/opentrons_hardware/hardware_control/tool_sensors.py @@ -519,7 +519,6 @@ async def capacitive_probe( tool: InstrumentProbeTarget, mover: NodeId, distance: float, - plunger_speed: float, mount_speed: float, sensor_id: SensorId = SensorId.S0, relative_threshold_pf: float = 1.0, @@ -538,6 +537,8 @@ async def capacitive_probe( """ log_files: Dict[SensorId, str] = {} if not data_files else data_files sensor_driver = SensorDriver() + pipette_present = tool in [NodeId.pipette_left, NodeId.pipette_right] + capacitive_sensors = await _setup_capacitive_sensors( messenger, sensor_id, @@ -546,10 +547,18 @@ async def capacitive_probe( sensor_driver, ) + probe_distance = {mover: distance} + probe_speed = {mover: mount_speed} + movers = [mover] + if pipette_present: + probe_distance[tool] = 0.0 + probe_speed[tool] = 0.0 + movers.append(tool) + sensor_group = _build_pass_step( - movers=[mover, tool], - distance={mover: distance, tool: 0.0}, - speed={mover: mount_speed, tool: 0.0}, + movers=movers, + distance=probe_distance, + speed=probe_speed, sensor_type=SensorType.capacitive, sensor_id=sensor_id, stop_condition=MoveStopCondition.sync_line, @@ -557,9 +566,9 @@ async def capacitive_probe( if sync_buffer_output: sensor_group = _fix_pass_step_for_buffer( sensor_group, - movers=[mover, tool], - distance={mover: distance, tool: distance}, - speed={mover: mount_speed, tool: plunger_speed}, + movers=movers, + distance=probe_distance, + speed=probe_speed, sensor_type=SensorType.capacitive, sensor_id=sensor_id, stop_condition=MoveStopCondition.sync_line, diff --git a/hardware/tests/opentrons_hardware/hardware_control/test_tool_sensors.py b/hardware/tests/opentrons_hardware/hardware_control/test_tool_sensors.py index 4065874739b..86a1d2d40a7 100644 --- a/hardware/tests/opentrons_hardware/hardware_control/test_tool_sensors.py +++ b/hardware/tests/opentrons_hardware/hardware_control/test_tool_sensors.py @@ -445,7 +445,7 @@ def move_responder( message_send_loopback.add_responder(move_responder) status = await capacitive_probe( - mock_messenger, target_node, motor_node, distance, speed, speed + mock_messenger, target_node, motor_node, distance, speed ) assert status.motor_position == 10 # this comes from the current_position_um above assert status.encoder_position == 10 From b2239a1cbb9a0ae8abcebbd2eea415d672ba75c4 Mon Sep 17 00:00:00 2001 From: Josh McVey Date: Thu, 11 Jul 2024 22:24:36 -0500 Subject: [PATCH 05/78] chore(release): internal release notes 1.5.0 to 2.0.0-alpha.1 (#15606) ## Internal release notes catch up ![image](https://github.com/Opentrons/opentrons/assets/502770/24d33498-5584-45c7-b1d3-a4915f25f21b) ### Merge to edge before cutting `2.0.0-alpha.1` > [!NOTE] > These feel perfunctory on most of our internal releases but there may one day be an internal release where they are not. --- api/release-notes-internal.md | 18 ++++++++++++++++++ app-shell/build/release-notes-internal.md | 18 ++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/api/release-notes-internal.md b/api/release-notes-internal.md index 353df2e8833..4cffb961116 100644 --- a/api/release-notes-internal.md +++ b/api/release-notes-internal.md @@ -2,6 +2,24 @@ For more details about this release, please see the full [technical change log][ [technical change log]: https://github.com/Opentrons/opentrons/releases +## Internal Release 2.0.0-alpha.1 + +This internal release, pulled from the `edge` branch, contains features being developed for 8.0.0. It's for internal testing only. Usage may require a robot factory reset to restore robot stability. + + + +## Internal Release 2.0.0-alpha.0 + +This internal release, pulled from the `edge` branch, contains features being developed for 8.0.0. It's for internal testing only. Usage may require a robot factory reset to restore robot stability. + + + +## Internal Release 1.5.0 + +This internal release is from the `edge` branch to contain rapid dev on new features for 7.3.0. This release is for internal testing purposes and if used may require a factory reset of the robot to return to a stable version. Though designated as stable, this build contains many critical bugs and should not be used in production. + + + ## Internal Release 1.5.0-alpha.1 This internal release is from the `edge` branch to contain rapid dev on new features for 7.3.0. This release is for internal testing purposes and if used may require a factory reset of the robot to return to a stable version. diff --git a/app-shell/build/release-notes-internal.md b/app-shell/build/release-notes-internal.md index e6925397157..c2193890ead 100644 --- a/app-shell/build/release-notes-internal.md +++ b/app-shell/build/release-notes-internal.md @@ -1,6 +1,24 @@ For more details about this release, please see the full [technical changelog][]. [technical change log]: https://github.com/Opentrons/opentrons/releases +## Internal Release 2.0.0-alpha.1 + +This internal release, pulled from the `edge` branch, contains features being developed for 8.0.0. It's for internal testing only. Usage may require a robot factory reset to restore robot stability. + + + +## Internal Release 2.0.0-alpha.0 + +This internal release, pulled from the `edge` branch, contains features being developed for 8.0.0. It's for internal testing only. Usage may require a robot factory reset to restore robot stability. + + + +## Internal Release 1.5.0 + +This internal release is from the `edge` branch to contain rapid dev on new features for 7.3.0. This release is for internal testing purposes and if used may require a factory reset of the robot to return to a stable version. Though designated as stable, this build contains many critical bugs and should not be used in production. + + + ## Internal Release 1.5.0-alpha.1 This internal release is from the `edge` branch to contain rapid dev on new features for 7.3.0. This release is for internal testing purposes and if used may require a factory reset of the robot to return to a stable version. From 7a9479ca0d30706ee4ea9efdfe98bc3166f9f275 Mon Sep 17 00:00:00 2001 From: Rhyann Clarke <146747548+rclarke0@users.noreply.github.com> Date: Fri, 12 Jul 2024 09:15:32 -0400 Subject: [PATCH 06/78] feat(abr-testing): Compare LPC of errored labware to LPC data (#15637) # Overview Compares LPC of labware in error step to average LPC coordinates of historical runs. # Test Plan Tested on robots with errors including and excluding labware in their error step. # Changelog - Gets LPC coordinates of labware from errored protocol step - Connects to ABR LPC sheet - Filters data to match robot, labware type, adaptor, module, and slot location - Finds average of each coordinate that was not from a run with an error - Creates string that is posted in jira ticket description. # Review requests # Risk assessment --- .../data_collection/abr_robot_error.py | 80 ++++++++++++++++++- 1 file changed, 78 insertions(+), 2 deletions(-) diff --git a/abr-testing/abr_testing/data_collection/abr_robot_error.py b/abr-testing/abr_testing/data_collection/abr_robot_error.py index 9e1569f47af..8ca606004f5 100644 --- a/abr-testing/abr_testing/data_collection/abr_robot_error.py +++ b/abr-testing/abr_testing/data_collection/abr_robot_error.py @@ -1,5 +1,5 @@ """Create ticket for robot with error.""" -from typing import List, Tuple, Any +from typing import List, Tuple, Any, Dict from abr_testing.data_collection import read_robot_logs, abr_google_drive, get_run_logs import requests import argparse @@ -10,6 +10,49 @@ import sys import json import re +import pandas as pd + + +def compare_lpc_to_historical_data( + labware_dict: Dict[str, Any], robot: str, storage_directory: str +) -> str: + """Compare LPC data of slot error occurred in to historical relevant data.""" + # Connect to LPC Google Sheet and get data. + credentials_path = os.path.join(storage_directory, "credentials.json") + google_sheet_lpc = google_sheets_tool.google_sheet(credentials_path, "ABR-LPC", 0) + headers = google_sheet_lpc.get_row(1) + all_lpc_data = google_sheet_lpc.get_all_data(expected_headers=headers) + df_lpc_data = pd.DataFrame(all_lpc_data) + labware = labware_dict["Labware Type"] + slot = labware_dict["Slot"] + # Filter data to match to appropriate labware and slot. + # Discludes any run with an error. + relevant_lpc = df_lpc_data[ + (df_lpc_data["Slot"] == slot) + & (df_lpc_data["Labware Type"] == labware) + & (df_lpc_data["Robot"] == robot) + & (df_lpc_data["Module"] == labware_dict["Module"]) + & (df_lpc_data["Adapter"] == labware_dict["Adapter"]) + & (df_lpc_data["Errors"] < 1) + ] + # Converts coordinates to floats and finds averages. + x_float = [float(value) for value in relevant_lpc["X"]] + y_float = [float(value) for value in relevant_lpc["Y"]] + z_float = [float(value) for value in relevant_lpc["Z"]] + current_x = round(labware_dict["X"], 2) + current_y = round(labware_dict["Y"], 2) + current_z = round(labware_dict["Z"], 2) + avg_x = round(sum(x_float) / len(x_float), 2) + avg_y = round(sum(y_float) / len(y_float), 2) + avg_z = round(sum(z_float) / len(z_float), 2) + + # Formats LPC message for ticket. + lpc_message = ( + f"There were {len(x_float)} LPC coords found for {labware} at {slot}. " + f"AVERAGE POSITION: ({avg_x}, {avg_y}, {avg_z}). " + f"CURRENT POSITION: ({current_x}, {current_y}, {current_z})" + ) + return lpc_message def read_each_log(folder_path: str, issue_url: str) -> None: @@ -199,11 +242,44 @@ def get_run_error_info_from_robot( description["protocol_name"] = results["protocol"]["metadata"].get( "protocolName", "" ) + # Get LPC coordinates of labware of failure + lpc_dict = results["labwareOffsets"] + labware_dict = results["labware"] description["error"] = " ".join([error_code, error_type, error_instrument]) - description["protocol_step"] = list(results["commands"])[-1] + protocol_step = list(results["commands"])[-1] + errored_labware_id = protocol_step["params"].get("labwareId", "") + errored_labware_dict = {} + lpc_message = "" + # If there is labware included in the error message, its LPC coords will be extracted. + if len(errored_labware_id) > 0: + for labware in labware_dict: + if labware["id"] == errored_labware_id: + errored_labware_dict["Slot"] = labware["location"].get("slotName", "") + errored_labware_dict["Labware Type"] = labware.get("definitionUri", "") + offset_id = labware.get("offsetId", "") + for lpc in lpc_dict: + if lpc.get("id", "") == offset_id: + errored_labware_dict["X"] = lpc["vector"].get("x", "") + errored_labware_dict["Y"] = lpc["vector"].get("y", "") + errored_labware_dict["Z"] = lpc["vector"].get("z", "") + errored_labware_dict["Module"] = lpc["location"].get( + "moduleModel", "" + ) + errored_labware_dict["Adapter"] = lpc["location"].get( + "definitionUri", "" + ) + + lpc_message = compare_lpc_to_historical_data( + errored_labware_dict, parent, storage_directory + ) + + description["protocol_step"] = protocol_step description["right_mount"] = results.get("right", "No attachment") description["left_mount"] = results.get("left", "No attachment") description["gripper"] = results.get("extension", "No attachment") + if len(lpc_message) < 1: + lpc_message = "No LPC coordinates found in relation to error." + description["LPC Comparison"] = lpc_message all_modules = abr_google_drive.get_modules(results) whole_description = {**description, **all_modules} whole_description_str = ( From 9769e7023440b7abfd2038dd0d35427f6ded9a2f Mon Sep 17 00:00:00 2001 From: Nicholas Shiland Date: Fri, 12 Jul 2024 10:17:09 -0400 Subject: [PATCH 07/78] Making the scale script usable for robots not doing liquid measurement. (#15628) # Overview # Test Plan # Changelog # Review requests # Risk assessment --- abr-testing/abr_testing/tools/abr_scale.py | 26 +++++++++++++++------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/abr-testing/abr_testing/tools/abr_scale.py b/abr-testing/abr_testing/tools/abr_scale.py index 6d83df03f2b..ad526792cf8 100644 --- a/abr-testing/abr_testing/tools/abr_scale.py +++ b/abr-testing/abr_testing/tools/abr_scale.py @@ -34,19 +34,29 @@ def get_protocol_step_as_int( # create an dict copying the contents of IP_N_Volumes try: ip_file = json.load(open(ip_json_file)) + try: + # grab IP and volume from the dict + tot_info = ip_file["information"] + robot_info = tot_info[robot] + IP_add = robot_info["IP"] + exp_volume = robot_info["volume"] + # sets return variables equal to those grabbed from the sheet + ip = IP_add + expected_liquid_moved = float(exp_volume) + except KeyError: + ip = input("Robot IP: ") + while True: + try: + expected_liquid_moved = float(input("Expected volume moved: ")) + if expected_liquid_moved >= 0 or expected_liquid_moved <= 0: + break + except ValueError: + print("Expected liquid moved volume should be an float.") except FileNotFoundError: print( f"Please add json file with robot IPs and expected volumes to: {storage_directory}." ) sys.exit() - # grab IP and volume from the dict - tot_info = ip_file["information"] - robot_info = tot_info[robot] - IP_add = robot_info["IP"] - exp_volume = robot_info["volume"] - # sets return variables equal to those grabbed from the sheet - ip = IP_add - expected_liquid_moved = float(exp_volume) return protocol_step, expected_liquid_moved, ip From a1b3953f15dc1d05a858a184474b7c0037f91a54 Mon Sep 17 00:00:00 2001 From: Ryan Howard Date: Fri, 12 Jul 2024 12:24:39 -0400 Subject: [PATCH 08/78] chore(api): make variable names in instrument context match protocol context (#15605) # Overview Feedback from Ed and Joe was that we should have the setter/getter match the variable name used when you load_instrument # Test Plan # Changelog # Review requests # Risk assessment --- api/src/opentrons/protocol_api/instrument_context.py | 6 +++--- api/tests/opentrons/protocol_api/test_instrument_context.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/api/src/opentrons/protocol_api/instrument_context.py b/api/src/opentrons/protocol_api/instrument_context.py index 7f2f86463ea..003b8817049 100644 --- a/api/src/opentrons/protocol_api/instrument_context.py +++ b/api/src/opentrons/protocol_api/instrument_context.py @@ -1678,7 +1678,7 @@ def tip_racks(self, racks: List[labware.Labware]) -> None: @property @requires_version(2, 20) - def liquid_detection(self) -> bool: + def liquid_presence_detection(self) -> bool: """ Gets the global setting for liquid level detection. @@ -1689,9 +1689,9 @@ def liquid_detection(self) -> bool: """ return self._core.get_liquid_presence_detection() - @liquid_detection.setter + @liquid_presence_detection.setter @requires_version(2, 20) - def liquid_detection(self, enable: bool) -> None: + def liquid_presence_detection(self, enable: bool) -> None: self._core.set_liquid_presence_detection(enable) @property diff --git a/api/tests/opentrons/protocol_api/test_instrument_context.py b/api/tests/opentrons/protocol_api/test_instrument_context.py index 1e3af474497..bd9d9c7c736 100644 --- a/api/tests/opentrons/protocol_api/test_instrument_context.py +++ b/api/tests/opentrons/protocol_api/test_instrument_context.py @@ -1069,8 +1069,8 @@ def test_liquid_presence_detection( ) -> None: """It should have a default liquid presence detection boolean set to False.""" decoy.when(mock_instrument_core.get_liquid_presence_detection()).then_return(False) - assert subject.liquid_detection is False - subject.liquid_detection = True + assert subject.liquid_presence_detection is False + subject.liquid_presence_detection = True decoy.verify(mock_instrument_core.set_liquid_presence_detection(True), times=1) From bf2b49a2c059890087d1c9675bff21f73bb7f874 Mon Sep 17 00:00:00 2001 From: CaseyBatten Date: Fri, 12 Jul 2024 14:59:13 -0400 Subject: [PATCH 09/78] feat(api): Partial tip pickup support for Single, Row and Partial Column (#15585) Covers PLAT-51, PLAT-217 and PLAT-355 Publicly exposes Single and Row configurations, introduces Partial Column configuration and the necessary logic to support it. --- api/src/opentrons/protocol_api/__init__.py | 6 ++ .../opentrons/protocol_api/_nozzle_layout.py | 4 ++ .../protocol_api/core/engine/instrument.py | 14 ++-- .../opentrons/protocol_api/core/instrument.py | 2 + .../core/legacy/legacy_instrument_core.py | 1 + .../legacy_instrument_core.py | 1 + .../protocol_api/instrument_context.py | 64 ++++++++++++++++--- .../commands/configure_nozzle_layout.py | 2 + .../protocol_engine/execution/tip_handler.py | 61 ++++++++++++++++-- .../opentrons/protocol_engine/state/tips.py | 16 ++--- api/src/opentrons/protocol_engine/types.py | 7 +- .../core/engine/test_instrument_core.py | 18 +++++- .../protocol_api/test_instrument_context.py | 15 +++-- .../commands/test_configure_nozzle_layout.py | 8 ++- .../execution/test_tip_handler.py | 28 ++++++-- .../hardware_testing/gravimetric/helpers.py | 5 +- shared-data/command/schemas/8.json | 8 ++- .../2/general/eight_channel/p1000/3_3.json | 4 +- .../2/general/eight_channel/p1000/3_4.json | 2 +- .../2/general/eight_channel/p1000/3_5.json | 2 +- .../2/general/eight_channel/p50/3_3.json | 4 +- .../2/general/eight_channel/p50/3_4.json | 2 +- .../2/general/eight_channel/p50/3_5.json | 2 +- 23 files changed, 219 insertions(+), 57 deletions(-) diff --git a/api/src/opentrons/protocol_api/__init__.py b/api/src/opentrons/protocol_api/__init__.py index 3f82aa41303..3bf263d6b76 100644 --- a/api/src/opentrons/protocol_api/__init__.py +++ b/api/src/opentrons/protocol_api/__init__.py @@ -31,6 +31,9 @@ from ._types import OFF_DECK from ._nozzle_layout import ( COLUMN, + PARTIAL_COLUMN, + SINGLE, + ROW, ALL, ) from ._parameters import Parameters @@ -63,6 +66,9 @@ "Liquid", "Parameters", "COLUMN", + "PARTIAL_COLUMN", + "SINGLE", + "ROW", "ALL", "OFF_DECK", "RuntimeParameterRequiredError", diff --git a/api/src/opentrons/protocol_api/_nozzle_layout.py b/api/src/opentrons/protocol_api/_nozzle_layout.py index 8e8cdf99521..06da3fc111e 100644 --- a/api/src/opentrons/protocol_api/_nozzle_layout.py +++ b/api/src/opentrons/protocol_api/_nozzle_layout.py @@ -4,6 +4,7 @@ class NozzleLayout(enum.Enum): COLUMN = "COLUMN" + PARTIAL_COLUMN = "PARTIAL_COLUMN" SINGLE = "SINGLE" ROW = "ROW" QUADRANT = "QUADRANT" @@ -11,6 +12,9 @@ class NozzleLayout(enum.Enum): COLUMN: Final = NozzleLayout.COLUMN +PARTIAL_COLUMN: Final = NozzleLayout.PARTIAL_COLUMN +SINGLE: Final = NozzleLayout.SINGLE +ROW: Final = NozzleLayout.ROW ALL: Final = NozzleLayout.ALL # Set __doc__ manually as a workaround. When this docstring is written the normal way, right after diff --git a/api/src/opentrons/protocol_api/core/engine/instrument.py b/api/src/opentrons/protocol_api/core/engine/instrument.py index 7b6400bc561..fcf853067fc 100644 --- a/api/src/opentrons/protocol_api/core/engine/instrument.py +++ b/api/src/opentrons/protocol_api/core/engine/instrument.py @@ -754,20 +754,13 @@ def get_liquid_presence_detection(self) -> bool: return self._liquid_presence_detection def is_tip_tracking_available(self) -> bool: - primary_nozzle = self._engine_client.state.pipettes.get_primary_nozzle( - self._pipette_id - ) if self.get_nozzle_configuration() == NozzleConfigurationType.FULL: return True else: if self.get_channels() == 96: return True if self.get_channels() == 8: - # TODO: (cb, 03/06/24): Enable automatic tip tracking on the 8 channel pipettes once PAPI support exists - return ( - self.get_nozzle_configuration() == NozzleConfigurationType.SINGLE - and primary_nozzle == "H1" - ) + return True return False def set_flow_rate( @@ -810,6 +803,7 @@ def configure_nozzle_layout( style: NozzleLayout, primary_nozzle: Optional[str], front_right_nozzle: Optional[str], + back_left_nozzle: Optional[str], ) -> None: if style == NozzleLayout.COLUMN: configuration_model: NozzleLayoutConfigurationType = ( @@ -821,11 +815,11 @@ def configure_nozzle_layout( configuration_model = RowNozzleLayoutConfiguration( primaryNozzle=cast(PRIMARY_NOZZLE_LITERAL, primary_nozzle) ) - elif style == NozzleLayout.QUADRANT: - assert front_right_nozzle is not None + elif style == NozzleLayout.QUADRANT or style == NozzleLayout.PARTIAL_COLUMN: configuration_model = QuadrantNozzleLayoutConfiguration( primaryNozzle=cast(PRIMARY_NOZZLE_LITERAL, primary_nozzle), frontRightNozzle=front_right_nozzle, + backLeftNozzle=back_left_nozzle, ) elif style == NozzleLayout.SINGLE: configuration_model = SingleNozzleLayoutConfiguration( diff --git a/api/src/opentrons/protocol_api/core/instrument.py b/api/src/opentrons/protocol_api/core/instrument.py index 2ad70c7274b..4108753a325 100644 --- a/api/src/opentrons/protocol_api/core/instrument.py +++ b/api/src/opentrons/protocol_api/core/instrument.py @@ -284,6 +284,7 @@ def configure_nozzle_layout( style: NozzleLayout, primary_nozzle: Optional[str], front_right_nozzle: Optional[str], + back_left_nozzle: Optional[str], ) -> None: """Configure the pipette to a specific nozzle layout. @@ -291,6 +292,7 @@ def configure_nozzle_layout( style: The type of configuration you wish to build. primary_nozzle: The nozzle that will determine a pipette's critical point. front_right_nozzle: The front right most nozzle in the requested layout. + back_left_nozzle: The back left most nozzle in the requested layout. """ ... diff --git a/api/src/opentrons/protocol_api/core/legacy/legacy_instrument_core.py b/api/src/opentrons/protocol_api/core/legacy/legacy_instrument_core.py index 6090c62a083..2d7dfb87da8 100644 --- a/api/src/opentrons/protocol_api/core/legacy/legacy_instrument_core.py +++ b/api/src/opentrons/protocol_api/core/legacy/legacy_instrument_core.py @@ -545,6 +545,7 @@ def configure_nozzle_layout( style: NozzleLayout, primary_nozzle: Optional[str], front_right_nozzle: Optional[str], + back_left_nozzle: Optional[str], ) -> None: """This will never be called because it was added in API 2.16.""" pass diff --git a/api/src/opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py b/api/src/opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py index 6d02252ceb5..4734d40949d 100644 --- a/api/src/opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py +++ b/api/src/opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py @@ -463,6 +463,7 @@ def configure_nozzle_layout( style: NozzleLayout, primary_nozzle: Optional[str], front_right_nozzle: Optional[str], + back_left_nozzle: Optional[str], ) -> None: """This will never be called because it was added in API 2.15.""" pass diff --git a/api/src/opentrons/protocol_api/instrument_context.py b/api/src/opentrons/protocol_api/instrument_context.py index 003b8817049..10aa9933346 100644 --- a/api/src/opentrons/protocol_api/instrument_context.py +++ b/api/src/opentrons/protocol_api/instrument_context.py @@ -65,6 +65,8 @@ """The version after which automatic tip tracking supported partially configured nozzle layouts.""" _DISPOSAL_LOCATION_OFFSET_ADDED_IN = APIVersion(2, 18) """The version after which offsets for deck configured trash containers and changes to alternating tip drop behavior were introduced.""" +_PARTIAL_NOZZLE_CONFIGURATION_SINGLE_ROW_PARTIAL_COLUMN_ADDED_IN = APIVersion(2, 20) +"""The version after which partial nozzle configurations of single, row, and partial column layouts became available.""" class InstrumentContext(publisher.CommandPublisher): @@ -1969,14 +1971,16 @@ def prepare_to_aspirate(self) -> None: self._core.prepare_to_aspirate() @requires_version(2, 16) - def configure_nozzle_layout( + def configure_nozzle_layout( # noqa: C901 self, style: NozzleLayout, start: Optional[str] = None, + end: Optional[str] = None, front_right: Optional[str] = None, + back_left: Optional[str] = None, tip_racks: Optional[List[labware.Labware]] = None, ) -> None: - """Configure how many tips the 96-channel pipette will pick up. + """Configure how many tips the 8-channel or 96-channel pipette will pick up. Changing the nozzle layout will affect gantry movement for all subsequent pipetting actions that the pipette performs. It also alters the pipette's @@ -1990,13 +1994,18 @@ def configure_nozzle_layout( :param style: The shape of the nozzle layout. + - ``SINGLE`` sets the pipette to use 1 nozzle. This corresponds to a single of well on labware. - ``COLUMN`` sets the pipette to use 8 nozzles, aligned from front to back with respect to the deck. This corresponds to a column of wells on labware. + - ``PARTIAL_COLUMN`` sets the pipette to use 2-7 nozzles, aligned from front to back + with respect to the deck. + - ``ROW`` sets the pipette to use 12 nozzles, aligned from left to right + with respect to the deck. This corresponds to a row of wells on labware. - ``ALL`` resets the pipette to use all of its nozzles. Calling ``configure_nozzle_layout`` with no arguments also resets the pipette. :type style: ``NozzleLayout`` or ``None`` - :param start: The nozzle at the back left of the layout, which the robot uses + :param start: The primary nozzle of the layout, which the robot uses to determine how it will move to different locations on the deck. The string should be of the same format used when identifying wells by name. Required unless setting ``style=ALL``. @@ -2006,6 +2015,16 @@ def configure_nozzle_layout( tips *from the same rack*. Doing so can affect positional accuracy. :type start: str or ``None`` + :param end: The nozzle at the end of a linear layout, which is used + to determine how many tips will be picked up by a pipette. The string + should be of the same format used when identifying wells by name. + Required when setting ``style=PARTIAL_COLUMN``. + + .. note:: + Nozzle layouts numbering between 2-7 nozzles, account for the distance from + ``start``. For example, 4 nozzles would require ``start="H1"`` and ``end="E1"``. + + :type end: str or ``None`` :param tip_racks: Behaves the same as setting the ``tip_racks`` parameter of :py:meth:`.load_instrument`. If not specified, the new configuration resets :py:obj:`.InstrumentContext.tip_racks` and you must specify the location @@ -2021,9 +2040,8 @@ def configure_nozzle_layout( # NOTE: Disabled layouts error case can be removed once desired map configurations # have appropriate data regarding tip-type to map current values added to the # pipette definitions. + disabled_layouts = [ - NozzleLayout.ROW, - NozzleLayout.SINGLE, NozzleLayout.QUADRANT, ] if style in disabled_layouts: @@ -2031,6 +2049,15 @@ def configure_nozzle_layout( f"Nozzle layout configuration of style {style.value} is currently unsupported." ) + original_enabled_layouts = [NozzleLayout.COLUMN, NozzleLayout.ALL] + if ( + self._api_version + < _PARTIAL_NOZZLE_CONFIGURATION_SINGLE_ROW_PARTIAL_COLUMN_ADDED_IN + ) and (style not in original_enabled_layouts): + raise ValueError( + f"Nozzle layout configuration of style {style.value} is unsupported in API Versions lower than {_PARTIAL_NOZZLE_CONFIGURATION_SINGLE_ROW_PARTIAL_COLUMN_ADDED_IN}." + ) + if style != NozzleLayout.ALL: if start is None: raise ValueError( @@ -2041,16 +2068,35 @@ def configure_nozzle_layout( f"Starting nozzle specified is not one of {types.ALLOWED_PRIMARY_NOZZLES}" ) if style == NozzleLayout.QUADRANT: - if front_right is None: + if front_right is None and back_left is None: raise ValueError( - "Cannot configure a QUADRANT layout without a front right nozzle." + "Cannot configure a QUADRANT layout without a front right or back left nozzle." ) + elif not (front_right is None and back_left is None): + raise ValueError( + f"Parameters 'front_right' and 'back_left' cannot be used with {style.value} Nozzle Configuration Layout." + ) + + front_right_resolved = front_right + back_left_resolved = back_left + if style == NozzleLayout.PARTIAL_COLUMN: + if end is None: + raise ValueError( + "Parameter 'end' is required for Partial Column Nozzle Configuration Layout." + ) + + # Determine if 'end' will be configured as front_right or back_left + if start == "H1" or start == "H12": + back_left_resolved = end + elif start == "A1" or start == "A12": + front_right_resolved = end + self._core.configure_nozzle_layout( style, primary_nozzle=start, - front_right_nozzle=front_right, + front_right_nozzle=front_right_resolved, + back_left_nozzle=back_left_resolved, ) - # TODO (spp, 2023-12-05): verify that tipracks are on adapters for only full 96 channel config self._tip_racks = tip_racks or [] @requires_version(2, 20) diff --git a/api/src/opentrons/protocol_engine/commands/configure_nozzle_layout.py b/api/src/opentrons/protocol_engine/commands/configure_nozzle_layout.py index ace59d49fde..74681098ab9 100644 --- a/api/src/opentrons/protocol_engine/commands/configure_nozzle_layout.py +++ b/api/src/opentrons/protocol_engine/commands/configure_nozzle_layout.py @@ -71,11 +71,13 @@ async def execute( """Check that requested pipette can support the requested nozzle layout.""" primary_nozzle = params.configurationParams.dict().get("primaryNozzle") front_right_nozzle = params.configurationParams.dict().get("frontRightNozzle") + back_left_nozzle = params.configurationParams.dict().get("backLeftNozzle") nozzle_params = await self._tip_handler.available_for_nozzle_layout( pipette_id=params.pipetteId, style=params.configurationParams.style, primary_nozzle=primary_nozzle, front_right_nozzle=front_right_nozzle, + back_left_nozzle=back_left_nozzle, ) nozzle_map = await self._equipment.configure_nozzle_layout( diff --git a/api/src/opentrons/protocol_engine/execution/tip_handler.py b/api/src/opentrons/protocol_engine/execution/tip_handler.py index 6638d216095..7acfae1e3ef 100644 --- a/api/src/opentrons/protocol_engine/execution/tip_handler.py +++ b/api/src/opentrons/protocol_engine/execution/tip_handler.py @@ -28,6 +28,13 @@ "H12": {"COLUMN": "A12", "ROW": "H1"}, } +PRIMARY_NOZZLE_TO_BACK_LEFT_NOZZLE_MAP = { + "A1": {"COLUMN": "A1", "ROW": "A1"}, + "H1": {"COLUMN": "A1", "ROW": "H1"}, + "A12": {"COLUMN": "A12", "ROW": "A1"}, + "H12": {"COLUMN": "A12", "ROW": "H1"}, +} + class TipHandler(TypingProtocol): """Pick up and drop tips.""" @@ -38,6 +45,7 @@ async def available_for_nozzle_layout( style: str, primary_nozzle: Optional[str] = None, front_right_nozzle: Optional[str] = None, + back_left_nozzle: Optional[str] = None, ) -> Dict[str, str]: """Check nozzle layout is compatible with the pipette. @@ -82,11 +90,12 @@ async def verify_tip_presence( """Verify the expected tip presence status.""" -async def _available_for_nozzle_layout( +async def _available_for_nozzle_layout( # noqa: C901 channels: int, style: str, primary_nozzle: Optional[str], front_right_nozzle: Optional[str], + back_left_nozzle: Optional[str], ) -> Dict[str, str]: """Check nozzle layout is compatible with the pipette. @@ -106,20 +115,60 @@ async def _available_for_nozzle_layout( limit_statement="RowNozzleLayout is incompatible with {channels} channel pipettes.", actual_value=str(primary_nozzle), ) + if style == "PARTIAL_COLUM" and channels == 96: + raise CommandParameterLimitViolated( + command_name="configure_nozzle_layout", + parameter_name="PartialColumnNozzleLayout", + limit_statement="PartialColumnNozzleLayout is incompatible with {channels} channel pipettes.", + actual_value=str(primary_nozzle), + ) if not primary_nozzle: return {"primary_nozzle": "A1"} if style == "SINGLE": return {"primary_nozzle": primary_nozzle} - if not front_right_nozzle: + if style == "QUADRANT" and front_right_nozzle and not back_left_nozzle: + return { + "primary_nozzle": primary_nozzle, + "front_right_nozzle": front_right_nozzle, + "back_left_nozzle": primary_nozzle, + } + if style == "QUADRANT" and back_left_nozzle and not front_right_nozzle: + return { + "primary_nozzle": primary_nozzle, + "front_right_nozzle": primary_nozzle, + "back_left_nozzle": back_left_nozzle, + } + if not front_right_nozzle and back_left_nozzle: return { "primary_nozzle": primary_nozzle, "front_right_nozzle": PRIMARY_NOZZLE_TO_ENDING_NOZZLE_MAP[primary_nozzle][ style ], + "back_left_nozzle": back_left_nozzle, } + if front_right_nozzle and not back_left_nozzle: + return { + "primary_nozzle": primary_nozzle, + "front_right_nozzle": front_right_nozzle, + "back_left_nozzle": PRIMARY_NOZZLE_TO_BACK_LEFT_NOZZLE_MAP[primary_nozzle][ + style + ], + } + if front_right_nozzle and back_left_nozzle: + return { + "primary_nozzle": primary_nozzle, + "front_right_nozzle": front_right_nozzle, + "back_left_nozzle": back_left_nozzle, + } + return { "primary_nozzle": primary_nozzle, - "front_right_nozzle": front_right_nozzle, + "front_right_nozzle": PRIMARY_NOZZLE_TO_ENDING_NOZZLE_MAP[primary_nozzle][ + style + ], + "back_left_nozzle": PRIMARY_NOZZLE_TO_BACK_LEFT_NOZZLE_MAP[primary_nozzle][ + style + ], } @@ -142,6 +191,7 @@ async def available_for_nozzle_layout( style: str, primary_nozzle: Optional[str] = None, front_right_nozzle: Optional[str] = None, + back_left_nozzle: Optional[str] = None, ) -> Dict[str, str]: """Returns configuration for nozzle layout to pass to configure_nozzle_layout.""" if self._state_view.pipettes.get_attached_tip(pipette_id): @@ -150,7 +200,7 @@ async def available_for_nozzle_layout( ) channels = self._state_view.pipettes.get_channels(pipette_id) return await _available_for_nozzle_layout( - channels, style, primary_nozzle, front_right_nozzle + channels, style, primary_nozzle, front_right_nozzle, back_left_nozzle ) async def pick_up_tip( @@ -307,6 +357,7 @@ async def available_for_nozzle_layout( style: str, primary_nozzle: Optional[str] = None, front_right_nozzle: Optional[str] = None, + back_left_nozzle: Optional[str] = None, ) -> Dict[str, str]: """Returns configuration for nozzle layout to pass to configure_nozzle_layout.""" if self._state_view.pipettes.get_attached_tip(pipette_id): @@ -315,7 +366,7 @@ async def available_for_nozzle_layout( ) channels = self._state_view.pipettes.get_channels(pipette_id) return await _available_for_nozzle_layout( - channels, style, primary_nozzle, front_right_nozzle + channels, style, primary_nozzle, front_right_nozzle, back_left_nozzle ) async def drop_tip( diff --git a/api/src/opentrons/protocol_engine/state/tips.py b/api/src/opentrons/protocol_engine/state/tips.py index 5af1e19a31f..85d437888fb 100644 --- a/api/src/opentrons/protocol_engine/state/tips.py +++ b/api/src/opentrons/protocol_engine/state/tips.py @@ -317,8 +317,8 @@ def _cluster_search_A1(active_columns: int, active_rows: int) -> Optional[str]: return result elif isinstance(result, int) and result == -1: return None - if critical_row + active_rows < len(columns[0]): - critical_row = critical_row + active_rows + if critical_row + 1 < len(columns[0]): + critical_row = critical_row + 1 else: critical_column += 1 critical_row = active_rows - 1 @@ -341,8 +341,8 @@ def _cluster_search_A12(active_columns: int, active_rows: int) -> Optional[str]: return result elif isinstance(result, int) and result == -1: return None - if critical_row + active_rows < len(columns[0]): - critical_row = critical_row + active_rows + if critical_row + 1 < len(columns[0]): + critical_row = critical_row + 1 else: critical_column -= 1 critical_row = active_rows - 1 @@ -365,8 +365,8 @@ def _cluster_search_H1(active_columns: int, active_rows: int) -> Optional[str]: return result elif isinstance(result, int) and result == -1: return None - if critical_row - active_rows >= 0: - critical_row = critical_row - active_rows + if critical_row - 1 >= 0: + critical_row = critical_row - 1 else: critical_column += 1 if critical_column >= len(columns): @@ -391,8 +391,8 @@ def _cluster_search_H12(active_columns: int, active_rows: int) -> Optional[str]: return result elif isinstance(result, int) and result == -1: return None - if critical_row - active_rows >= 0: - critical_row = critical_row - active_rows + if critical_row - 1 >= 0: + critical_row = critical_row - 1 else: critical_column -= 1 if critical_column < 0: diff --git a/api/src/opentrons/protocol_engine/types.py b/api/src/opentrons/protocol_engine/types.py index e596d2314fe..17a18a8ae4f 100644 --- a/api/src/opentrons/protocol_engine/types.py +++ b/api/src/opentrons/protocol_engine/types.py @@ -876,11 +876,16 @@ class QuadrantNozzleLayoutConfiguration(BaseModel): ..., description="The primary nozzle to use in the layout configuration. This nozzle will update the critical point of the current pipette. For now, this is also the back left corner of your rectangle.", ) - frontRightNozzle: str = Field( + frontRightNozzle: Optional[str] = Field( ..., regex=NOZZLE_NAME_REGEX, description="The front right nozzle in your configuration.", ) + backLeftNozzle: Optional[str] = Field( + ..., + regex=NOZZLE_NAME_REGEX, + description="The back left nozzle in your configuration.", + ) NozzleLayoutConfigurationType = Union[ diff --git a/api/tests/opentrons/protocol_api/core/engine/test_instrument_core.py b/api/tests/opentrons/protocol_api/core/engine/test_instrument_core.py index 3ca12bc004f..6b97040c2ec 100644 --- a/api/tests/opentrons/protocol_api/core/engine/test_instrument_core.py +++ b/api/tests/opentrons/protocol_api/core/engine/test_instrument_core.py @@ -1166,24 +1166,33 @@ def test_liquid_presence_detection( @pytest.mark.parametrize( - argnames=["style", "primary_nozzle", "front_right_nozzle", "expected_model"], + argnames=[ + "style", + "primary_nozzle", + "front_right_nozzle", + "back_left_nozzle", + "expected_model", + ], argvalues=[ [ NozzleLayout.COLUMN, "A1", "H1", + None, ColumnNozzleLayoutConfiguration(primaryNozzle="A1"), ], [ NozzleLayout.SINGLE, "H12", None, + None, SingleNozzleLayoutConfiguration(primaryNozzle="H12"), ], [ NozzleLayout.ROW, "A12", None, + None, RowNozzleLayoutConfiguration(primaryNozzle="A12"), ], ], @@ -1195,10 +1204,13 @@ def test_configure_nozzle_layout( style: NozzleLayout, primary_nozzle: Optional[str], front_right_nozzle: Optional[str], + back_left_nozzle: Optional[str], expected_model: NozzleLayoutConfigurationType, ) -> None: """The correct model is passed to the engine client.""" - subject.configure_nozzle_layout(style, primary_nozzle, front_right_nozzle) + subject.configure_nozzle_layout( + style, primary_nozzle, front_right_nozzle, back_left_nozzle + ) decoy.verify( mock_engine_client.execute_command( @@ -1222,7 +1234,7 @@ def test_configure_nozzle_layout( (8, NozzleConfigurationType.FULL, "A1", True), (8, NozzleConfigurationType.FULL, None, True), (8, NozzleConfigurationType.SINGLE, "H1", True), - (8, NozzleConfigurationType.SINGLE, "A1", False), + (8, NozzleConfigurationType.SINGLE, "A1", True), (1, NozzleConfigurationType.FULL, None, True), ], ) diff --git a/api/tests/opentrons/protocol_api/test_instrument_context.py b/api/tests/opentrons/protocol_api/test_instrument_context.py index bd9d9c7c736..f25923acad1 100644 --- a/api/tests/opentrons/protocol_api/test_instrument_context.py +++ b/api/tests/opentrons/protocol_api/test_instrument_context.py @@ -1119,11 +1119,13 @@ def test_prepare_to_aspirate_checks_volume( @pytest.mark.parametrize( - argnames=["style", "primary_nozzle", "front_right_nozzle", "exception"], + argnames=["style", "primary_nozzle", "front_right_nozzle", "end", "exception"], argvalues=[ - [NozzleLayout.COLUMN, "A1", "H1", does_not_raise()], - [NozzleLayout.SINGLE, None, None, pytest.raises(ValueError)], - [NozzleLayout.ROW, "E1", None, pytest.raises(ValueError)], + [NozzleLayout.COLUMN, "A1", None, None, does_not_raise()], + [NozzleLayout.SINGLE, None, None, None, pytest.raises(ValueError)], + [NozzleLayout.ROW, "E1", None, None, pytest.raises(ValueError)], + [NozzleLayout.PARTIAL_COLUMN, "H1", None, "G1", does_not_raise()], + [NozzleLayout.PARTIAL_COLUMN, "H1", "H1", "G1", pytest.raises(ValueError)], ], ) def test_configure_nozzle_layout( @@ -1131,11 +1133,14 @@ def test_configure_nozzle_layout( style: NozzleLayout, primary_nozzle: Optional[str], front_right_nozzle: Optional[str], + end: Optional[str], exception: ContextManager[None], ) -> None: """The correct model is passed to the engine client.""" with exception: - subject.configure_nozzle_layout(style, primary_nozzle, front_right_nozzle) + subject.configure_nozzle_layout( + style=style, start=primary_nozzle, end=end, front_right=front_right_nozzle + ) @pytest.mark.parametrize("api_version", [APIVersion(2, 15)]) diff --git a/api/tests/opentrons/protocol_engine/commands/test_configure_nozzle_layout.py b/api/tests/opentrons/protocol_engine/commands/test_configure_nozzle_layout.py index 2159d5efb9c..2f318b147ac 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_configure_nozzle_layout.py +++ b/api/tests/opentrons/protocol_engine/commands/test_configure_nozzle_layout.py @@ -66,7 +66,7 @@ ], [ QuadrantNozzleLayoutConfiguration( - primaryNozzle="A1", frontRightNozzle="E1" + primaryNozzle="A1", frontRightNozzle="E1", backLeftNozzle="A1" ), NozzleMap.build( physical_nozzles=NINETY_SIX_MAP, @@ -115,6 +115,11 @@ async def test_configure_nozzle_layout_implementation( if isinstance(request_model, QuadrantNozzleLayoutConfiguration) else None ) + back_left_nozzle = ( + request_model.backLeftNozzle + if isinstance(request_model, QuadrantNozzleLayoutConfiguration) + else None + ) decoy.when( await tip_handler.available_for_nozzle_layout( @@ -122,6 +127,7 @@ async def test_configure_nozzle_layout_implementation( style=request_model.style, primary_nozzle=primary_nozzle, front_right_nozzle=front_right_nozzle, + back_left_nozzle=back_left_nozzle, ) ).then_return(nozzle_params) diff --git a/api/tests/opentrons/protocol_engine/execution/test_tip_handler.py b/api/tests/opentrons/protocol_engine/execution/test_tip_handler.py index 2e5205bdc66..dfd02e9dfd5 100644 --- a/api/tests/opentrons/protocol_engine/execution/test_tip_handler.py +++ b/api/tests/opentrons/protocol_engine/execution/test_tip_handler.py @@ -275,6 +275,7 @@ async def test_add_tip( "style", "primary_nozzle", "front_nozzle", + "back_nozzle", "exception", "expected_result", "tip_result", @@ -285,8 +286,13 @@ async def test_add_tip( "COLUMN", "A1", None, + None, does_not_raise(), - {"primary_nozzle": "A1", "front_right_nozzle": "H1"}, + { + "primary_nozzle": "A1", + "front_right_nozzle": "H1", + "back_left_nozzle": "A1", + }, None, ], [ @@ -294,16 +300,27 @@ async def test_add_tip( "ROW", "A1", None, + None, pytest.raises(CommandParameterLimitViolated), None, None, ], - [8, "SINGLE", "A1", None, does_not_raise(), {"primary_nozzle": "A1"}, None], + [ + 8, + "SINGLE", + "A1", + None, + None, + does_not_raise(), + {"primary_nozzle": "A1"}, + None, + ], [ 1, "SINGLE", "A1", None, + None, pytest.raises(CommandPreconditionViolated), None, None, @@ -313,6 +330,7 @@ async def test_add_tip( "COLUMN", "A1", None, + None, pytest.raises(CommandPreconditionViolated), None, TipGeometry(length=50, diameter=5, volume=300), @@ -328,6 +346,7 @@ async def test_available_nozzle_layout( style: str, primary_nozzle: Optional[str], front_nozzle: Optional[str], + back_nozzle: Optional[str], exception: ContextManager[None], expected_result: Optional[Dict[str, str]], tip_result: Optional[TipGeometry], @@ -348,12 +367,11 @@ async def test_available_nozzle_layout( with exception: hw_result = await hw_subject.available_for_nozzle_layout( - "pipette-id", style, primary_nozzle, front_nozzle + "pipette-id", style, primary_nozzle, front_nozzle, back_nozzle ) virtual_result = await virtual_subject.available_for_nozzle_layout( - "pipette-id", style, primary_nozzle, front_nozzle + "pipette-id", style, primary_nozzle, front_nozzle, back_nozzle ) - assert hw_result == virtual_result == expected_result diff --git a/hardware-testing/hardware_testing/gravimetric/helpers.py b/hardware-testing/hardware_testing/gravimetric/helpers.py index 02510c99f24..f63928e4893 100644 --- a/hardware-testing/hardware_testing/gravimetric/helpers.py +++ b/hardware-testing/hardware_testing/gravimetric/helpers.py @@ -445,7 +445,10 @@ def _load_pipette( # so we need to decrease the pick-up current to work with 1 tip. if pipette.channels == 8 and not increment and not photometric: pipette._core.configure_nozzle_layout( - style=NozzleLayout.SINGLE, primary_nozzle="A1", front_right_nozzle="A1" + style=NozzleLayout.SINGLE, + primary_nozzle="A1", + front_right_nozzle="A1", + back_left_nozzle="A1", ) # override deck conflict checking cause we specially lay out our tipracks DeckConflit.check_safe_for_pipette_movement = ( diff --git a/shared-data/command/schemas/8.json b/shared-data/command/schemas/8.json index d6fb78e12c3..4700ace9229 100644 --- a/shared-data/command/schemas/8.json +++ b/shared-data/command/schemas/8.json @@ -635,9 +635,15 @@ "description": "The front right nozzle in your configuration.", "pattern": "[A-Z]\\d{1,2}", "type": "string" + }, + "backLeftNozzle": { + "title": "Backleftnozzle", + "description": "The back left nozzle in your configuration.", + "pattern": "[A-Z]\\d{1,2}", + "type": "string" } }, - "required": ["primaryNozzle", "frontRightNozzle"] + "required": ["primaryNozzle", "frontRightNozzle", "backLeftNozzle"] }, "ConfigureNozzleLayoutParams": { "title": "ConfigureNozzleLayoutParams", diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_3.json b/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_3.json index 62033d0444c..f7a517120de 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_3.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_3.json @@ -61,7 +61,7 @@ "default": { "speed": 10.0, "distance": 13.0, - "current": 0.13, + "current": 0.2, "tipOverlaps": { "v0": { "default": 10.5, @@ -79,7 +79,7 @@ "default": { "speed": 10.0, "distance": 13.0, - "current": 0.19, + "current": 0.2, "tipOverlaps": { "v0": { "default": 10.5, diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_4.json b/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_4.json index efc70c53611..582288c71d5 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_4.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_4.json @@ -79,7 +79,7 @@ "default": { "speed": 10.0, "distance": 13.0, - "current": 0.14, + "current": 0.2, "tipOverlaps": { "v0": { "default": 10.5, diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_5.json b/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_5.json index efc70c53611..582288c71d5 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_5.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_5.json @@ -79,7 +79,7 @@ "default": { "speed": 10.0, "distance": 13.0, - "current": 0.14, + "current": 0.2, "tipOverlaps": { "v0": { "default": 10.5, diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p50/3_3.json b/shared-data/pipette/definitions/2/general/eight_channel/p50/3_3.json index 514677f27f1..d5ef4b281ce 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p50/3_3.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p50/3_3.json @@ -53,7 +53,7 @@ "default": { "speed": 10.0, "distance": 13.0, - "current": 0.13, + "current": 0.2, "tipOverlaps": { "v0": { "default": 10.5, @@ -67,7 +67,7 @@ "default": { "speed": 10.0, "distance": 13.0, - "current": 0.19, + "current": 0.2, "tipOverlaps": { "v0": { "default": 10.5, diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p50/3_4.json b/shared-data/pipette/definitions/2/general/eight_channel/p50/3_4.json index 9ca1bfa8926..c901983a655 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p50/3_4.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p50/3_4.json @@ -63,7 +63,7 @@ "default": { "speed": 10.0, "distance": 13.0, - "current": 0.14, + "current": 0.2, "tipOverlaps": { "v0": { "default": 10.5, diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p50/3_5.json b/shared-data/pipette/definitions/2/general/eight_channel/p50/3_5.json index 9ca1bfa8926..c901983a655 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p50/3_5.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p50/3_5.json @@ -63,7 +63,7 @@ "default": { "speed": 10.0, "distance": 13.0, - "current": 0.14, + "current": 0.2, "tipOverlaps": { "v0": { "default": 10.5, From f34e1c3234d537c26c9f5c6e9b4aa6c6b9b03ce1 Mon Sep 17 00:00:00 2001 From: Seth Foster Date: Fri, 12 Jul 2024 17:02:39 -0400 Subject: [PATCH 10/78] refactor(app): Componentize "intervention info" (#15617) Intervention info is the name of the grey block with the labware name and the labware icon, and this makes it a component and updates its style. There's also a new content component called `InterventionInfoComponent` that mostly exists to hold the intervention info. This is used in Error Recovery, when you pick tips in a rack to pick up or tipracks to use. ## Review Seem like good changes? ## Testing - In Error Recovery ODD, do pick up new tip / refill well flows and check that it works out nicely - Unfortunately you can't really get to the relevant screens on desktop so Storybook will have to suffice - DQA Storybook will be at https://s3-us-west-2.amazonaws.com/opentrons-components/exec-500-intervention-content/index.html?path=/docs/app-molecules-interventionmodal-interventioncontent-interventioncontent--docs Closes EXEC-500 Closes EXEC-530 --------- Co-authored-by: koji --- .../InterventionContent.stories.tsx | 126 +++++++++++++ .../InterventionInfo.stories.tsx} | 53 +++--- .../InterventionContent/InterventionInfo.tsx | 169 ++++++++++++++++++ .../InterventionContent/index.tsx | 61 +++++++ .../InterventionStep/Move.tsx | 98 ---------- .../InterventionStep/index.tsx | 3 - .../InterventionModal/story-utils/StandIn.tsx | 3 +- .../story-utils/VisibleContainer.tsx | 25 +++ .../RecoveryOptions/FillWellAndSkip.tsx | 2 +- .../hooks/useFailedLabwareUtils.ts | 22 ++- .../shared/LeftColumnLabwareInfo.tsx | 56 +++--- .../ErrorRecoveryFlows/shared/ReplaceTips.tsx | 2 +- .../ErrorRecoveryFlows/shared/SelectTips.tsx | 2 +- .../__tests__/LeftColumnLabwareInfo.test.tsx | 18 +- .../src/atoms/StyledText/StyledText.tsx | 16 ++ 15 files changed, 478 insertions(+), 178 deletions(-) create mode 100644 app/src/molecules/InterventionModal/InterventionContent/InterventionContent.stories.tsx rename app/src/molecules/InterventionModal/{InterventionStep/Move.stories.tsx => InterventionContent/InterventionInfo.stories.tsx} (62%) create mode 100644 app/src/molecules/InterventionModal/InterventionContent/InterventionInfo.tsx create mode 100644 app/src/molecules/InterventionModal/InterventionContent/index.tsx delete mode 100644 app/src/molecules/InterventionModal/InterventionStep/Move.tsx delete mode 100644 app/src/molecules/InterventionModal/InterventionStep/index.tsx create mode 100644 app/src/molecules/InterventionModal/story-utils/VisibleContainer.tsx diff --git a/app/src/molecules/InterventionModal/InterventionContent/InterventionContent.stories.tsx b/app/src/molecules/InterventionModal/InterventionContent/InterventionContent.stories.tsx new file mode 100644 index 00000000000..d910847c2d3 --- /dev/null +++ b/app/src/molecules/InterventionModal/InterventionContent/InterventionContent.stories.tsx @@ -0,0 +1,126 @@ +import * as React from 'react' +import { ICON_DATA_BY_NAME } from '@opentrons/components' +import { InterventionContent } from '.' +import { TwoColumn } from '../TwoColumn' +import { StandInContent } from '../story-utils/StandIn' +import { VisibleContainer } from '../story-utils/VisibleContainer' + +import type { Meta, StoryObj } from '@storybook/react' + +const meta: Meta = { + title: + 'App/Molecules/InterventionModal/InterventionContent/InterventionContent', + component: InterventionContent, + argTypes: { + headline: { + control: { + type: 'text', + }, + }, + infoProps: { + control: { + type: 'object', + }, + type: { + control: { + type: 'select', + }, + options: [ + 'location', + 'location-arrow-location', + 'location-colon-location', + ], + }, + labwareName: { + control: 'text', + }, + currentLocationProps: { + control: { + type: 'object', + }, + slotName: { + control: 'text', + }, + iconName: { + control: { + type: 'select', + }, + options: Object.keys(ICON_DATA_BY_NAME), + }, + }, + newLocationProps: { + control: { + type: 'object', + }, + slotName: { + control: 'text', + }, + iconName: { + control: { + type: 'select', + }, + options: Object.keys(ICON_DATA_BY_NAME), + }, + }, + labwareNickname: { + control: { + type: 'text', + }, + }, + }, + notificationProps: { + control: { + type: 'object', + }, + type: { + control: { + type: 'select', + }, + options: ['alert', 'error', 'neutral', 'success'], + }, + heading: { + control: { + type: 'text', + }, + }, + message: { + control: { + type: 'text', + }, + }, + }, + }, + decorators: [ + Story => ( + + + + + + + ), + ], +} + +export default meta + +type Story = StoryObj + +export const InterventionContentStory: Story = { + args: { + headline: 'You have something to do', + infoProps: { + type: 'location', + labwareName: 'Biorad Plate 200ML', + labwareNickname: 'The biggest plate I have', + currentLocationProps: { + slotName: 'C2', + }, + }, + notificationProps: { + type: 'alert', + heading: 'An alert', + message: 'Oh no', + }, + }, +} diff --git a/app/src/molecules/InterventionModal/InterventionStep/Move.stories.tsx b/app/src/molecules/InterventionModal/InterventionContent/InterventionInfo.stories.tsx similarity index 62% rename from app/src/molecules/InterventionModal/InterventionStep/Move.stories.tsx rename to app/src/molecules/InterventionModal/InterventionContent/InterventionInfo.stories.tsx index 2bbb12d3e14..caac9a06d5c 100644 --- a/app/src/molecules/InterventionModal/InterventionStep/Move.stories.tsx +++ b/app/src/molecules/InterventionModal/InterventionContent/InterventionInfo.stories.tsx @@ -2,19 +2,23 @@ import * as React from 'react' import { Box, ICON_DATA_BY_NAME } from '@opentrons/components' -import { Move } from './Move' +import { InterventionInfo } from './InterventionInfo' import type { Meta, StoryObj } from '@storybook/react' -const meta: Meta = { - title: 'App/Organisms/InterventionModal/InterventionStep/Move', - component: Move, +const meta: Meta = { + title: 'App/Molecules/InterventionModal/InterventionContent/InterventionInfo', + component: InterventionInfo, argTypes: { type: { control: { type: 'select', - options: ['move', 'refill', 'select'], }, + options: [ + 'location', + 'location-arrow-location', + 'location-colon-location', + ], }, labwareName: { control: 'text', @@ -29,8 +33,8 @@ const meta: Meta = { iconName: { control: { type: 'select', - options: Object.keys(ICON_DATA_BY_NAME), }, + options: Object.keys(ICON_DATA_BY_NAME), }, }, newLocationProps: { @@ -43,20 +47,32 @@ const meta: Meta = { iconName: { control: { type: 'select', - options: Object.keys(ICON_DATA_BY_NAME), }, + options: Object.keys(ICON_DATA_BY_NAME), + }, + }, + labwareNickname: { + control: { + type: 'text', }, }, }, + decorators: [ + Story => ( + + + + ), + ], } export default meta -type Story = StoryObj +type Story = StoryObj export const MoveBetweenSlots: Story = { args: { - type: 'move', + type: 'location-arrow-location', labwareName: 'Plate', currentLocationProps: { slotName: 'A1', @@ -65,31 +81,21 @@ export const MoveBetweenSlots: Story = { slotName: 'B2', }, }, - render: args => ( - - - - ), } export const Refill: Story = { args: { - type: 'refill', + type: 'location', labwareName: 'Tip Rack', currentLocationProps: { slotName: 'A1', }, }, - render: args => ( - - - - ), } export const Select: Story = { args: { - type: 'select', + type: 'location-colon-location', labwareName: 'Well', currentLocationProps: { slotName: 'A1', @@ -98,9 +104,4 @@ export const Select: Story = { slotName: 'B1', }, }, - render: args => ( - - - - ), } diff --git a/app/src/molecules/InterventionModal/InterventionContent/InterventionInfo.tsx b/app/src/molecules/InterventionModal/InterventionContent/InterventionInfo.tsx new file mode 100644 index 00000000000..aa6ad81c97e --- /dev/null +++ b/app/src/molecules/InterventionModal/InterventionContent/InterventionInfo.tsx @@ -0,0 +1,169 @@ +import * as React from 'react' +import { css } from 'styled-components' + +import { + LocationIcon, + Flex, + Icon, + COLORS, + BORDERS, + SPACING, + DIRECTION_COLUMN, + StyledText, + ALIGN_CENTER, + RESPONSIVENESS, +} from '@opentrons/components' +import { Divider } from '../../../atoms/structure/Divider' + +import type { LocationIconProps } from '@opentrons/components' + +export interface InterventionInfoProps { + type: 'location-arrow-location' | 'location-colon-location' | 'location' + labwareName: string + labwareNickname?: string + currentLocationProps: LocationIconProps + newLocationProps?: LocationIconProps +} + +export function InterventionInfo(props: InterventionInfoProps): JSX.Element { + const content = buildContent(props) + + return ( + + + + {props.labwareName} + + {props.labwareNickname != null ? ( + + {props.labwareNickname}{' '} + + ) : null} + + + {content} + + ) +} + +const buildContent = (props: InterventionInfoProps): JSX.Element => { + switch (props.type) { + case 'location-arrow-location': + return buildLocArrowLoc(props) + case 'location-colon-location': + return buildLocColonLoc(props) + case 'location': + return buildLoc(props) + } +} + +const buildLocArrowLoc = (props: InterventionInfoProps): JSX.Element => { + const { currentLocationProps, newLocationProps } = props + + if (newLocationProps != null) { + return ( + + + + + + ) + } else { + return buildLoc(props) + } +} + +const buildLoc = ({ + currentLocationProps, +}: InterventionInfoProps): JSX.Element => { + return ( + + + + ) +} + +const buildLocColonLoc = (props: InterventionInfoProps): JSX.Element => { + const { currentLocationProps, newLocationProps } = props + + if (newLocationProps != null) { + return ( + + + + + + ) + } else { + return buildLoc(props) + } +} + +const ICON_STYLE = css` + width: ${SPACING.spacing24}; + height: ${SPACING.spacing24}; + @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { + width: ${SPACING.spacing40}; + height: ${SPACING.spacing40}; + } +` + +const CARD_STYLE = css` + background-color: ${COLORS.grey20}; + border-radius: ${BORDERS.borderRadius4}; + gap: ${SPACING.spacing8}; + @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { + background-color: ${COLORS.grey35}; + border-radius: ${BORDERS.borderRadius8}; + } +` + +const LINE_CLAMP_STYLE = css` + display: -webkit-box; + -webkit-box-orient: vertical; + overflow: hidden; + text-overflow: ellipsis; + word-wrap: break-word; + -webkit-line-clamp: 2; +` diff --git a/app/src/molecules/InterventionModal/InterventionContent/index.tsx b/app/src/molecules/InterventionModal/InterventionContent/index.tsx new file mode 100644 index 00000000000..cc52255e4f9 --- /dev/null +++ b/app/src/molecules/InterventionModal/InterventionContent/index.tsx @@ -0,0 +1,61 @@ +import * as React from 'react' +import { + Flex, + StyledText, + DIRECTION_COLUMN, + SPACING, + RESPONSIVENESS, +} from '@opentrons/components' +import { InlineNotification } from '../../../atoms/InlineNotification' + +import { InterventionInfo } from './InterventionInfo' +export type { InterventionInfoProps } from './InterventionInfo' +export { InterventionInfo } + +export interface InterventionContentProps { + headline: string + infoProps: React.ComponentProps + notificationProps?: React.ComponentProps +} + +export function InterventionContent({ + headline, + infoProps, + notificationProps, +}: InterventionContentProps): JSX.Element { + return ( + + + {headline} + + + + {notificationProps ? ( + + ) : null} + + + ) +} diff --git a/app/src/molecules/InterventionModal/InterventionStep/Move.tsx b/app/src/molecules/InterventionModal/InterventionStep/Move.tsx deleted file mode 100644 index 156753d7bc8..00000000000 --- a/app/src/molecules/InterventionModal/InterventionStep/Move.tsx +++ /dev/null @@ -1,98 +0,0 @@ -import * as React from 'react' -import { css } from 'styled-components' - -import { - LocationIcon, - Flex, - Icon, - COLORS, - BORDERS, - SPACING, - DIRECTION_COLUMN, - LegacyStyledText, - ALIGN_CENTER, -} from '@opentrons/components' - -import type { LocationIconProps } from '@opentrons/components' - -export interface MoveProps { - type: 'move' | 'refill' | 'select' - labwareName: string - currentLocationProps: LocationIconProps - newLocationProps?: LocationIconProps -} - -export function Move(props: MoveProps): JSX.Element { - const content = buildContent(props) - - return ( - - {props.labwareName} - {content} - - ) -} - -const buildContent = (props: MoveProps): JSX.Element => { - switch (props.type) { - case 'move': - return buildMove(props) - case 'refill': - return buildRefill(props) - case 'select': - return buildSelect(props) - } -} - -const buildMove = (props: MoveProps): JSX.Element => { - const { currentLocationProps, newLocationProps } = props - - if (newLocationProps != null) { - return ( - - - - - - ) - } else { - return buildRefill(props) - } -} - -const buildRefill = ({ currentLocationProps }: MoveProps): JSX.Element => { - return ( - - - - ) -} - -const buildSelect = (props: MoveProps): JSX.Element => { - const { currentLocationProps, newLocationProps } = props - - if (newLocationProps != null) { - return ( - - - - - - ) - } else { - return buildRefill(props) - } -} - -const ICON_STYLE = css` - width: ${SPACING.spacing40}; - height: ${SPACING.spacing40}; -` - -const CARD_STYLE = css` - flex-direction: ${DIRECTION_COLUMN}; - background-color: ${COLORS.grey35}; - padding: ${SPACING.spacing16}; - grid-gap: ${SPACING.spacing8}; - border-radius: ${BORDERS.borderRadius8}; -` diff --git a/app/src/molecules/InterventionModal/InterventionStep/index.tsx b/app/src/molecules/InterventionModal/InterventionStep/index.tsx deleted file mode 100644 index a6f80de51bf..00000000000 --- a/app/src/molecules/InterventionModal/InterventionStep/index.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export { Move } from './Move' - -export type { MoveProps } from './Move' diff --git a/app/src/molecules/InterventionModal/story-utils/StandIn.tsx b/app/src/molecules/InterventionModal/story-utils/StandIn.tsx index f6ac9e7dd78..28992aba717 100644 --- a/app/src/molecules/InterventionModal/story-utils/StandIn.tsx +++ b/app/src/molecules/InterventionModal/story-utils/StandIn.tsx @@ -1,12 +1,11 @@ import * as React from 'react' -import { Box, BORDERS, SPACING } from '@opentrons/components' +import { Box, BORDERS } from '@opentrons/components' export function StandInContent(): JSX.Element { return ( diff --git a/app/src/molecules/InterventionModal/story-utils/VisibleContainer.tsx b/app/src/molecules/InterventionModal/story-utils/VisibleContainer.tsx new file mode 100644 index 00000000000..ac80ecdb063 --- /dev/null +++ b/app/src/molecules/InterventionModal/story-utils/VisibleContainer.tsx @@ -0,0 +1,25 @@ +import * as React from 'react' + +import { Box, BORDERS, SPACING } from '@opentrons/components' + +export interface VisibleContainerProps { + children: JSX.Element | JSX.Element[] +} + +export function VisibleContainer({ + children, +}: VisibleContainerProps): JSX.Element { + return ( + + {children} + + ) +} diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/FillWellAndSkip.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/FillWellAndSkip.tsx index 583d0428ee0..9759db35575 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/FillWellAndSkip.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/FillWellAndSkip.tsx @@ -63,7 +63,7 @@ export function FillWell(props: RecoveryContentProps): JSX.Element | null { title={t('manually_fill_liquid_in_well', { well: failedLabwareUtils.relevantWellName, })} - moveType="refill" + type="location" /> diff --git a/app/src/organisms/ErrorRecoveryFlows/hooks/useFailedLabwareUtils.ts b/app/src/organisms/ErrorRecoveryFlows/hooks/useFailedLabwareUtils.ts index 0ff11b7d00a..5de95fe90da 100644 --- a/app/src/organisms/ErrorRecoveryFlows/hooks/useFailedLabwareUtils.ts +++ b/app/src/organisms/ErrorRecoveryFlows/hooks/useFailedLabwareUtils.ts @@ -20,6 +20,7 @@ import type { DispenseRunTimeCommand, LiquidProbeRunTimeCommand, } from '@opentrons/shared-data' +import { getLoadedLabware } from '../../../molecules/Command/utils/accessors' import type { ErrorRecoveryFlowsProps } from '..' interface UseFailedLabwareUtilsProps { @@ -37,6 +38,8 @@ export type UseFailedLabwareUtilsResult = UseTipSelectionUtilsResult & { failedLabware: LoadedLabware | null /* The name of the well(s) or tip location(s), if any. */ relevantWellName: string | null + /* The user-content nickname of the failed labware, if any */ + failedLabwareNickname: string | null } /** Utils for labware relating to the failedCommand. @@ -59,7 +62,7 @@ export function useFailedLabwareUtils({ const tipSelectionUtils = useTipSelectionUtils(recentRelevantFailedLabwareCmd) - const failedLabwareName = React.useMemo( + const failedLabwareDetails = React.useMemo( () => getFailedCmdRelevantLabware( protocolAnalysis, @@ -81,9 +84,10 @@ export function useFailedLabwareUtils({ return { ...tipSelectionUtils, - failedLabwareName, + failedLabwareName: failedLabwareDetails?.name ?? null, failedLabware, relevantWellName, + failedLabwareNickname: failedLabwareDetails?.nickname ?? null, } } @@ -238,15 +242,25 @@ export function getFailedCmdRelevantLabware( protocolAnalysis: ErrorRecoveryFlowsProps['protocolAnalysis'], recentRelevantFailedLabwareCmd: FailedCommandRelevantLabware, runRecord?: Run -): string | null { +): { name: string; nickname: string | null } | null { const lwDefsByURI = getLoadedLabwareDefinitionsByUri( protocolAnalysis?.commands ?? [] ) + const labwareNickname = + protocolAnalysis != null + ? getLoadedLabware( + protocolAnalysis, + recentRelevantFailedLabwareCmd?.params.labwareId || '' + )?.displayName ?? null + : null const failedLWURI = runRecord?.data.labware.find( labware => labware.id === recentRelevantFailedLabwareCmd?.params.labwareId )?.definitionUri if (failedLWURI != null) { - return getLabwareDisplayName(lwDefsByURI[failedLWURI]) + return { + name: getLabwareDisplayName(lwDefsByURI[failedLWURI]), + nickname: labwareNickname, + } } else { return null } diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/LeftColumnLabwareInfo.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/LeftColumnLabwareInfo.tsx index 259f7666a65..0229cf93dab 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/LeftColumnLabwareInfo.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/LeftColumnLabwareInfo.tsx @@ -1,20 +1,12 @@ import * as React from 'react' -import { - DIRECTION_COLUMN, - Flex, - SPACING, - LegacyStyledText, -} from '@opentrons/components' - -import { Move } from '../../../molecules/InterventionModal/InterventionStep' -import { InlineNotification } from '../../../atoms/InlineNotification' +import { InterventionContent } from '../../../molecules/InterventionModal/InterventionContent' import type { RecoveryContentProps } from '../types' type LeftColumnLabwareInfoProps = RecoveryContentProps & { title: string - moveType: React.ComponentProps['type'] + type: React.ComponentProps['infoProps']['type'] /* Renders a warning InlineNotification if provided. */ bannerText?: string } @@ -24,10 +16,14 @@ export function LeftColumnLabwareInfo({ title, failedLabwareUtils, isOnDevice, - moveType, + type, bannerText, }: LeftColumnLabwareInfoProps): JSX.Element | null { - const { failedLabwareName, failedLabware } = failedLabwareUtils + const { + failedLabwareName, + failedLabware, + failedLabwareNickname, + } = failedLabwareUtils const buildLabwareLocationSlotName = (): string => { const location = failedLabware?.location @@ -42,26 +38,18 @@ export function LeftColumnLabwareInfo({ } } - if (isOnDevice) { - return ( - - - {title} - - - {bannerText != null ? ( - - ) : null} - - ) - } else { - return null - } + return ( + + ) } diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/ReplaceTips.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/ReplaceTips.tsx index 48ddf992e2c..dc0bece20f6 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/ReplaceTips.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/ReplaceTips.tsx @@ -43,7 +43,7 @@ export function ReplaceTips(props: RecoveryContentProps): JSX.Element | null { diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/SelectTips.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/SelectTips.tsx index 72210a09b61..4f5716198a5 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/SelectTips.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/SelectTips.tsx @@ -53,7 +53,7 @@ export function SelectTips(props: RecoveryContentProps): JSX.Element | null { diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/LeftColumnLabwareInfo.test.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/LeftColumnLabwareInfo.test.tsx index 4ea8cee5ac4..0edf9b95236 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/LeftColumnLabwareInfo.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/LeftColumnLabwareInfo.test.tsx @@ -6,10 +6,12 @@ import { renderWithProviders } from '../../../../__testing-utils__' import { mockRecoveryContentProps } from '../../__fixtures__' import { i18n } from '../../../../i18n' import { LeftColumnLabwareInfo } from '../LeftColumnLabwareInfo' -import { Move } from '../../../../molecules/InterventionModal/InterventionStep' +import { InterventionInfo } from '../../../../molecules/InterventionModal/InterventionContent/InterventionInfo' import { InlineNotification } from '../../../../atoms/InlineNotification' -vi.mock('../../../../molecules/InterventionModal/InterventionStep') +vi.mock( + '../../../../molecules/InterventionModal/InterventionContent/InterventionInfo' +) vi.mock('../../../../atoms/InlineNotification') const render = (props: React.ComponentProps) => { @@ -31,24 +33,24 @@ describe('LeftColumnLabwareInfo', () => { location: { slotName: 'A1' }, }, } as any, - moveType: 'refill', + type: 'location', bannerText: 'MOCK_BANNER_TEXT', } - vi.mocked(Move).mockReturnValue(
MOCK_MOVE
) + vi.mocked(InterventionInfo).mockReturnValue(
MOCK_MOVE
) vi.mocked(InlineNotification).mockReturnValue(
MOCK_INLINE_NOTIFICATION
) }) - it('renders the title, Move component, and InlineNotification when bannerText is provided', () => { + it('renders the title, InterventionInfo component, and InlineNotification when bannerText is provided', () => { render(props) screen.getByText('MOCK_TITLE') screen.getByText('MOCK_MOVE') - expect(vi.mocked(Move)).toHaveBeenCalledWith( + expect(vi.mocked(InterventionInfo)).toHaveBeenCalledWith( expect.objectContaining({ - type: 'refill', + type: 'location', labwareName: 'MOCK_LW_NAME', currentLocationProps: { slotName: 'A1' }, }), @@ -78,7 +80,7 @@ describe('LeftColumnLabwareInfo', () => { props.failedLabwareUtils.failedLabware.location = 'offDeck' render(props) - expect(vi.mocked(Move)).toHaveBeenCalledWith( + expect(vi.mocked(InterventionInfo)).toHaveBeenCalledWith( expect.objectContaining({ currentLocationProps: { slotName: '' }, }), diff --git a/components/src/atoms/StyledText/StyledText.tsx b/components/src/atoms/StyledText/StyledText.tsx index c4796c1f74d..17e41ddc462 100644 --- a/components/src/atoms/StyledText/StyledText.tsx +++ b/components/src/atoms/StyledText/StyledText.tsx @@ -111,6 +111,14 @@ const helixProductStyleMap = { } `, }, + hidden: { + as: 'none', + style: css` + @media not (${RESPONSIVENESS.touchscreenMediaQuerySpecs}) { + display: none; + } + `, + }, } as const const ODDStyleMap = { @@ -249,6 +257,14 @@ const ODDStyleMap = { } `, }, + hidden: { + as: 'none', + style: css` + @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { + display: none; + } + `, + }, } as const export interface Props extends React.ComponentProps { From 5bdbe22b5d68a29af503405bfea65bc29b25b359 Mon Sep 17 00:00:00 2001 From: Seth Foster Date: Fri, 12 Jul 2024 17:24:05 -0400 Subject: [PATCH 11/78] refactor(app): Recovery fixups (#15636) This is a couple fixups and a couple removals of hidden rendering for error recovery. Notably, - Remove all the places where we just didn't render anything on desktop. Now we render the wrong thing, which doesn't seem like a step forward but actually does let us see those screens now, which is good - In a couple places get some incredibly basic components in; for instance, the footers now use desktop components on desktop - Remove a lot of `isOnDevice` checks - Remove `BeforeBeginning`, which wasn't used, didn't have its content in the translation files, and isn't in design - Fix a couple of the grosser mis-stylings; the content is vaguely the right size and shape and has fewer (not none) scroll bars, and things that should be full width are --- .../ErrorRecoveryFlows/BeforeBeginning.tsx | 51 ------ .../ErrorRecoveryWizard.tsx | 9 -- .../ErrorRecoveryFlows/RecoveryError.tsx | 56 ++++--- .../ErrorRecoveryFlows/RecoveryInProgress.tsx | 11 +- .../RecoveryOptions/CancelRun.tsx | 74 ++++----- .../RecoveryOptions/FillWellAndSkip.tsx | 56 +++---- .../RecoveryOptions/IgnoreErrorSkipStep.tsx | 40 ++--- .../RecoveryOptions/ManageTips.tsx | 85 +++++----- .../RecoveryOptions/SelectRecoveryOption.tsx | 46 +++--- .../__tests__/CancelRun.test.tsx | 23 +-- .../__tests__/FillWellAndSkip.test.tsx | 10 +- .../__tests__/IgnoreErrorSkipStep.test.tsx | 7 +- .../__tests__/ManageTips.test.tsx | 11 +- .../__tests__/RetryNewTips.test.tsx | 5 +- .../__tests__/RetrySameTips.test.tsx | 6 +- .../__tests__/RetryStep.test.tsx | 6 +- .../__tests__/SelectRecoveryOptions.test.tsx | 20 +-- .../__tests__/SkipStepNewTips.test.tsx | 5 +- .../__tests__/SkipStepSameTips.test.tsx | 6 +- .../__tests__/BeforeBeginning.test.tsx | 56 ------- .../__tests__/ErrorRecoveryWizard.test.tsx | 17 -- .../ErrorRecoveryFlows/__tests__/util.ts | 6 + .../organisms/ErrorRecoveryFlows/constants.ts | 8 - .../__tests__/useRecoveryRouting.test.ts | 4 +- .../shared/LeftColumnLabwareInfo.tsx | 1 - .../shared/RecoveryContentWrapper.tsx | 3 +- .../shared/RecoveryFooterButtons.tsx | 146 ++++++++++++------ .../shared/RecoveryInterventionModal.tsx | 3 +- .../ErrorRecoveryFlows/shared/ReplaceTips.tsx | 38 ++--- .../ErrorRecoveryFlows/shared/SelectTips.tsx | 68 ++++---- .../TwoColTextAndFailedStepNextStep.tsx | 2 - .../__tests__/RecoveryFooterButtons.test.tsx | 52 +++++-- .../shared/__tests__/SelectTips.test.tsx | 12 +- 33 files changed, 409 insertions(+), 534 deletions(-) delete mode 100644 app/src/organisms/ErrorRecoveryFlows/BeforeBeginning.tsx delete mode 100644 app/src/organisms/ErrorRecoveryFlows/__tests__/BeforeBeginning.test.tsx create mode 100644 app/src/organisms/ErrorRecoveryFlows/__tests__/util.ts diff --git a/app/src/organisms/ErrorRecoveryFlows/BeforeBeginning.tsx b/app/src/organisms/ErrorRecoveryFlows/BeforeBeginning.tsx deleted file mode 100644 index 48a360901d9..00000000000 --- a/app/src/organisms/ErrorRecoveryFlows/BeforeBeginning.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import * as React from 'react' -import { Trans, useTranslation } from 'react-i18next' - -import { - DIRECTION_COLUMN, - Flex, - JUSTIFY_CENTER, - LegacyStyledText, -} from '@opentrons/components' - -import { SmallButton } from '../../atoms/buttons' -import { BODY_TEXT_STYLE, ODD_SECTION_TITLE_STYLE } from './constants' -import { RecoveryContentWrapper } from './shared' - -import type { RecoveryContentProps } from './types' - -export function BeforeBeginning({ - isOnDevice, - routeUpdateActions, -}: RecoveryContentProps): JSX.Element | null { - const { t } = useTranslation('error_recovery') - const { proceedNextStep } = routeUpdateActions - - if (isOnDevice) { - return ( - - - - {t('before_you_begin')} - - , - }} - /> - - - - ) - } else { - return null - } -} diff --git a/app/src/organisms/ErrorRecoveryFlows/ErrorRecoveryWizard.tsx b/app/src/organisms/ErrorRecoveryFlows/ErrorRecoveryWizard.tsx index 5a40b280139..a8da5b77038 100644 --- a/app/src/organisms/ErrorRecoveryFlows/ErrorRecoveryWizard.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/ErrorRecoveryWizard.tsx @@ -3,7 +3,6 @@ import { useTranslation } from 'react-i18next' import { LegacyStyledText } from '@opentrons/components' -import { BeforeBeginning } from './BeforeBeginning' import { RecoveryError } from './RecoveryError' import { SelectRecoveryOption, @@ -106,7 +105,6 @@ export function ErrorRecoveryComponent( const isLargeDesktopStyle = route === RECOVERY_MAP.DROP_TIP_FLOWS.ROUTE && step !== RECOVERY_MAP.DROP_TIP_FLOWS.STEPS.BEGIN_REMOVAL - return ( { - return - } - const buildSelectRecoveryOption = (): JSX.Element => { return } @@ -175,10 +169,7 @@ export function ErrorRecoveryContent(props: RecoveryContentProps): JSX.Element { const buildIgnoreErrorSkipStep = (): JSX.Element => { return } - switch (props.recoveryMap.route) { - case RECOVERY_MAP.BEFORE_BEGINNING.ROUTE: - return buildBeforeBeginning() case RECOVERY_MAP.OPTION_SELECTION.ROUTE: return buildSelectRecoveryOption() case RECOVERY_MAP.ERROR_WHILE_RECOVERING.ROUTE: diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryError.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryError.tsx index 5e7ba51c0bd..2c125f9897a 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryError.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryError.tsx @@ -167,39 +167,35 @@ export function ErrorContent({ btnText: string btnOnClick: () => void }): JSX.Element | null { - if (isOnDevice) { - return ( - + return ( + + + - - - {title} - {subTitle} - - - - + {title} + {subTitle} - - ) - } else { - return null - } +
+ + + + + ) } diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryInProgress.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryInProgress.tsx index 4453598de18..4a0d64f7661 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryInProgress.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryInProgress.tsx @@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next' import { InProgressModal } from '../../molecules/InProgressModal/InProgressModal' import { RECOVERY_MAP } from './constants' +import { Flex, ALIGN_CENTER, JUSTIFY_CENTER } from '@opentrons/components' import type { RobotMovingRoute, RecoveryContentProps } from './types' @@ -41,5 +42,13 @@ export function RecoveryInProgress({ const description = buildDescription() - return + return ( + + + + ) } diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/CancelRun.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/CancelRun.tsx index edd64990ea5..5cf63db7c2f 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/CancelRun.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/CancelRun.tsx @@ -41,7 +41,6 @@ export function CancelRun(props: RecoveryContentProps): JSX.Element { } function CancelRunConfirmation({ - isOnDevice, routeUpdateActions, recoveryCommands, tipStatusUtils, @@ -56,48 +55,43 @@ function CancelRunConfirmation({ tipStatusUtils, }) - if (isOnDevice) { - return ( - + - - - - {t('are_you_sure_you_want_to_cancel')} - - - {t('if_tips_are_attached')} - - - - - ) - } else { - return null - } + + {t('are_you_sure_you_want_to_cancel')} + + + {t('if_tips_are_attached')} + +
+ + + ) } interface OnCancelRunProps { diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/FillWellAndSkip.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/FillWellAndSkip.tsx index 9759db35575..19d51269d53 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/FillWellAndSkip.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/FillWellAndSkip.tsx @@ -44,42 +44,32 @@ export function FillWellAndSkip(props: RecoveryContentProps): JSX.Element { } export function FillWell(props: RecoveryContentProps): JSX.Element | null { - const { - isOnDevice, - routeUpdateActions, - failedLabwareUtils, - deckMapUtils, - } = props + const { routeUpdateActions, failedLabwareUtils, deckMapUtils } = props const { t } = useTranslation('error_recovery') const { goBackPrevStep, proceedNextStep } = routeUpdateActions - if (isOnDevice) { - return ( - - - - - - - - - - - - ) - } else { - return null - } + return ( + + + + + + + + + + + + ) } export function SkipToNextStep( diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/IgnoreErrorSkipStep.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/IgnoreErrorSkipStep.tsx index 1808729876d..f3f255381ca 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/IgnoreErrorSkipStep.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/IgnoreErrorSkipStep.tsx @@ -35,7 +35,6 @@ export function IgnoreErrorSkipStep(props: RecoveryContentProps): JSX.Element { } export function IgnoreErrorStepHome({ - isOnDevice, recoveryCommands, routeUpdateActions, }: RecoveryContentProps): JSX.Element | null { @@ -78,29 +77,24 @@ export function IgnoreErrorStepHome({ } } - if (isOnDevice) { - return ( - - - {t('ignore_similar_errors_later_in_run')} - - - - - + + {t('ignore_similar_errors_later_in_run')} + + + - - ) - } else { - return null - } +
+ + + ) } interface IgnoreOptionsProps { diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/ManageTips.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/ManageTips.tsx index a761d881f95..a5bfc5eede5 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/ManageTips.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/ManageTips.tsx @@ -48,7 +48,6 @@ export function ManageTips(props: RecoveryContentProps): JSX.Element { type RemovalOptions = 'begin-removal' | 'skip' export function BeginRemoval({ - isOnDevice, tipStatusUtils, routeUpdateActions, recoveryCommands, @@ -87,39 +86,32 @@ export function BeginRemoval({ } } - if (isOnDevice) { - return ( - - - {t('you_may_want_to_remove', { mount })} - - - { - setSelected('begin-removal') - }} - isSelected={selected === 'begin-removal'} - /> - { - setSelected('skip') - }} - isSelected={selected === 'skip'} - /> - - + + {t('you_may_want_to_remove', { mount })} + + + { + setSelected('begin-removal') + }} + isSelected={selected === 'begin-removal'} /> - - ) - } else { - return null - } + { + setSelected('skip') + }} + isSelected={selected === 'skip'} + /> + + + + ) } function DropTipFlowsContainer( @@ -131,7 +123,6 @@ function DropTipFlowsContainer( recoveryCommands, isFlex, currentRecoveryOptionUtils, - isOnDevice, } = props const { DROP_TIP_FLOWS, ROBOT_CANCELING, RETRY_NEW_TIPS } = RECOVERY_MAP const { proceedToRouteAndStep, setRobotInMotion } = routeUpdateActions @@ -166,21 +157,17 @@ function DropTipFlowsContainer( const fixitCommandTypeUtils = useDropTipFlowUtils(props) - if (isOnDevice) { - return ( - - - - ) - } else { - return null - } + return ( + + + + ) } // Builds the overrides injected into DT Wiz. diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/SelectRecoveryOption.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/SelectRecoveryOption.tsx index 25b71b64d0d..17ccd2e853d 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/SelectRecoveryOption.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/SelectRecoveryOption.tsx @@ -40,7 +40,6 @@ export function SelectRecoveryOption(props: RecoveryContentProps): JSX.Element { } export function SelectRecoveryOptionHome({ - isOnDevice, errorKind, routeUpdateActions, tipStatusUtils, @@ -58,32 +57,27 @@ export function SelectRecoveryOptionHome({ useCurrentTipStatus(determineTipStatus) - if (isOnDevice) { - return ( - - - {t('choose_a_recovery_action')} - - - - - { - setSelectedRecoveryOption(selectedRoute) - void proceedToRouteAndStep(selectedRoute as RecoveryRoute) - }} + return ( + + + {t('choose_a_recovery_action')} + + + - - ) - } else { - return null - } + + { + setSelectedRecoveryOption(selectedRoute) + void proceedToRouteAndStep(selectedRoute as RecoveryRoute) + }} + /> + + ) } interface RecoveryOptionsProps { diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/CancelRun.test.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/CancelRun.test.tsx index 251c6fa43bd..31e991deef9 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/CancelRun.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/CancelRun.test.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { vi, describe, it, expect, beforeEach } from 'vitest' -import { screen, fireEvent, waitFor } from '@testing-library/react' +import { screen, waitFor } from '@testing-library/react' import { renderWithProviders } from '../../../../__testing-utils__' import { i18n } from '../../../../i18n' @@ -8,7 +8,7 @@ import { mockRecoveryContentProps } from '../../__fixtures__' import { CancelRun } from '../CancelRun' import { RECOVERY_MAP } from '../../constants' import { SelectRecoveryOption } from '../SelectRecoveryOption' - +import { clickButtonLabeled } from '../../__tests__/util' import type { Mock } from 'vitest' vi.mock('../SelectRecoveryOption') @@ -70,9 +70,7 @@ describe('RecoveryFooterButtons', () => { 'If tips are attached, you can choose to blowout any aspirated liquid and drop tips before the run is terminated.' ) - const secondaryBtn = screen.getByRole('button', { name: 'Go back' }) - - fireEvent.click(secondaryBtn) + clickButtonLabeled('Go back') expect(mockGoBackPrevStep).toHaveBeenCalled() }) @@ -95,8 +93,7 @@ describe('RecoveryFooterButtons', () => { routeUpdateActions: mockRouteUpdateActions, }) - const primaryBtn = screen.getByRole('button', { name: 'Confirm' }) - fireEvent.click(primaryBtn) + clickButtonLabeled('Confirm') await waitFor(() => { expect(setRobotInMotionMock).toHaveBeenCalledTimes(1) @@ -127,9 +124,7 @@ describe('RecoveryFooterButtons', () => { render(props) - const primaryBtn = screen.getByRole('button', { name: 'Confirm' }) - - fireEvent.click(primaryBtn) + clickButtonLabeled('Confirm') expect(mockProceedToRouteAndStep).toHaveBeenCalledWith(DROP_TIP_FLOWS.ROUTE) }) @@ -144,9 +139,7 @@ describe('RecoveryFooterButtons', () => { render(props) - const primaryBtn = screen.getByRole('button', { name: 'Confirm' }) - - fireEvent.click(primaryBtn) + clickButtonLabeled('Confirm') expect(mockProceedToRouteAndStep).not.toHaveBeenCalled() expect(mockSetRobotInMotion).not.toHaveBeenCalled() }) @@ -162,9 +155,7 @@ describe('RecoveryFooterButtons', () => { render(props) - const primaryBtn = screen.getByRole('button', { name: 'Confirm' }) - - fireEvent.click(primaryBtn) + clickButtonLabeled('Confirm') expect(mockProceedToRouteAndStep).not.toHaveBeenCalled() expect(mockSetRobotInMotion).toHaveBeenCalled() }) diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/FillWellAndSkip.test.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/FillWellAndSkip.test.tsx index 47e8d604dcf..541081f889a 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/FillWellAndSkip.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/FillWellAndSkip.test.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { describe, it, vi, expect, beforeEach } from 'vitest' -import { screen, fireEvent, waitFor } from '@testing-library/react' +import { screen, waitFor } from '@testing-library/react' import { mockRecoveryContentProps } from '../../__fixtures__' import { renderWithProviders } from '../../../../__testing-utils__' @@ -9,6 +9,7 @@ import { FillWellAndSkip, FillWell, SkipToNextStep } from '../FillWellAndSkip' import { RECOVERY_MAP } from '../../constants' import { CancelRun } from '../CancelRun' import { SelectRecoveryOption } from '../SelectRecoveryOption' +import { clickButtonLabeled } from '../../__tests__/util' import type { Mock } from 'vitest' @@ -172,7 +173,7 @@ describe('SkipToNextStep', () => { }, } renderSkipToNextStep(props) - fireEvent.click(screen.getByRole('button', { name: 'Go back' })) + clickButtonLabeled('Go back') expect(mockProceedToRouteAndStep).toHaveBeenCalledWith( RECOVERY_MAP.IGNORE_AND_SKIP.ROUTE ) @@ -180,14 +181,13 @@ describe('SkipToNextStep', () => { it('calls goBackPrevStep when selectedRecoveryOption is not IGNORE_AND_SKIP and secondary button is clicked', () => { renderSkipToNextStep(props) - fireEvent.click(screen.getByRole('button', { name: 'Go back' })) + clickButtonLabeled('Go back') expect(mockGoBackPrevStep).toHaveBeenCalled() }) it('calls the correct routeUpdateActions and recoveryCommands in the correct order when the primary button is clicked', async () => { renderSkipToNextStep(props) - fireEvent.click(screen.getByRole('button', { name: 'Continue run now' })) - + clickButtonLabeled('Continue run now') await waitFor(() => { expect(mockSetRobotInMotion).toHaveBeenCalledWith( true, diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/IgnoreErrorSkipStep.test.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/IgnoreErrorSkipStep.test.tsx index 68b16d11fd3..b0dd4ec9f1d 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/IgnoreErrorSkipStep.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/IgnoreErrorSkipStep.test.tsx @@ -12,6 +12,7 @@ import { } from '../IgnoreErrorSkipStep' import { RECOVERY_MAP } from '../../constants' import { SelectRecoveryOption } from '../SelectRecoveryOption' +import { clickButtonLabeled } from '../../__tests__/util' import type { Mock } from 'vitest' @@ -103,7 +104,7 @@ describe('IgnoreErrorStepHome', () => { it('calls ignoreOnce when "ignore_only_this_error" is selected and primary button is clicked', async () => { renderIgnoreErrorStepHome(props) fireEvent.click(screen.getByText('Ignore only this error')) - fireEvent.click(screen.getByRole('button', { name: 'Continue' })) + clickButtonLabeled('Continue') await waitFor(() => { expect(mockProceedToRouteAndStep).toHaveBeenCalledWith( RECOVERY_MAP.FILL_MANUALLY_AND_SKIP.ROUTE, @@ -115,7 +116,7 @@ describe('IgnoreErrorStepHome', () => { it('calls ignoreAlways when "ignore_all_errors_of_this_type" is selected and primary button is clicked', async () => { renderIgnoreErrorStepHome(props) fireEvent.click(screen.getByText('Ignore all errors of this type')) - fireEvent.click(screen.getByRole('button', { name: 'Continue' })) + clickButtonLabeled('Continue') await waitFor(() => { expect(mockIgnoreErrorKindThisRun).toHaveBeenCalled() }) @@ -129,7 +130,7 @@ describe('IgnoreErrorStepHome', () => { it('calls goBackPrevStep when secondary button is clicked', () => { renderIgnoreErrorStepHome(props) - fireEvent.click(screen.getByRole('button', { name: 'Go back' })) + clickButtonLabeled('Go back') expect(mockGoBackPrevStep).toHaveBeenCalled() }) }) diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/ManageTips.test.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/ManageTips.test.tsx index bc54129c614..d9fbd66af49 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/ManageTips.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/ManageTips.test.tsx @@ -16,6 +16,7 @@ import { RECOVERY_MAP } from '../../constants' import { DropTipWizardFlows } from '../../../DropTipWizardFlows' import { DT_ROUTES } from '../../../DropTipWizardFlows/constants' import { SelectRecoveryOption } from '../SelectRecoveryOption' +import { clickButtonLabeled } from '../../__tests__/util' import type { Mock } from 'vitest' import type { PipetteModelSpecs } from '@opentrons/shared-data' @@ -94,7 +95,7 @@ describe('ManageTips', () => { ) screen.getByText('Begin removal') screen.getByText('Skip') - screen.getByText('Continue') + expect(screen.getAllByText('Continue').length).toBe(2) }) it('routes correctly when continuing on BeginRemoval', () => { @@ -102,15 +103,14 @@ describe('ManageTips', () => { const beginRemovalBtn = screen.getByText('Begin removal') const skipBtn = screen.getByText('Skip') - const continueBtn = screen.getByRole('button', { name: 'Continue' }) fireEvent.click(beginRemovalBtn) - fireEvent.click(continueBtn) + clickButtonLabeled('Continue') expect(mockProceedNextStep).toHaveBeenCalled() fireEvent.click(skipBtn) - fireEvent.click(continueBtn) + clickButtonLabeled('Continue') expect(mockSetRobotInMotion).toHaveBeenCalled() }) @@ -125,10 +125,9 @@ describe('ManageTips', () => { render(props) const skipBtn = screen.getByText('Skip') - const continueBtn = screen.getByRole('button', { name: 'Continue' }) fireEvent.click(skipBtn) - fireEvent.click(continueBtn) + clickButtonLabeled('Continue') expect(mockProceedToRouteAndStep).toHaveBeenCalledWith( RETRY_NEW_TIPS.ROUTE, diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/RetryNewTips.test.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/RetryNewTips.test.tsx index 8e79d193f8f..708c847cd70 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/RetryNewTips.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/RetryNewTips.test.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { describe, it, vi, expect, beforeEach, afterEach } from 'vitest' -import { screen, fireEvent, waitFor } from '@testing-library/react' +import { screen, waitFor } from '@testing-library/react' import { mockRecoveryContentProps } from '../../__fixtures__' import { renderWithProviders } from '../../../../__testing-utils__' @@ -8,6 +8,7 @@ import { i18n } from '../../../../i18n' import { RetryNewTips, RetryWithNewTips } from '../RetryNewTips' import { RECOVERY_MAP } from '../../constants' import { SelectRecoveryOption } from '../SelectRecoveryOption' +import { clickButtonLabeled } from '../../__tests__/util' import type { Mock } from 'vitest' @@ -155,7 +156,7 @@ describe('RetryWithNewTips', () => { it('calls the correct routeUpdateActions and recoveryCommands in the correct order when the primary button is clicked', async () => { renderRetryWithNewTips(props) - fireEvent.click(screen.getByRole('button', { name: 'Retry now' })) + clickButtonLabeled('Retry now') await waitFor(() => { expect(mockSetRobotInMotion).toHaveBeenCalledWith( diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/RetrySameTips.test.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/RetrySameTips.test.tsx index f2e3cbd2b48..da7e85fcadc 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/RetrySameTips.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/RetrySameTips.test.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { describe, it, vi, expect, beforeEach, afterEach } from 'vitest' -import { screen, fireEvent, waitFor } from '@testing-library/react' +import { screen, waitFor } from '@testing-library/react' import { mockRecoveryContentProps } from '../../__fixtures__' import { renderWithProviders } from '../../../../__testing-utils__' @@ -8,6 +8,7 @@ import { i18n } from '../../../../i18n' import { RetrySameTips, RetrySameTipsInfo } from '../RetrySameTips' import { RECOVERY_MAP } from '../../constants' import { SelectRecoveryOption } from '../SelectRecoveryOption' +import { clickButtonLabeled } from '../../__tests__/util' import type { Mock } from 'vitest' @@ -106,7 +107,8 @@ describe('RetrySameTipsInfo', () => { it('calls the correct routeUpdateActions and recoveryCommands in the correct order when the primary button is clicked', async () => { renderRetrySameTipsInfo(props) - fireEvent.click(screen.getByRole('button', { name: 'Retry now' })) + + clickButtonLabeled('Retry now') await waitFor(() => { expect(mockSetRobotInMotion).toHaveBeenCalledWith( diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/RetryStep.test.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/RetryStep.test.tsx index bbd0b6a742e..40a7095b51d 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/RetryStep.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/RetryStep.test.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { describe, it, vi, expect, beforeEach, afterEach } from 'vitest' -import { screen, fireEvent, waitFor } from '@testing-library/react' +import { screen, waitFor } from '@testing-library/react' import { mockRecoveryContentProps } from '../../__fixtures__' import { renderWithProviders } from '../../../../__testing-utils__' @@ -9,6 +9,8 @@ import { RetryStep, RetryStepInfo } from '../RetryStep' import { RECOVERY_MAP } from '../../constants' import { SelectRecoveryOption } from '../SelectRecoveryOption' +import { clickButtonLabeled } from '../../__tests__/util' + import type { Mock } from 'vitest' vi.mock('../../../../molecules/Command') @@ -108,7 +110,7 @@ describe('RetryStepInfo', () => { it('calls the correct routeUpdateActions and recoveryCommands in the correct order when the primary button is clicked', async () => { renderRetryStepInfo(props) - fireEvent.click(screen.getByRole('button', { name: 'Retry now' })) + clickButtonLabeled('Retry now') await waitFor(() => { expect(mockSetRobotInMotion).toHaveBeenCalledWith( diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/SelectRecoveryOptions.test.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/SelectRecoveryOptions.test.tsx index c73afd73539..0db6521b2fc 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/SelectRecoveryOptions.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/SelectRecoveryOptions.test.tsx @@ -17,6 +17,7 @@ import { NO_LIQUID_DETECTED_OPTIONS, } from '../SelectRecoveryOption' import { RECOVERY_MAP, ERROR_KINDS } from '../../constants' +import { clickButtonLabeled } from '../../__tests__/util' import type { Mock } from 'vitest' @@ -88,8 +89,7 @@ describe('SelectRecoveryOption', () => { it('sets the selected recovery option when clicking continue', () => { renderSelectRecoveryOption(props) - const continueBtn = screen.getByRole('button', { name: 'Continue' }) - fireEvent.click(continueBtn) + clickButtonLabeled('Continue') expect(mockSetSelectedRecoveryOption).toHaveBeenCalledWith( RETRY_FAILED_COMMAND.ROUTE @@ -102,13 +102,13 @@ describe('SelectRecoveryOption', () => { screen.getByText('Choose a recovery action') const retryStepOption = screen.getByRole('label', { name: 'Retry step' }) - const continueBtn = screen.getByRole('button', { name: 'Continue' }) + clickButtonLabeled('Continue') expect( screen.queryByRole('button', { name: 'Go back' }) ).not.toBeInTheDocument() fireEvent.click(retryStepOption) - fireEvent.click(continueBtn) + clickButtonLabeled('Continue') expect(mockProceedToRouteAndStep).toHaveBeenCalledWith( RETRY_FAILED_COMMAND.ROUTE @@ -128,13 +128,12 @@ describe('SelectRecoveryOption', () => { const retryNewTips = screen.getByRole('label', { name: 'Retry with new tips', }) - const continueBtn = screen.getByRole('button', { name: 'Continue' }) expect( screen.queryByRole('button', { name: 'Go back' }) ).not.toBeInTheDocument() fireEvent.click(retryNewTips) - fireEvent.click(continueBtn) + clickButtonLabeled('Continue') expect(mockProceedToRouteAndStep).toHaveBeenCalledWith(RETRY_NEW_TIPS.ROUTE) }) @@ -152,10 +151,9 @@ describe('SelectRecoveryOption', () => { const fillManuallyAndSkip = screen.getByRole('label', { name: 'Manually fill well and skip to next step', }) - const continueBtn = screen.getByRole('button', { name: 'Continue' }) fireEvent.click(fillManuallyAndSkip) - fireEvent.click(continueBtn) + clickButtonLabeled('Continue') expect(mockProceedToRouteAndStep).toHaveBeenCalledWith( RECOVERY_MAP.FILL_MANUALLY_AND_SKIP.ROUTE @@ -175,10 +173,9 @@ describe('SelectRecoveryOption', () => { const retrySameTips = screen.getByRole('label', { name: 'Retry with same tips', }) - const continueBtn = screen.getByRole('button', { name: 'Continue' }) fireEvent.click(retrySameTips) - fireEvent.click(continueBtn) + clickButtonLabeled('Continue') expect(mockProceedToRouteAndStep).toHaveBeenCalledWith( RECOVERY_MAP.RETRY_SAME_TIPS.ROUTE @@ -198,10 +195,9 @@ describe('SelectRecoveryOption', () => { const skipStepWithSameTips = screen.getByRole('label', { name: 'Skip to next step with same tips', }) - const continueBtn = screen.getByRole('button', { name: 'Continue' }) fireEvent.click(skipStepWithSameTips) - fireEvent.click(continueBtn) + clickButtonLabeled('Continue') expect(mockProceedToRouteAndStep).toHaveBeenCalledWith( RECOVERY_MAP.SKIP_STEP_WITH_SAME_TIPS.ROUTE diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/SkipStepNewTips.test.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/SkipStepNewTips.test.tsx index 25ea34dfbf3..c7c9b6d27f2 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/SkipStepNewTips.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/SkipStepNewTips.test.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { describe, it, vi, expect, beforeEach, afterEach } from 'vitest' -import { screen, fireEvent, waitFor } from '@testing-library/react' +import { screen, waitFor } from '@testing-library/react' import { mockRecoveryContentProps } from '../../__fixtures__' import { renderWithProviders } from '../../../../__testing-utils__' @@ -8,6 +8,7 @@ import { i18n } from '../../../../i18n' import { SkipStepNewTips, SkipStepWithNewTips } from '../SkipStepNewTips' import { RECOVERY_MAP } from '../../constants' import { SelectRecoveryOption } from '../SelectRecoveryOption' +import { clickButtonLabeled } from '../../__tests__/util' import type { Mock } from 'vitest' @@ -157,7 +158,7 @@ describe('SkipStepWithNewTips', () => { it('calls the correct routeUpdateActions and recoveryCommands in the correct order when the primary button is clicked', async () => { renderSkipStepWithNewTips(props) - fireEvent.click(screen.getByRole('button', { name: 'Continue run now' })) + clickButtonLabeled('Continue run now') await waitFor(() => { expect(mockSetRobotInMotion).toHaveBeenCalledWith( diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/SkipStepSameTips.test.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/SkipStepSameTips.test.tsx index 6c8c3ec4c54..1b6cd3c275e 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/SkipStepSameTips.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/SkipStepSameTips.test.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { describe, it, vi, expect, beforeEach, afterEach } from 'vitest' -import { screen, fireEvent, waitFor } from '@testing-library/react' +import { screen, waitFor } from '@testing-library/react' import { mockRecoveryContentProps } from '../../__fixtures__' import { renderWithProviders } from '../../../../__testing-utils__' @@ -9,6 +9,8 @@ import { SkipStepSameTips, SkipStepSameTipsInfo } from '../SkipStepSameTips' import { RECOVERY_MAP } from '../../constants' import { SelectRecoveryOption } from '../SelectRecoveryOption' +import { clickButtonLabeled } from '../../__tests__/util' + import type { Mock } from 'vitest' vi.mock('../../../../molecules/Command') @@ -104,7 +106,7 @@ describe('SkipStepSameTipsInfo', () => { it('calls the correct routeUpdateActions and recoveryCommands in the correct order when the primary button is clicked', async () => { renderSkipStepSameTipsInfo(props) - fireEvent.click(screen.getByRole('button', { name: 'Continue run now' })) + clickButtonLabeled('Continue run now') await waitFor(() => { expect(mockSetRobotInMotion).toHaveBeenCalledWith( diff --git a/app/src/organisms/ErrorRecoveryFlows/__tests__/BeforeBeginning.test.tsx b/app/src/organisms/ErrorRecoveryFlows/__tests__/BeforeBeginning.test.tsx deleted file mode 100644 index 676fad63a88..00000000000 --- a/app/src/organisms/ErrorRecoveryFlows/__tests__/BeforeBeginning.test.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import * as React from 'react' -import { beforeEach, describe, expect, it, vi } from 'vitest' -import { fireEvent, screen } from '@testing-library/react' - -import { renderWithProviders } from '../../../__testing-utils__' -import { i18n } from '../../../i18n' -import { mockRecoveryContentProps } from '../__fixtures__' -import { BeforeBeginning } from '../BeforeBeginning' -import { RECOVERY_MAP } from '../constants' - -import type { Mock } from 'vitest' - -const render = (props: React.ComponentProps) => { - return renderWithProviders(, { - i18nInstance: i18n, - })[0] -} - -describe('BeforeBeginning', () => { - const { BEFORE_BEGINNING } = RECOVERY_MAP - let props: React.ComponentProps - let mockProceedNextStep: Mock - - beforeEach(() => { - mockProceedNextStep = vi.fn() - const mockRouteUpdateActions = { - proceedNextStep: mockProceedNextStep, - } as any - - props = { - ...mockRecoveryContentProps, - routeUpdateActions: mockRouteUpdateActions, - recoveryMap: { - route: BEFORE_BEGINNING.ROUTE, - step: BEFORE_BEGINNING.STEPS.RECOVERY_DESCRIPTION, - }, - } - }) - - it('renders appropriate copy and click behavior', () => { - render(props) - - screen.getByText('Before you begin') - screen.queryByText( - 'Recovery Mode provides you with guided and manual controls for handling errors at runtime.' - ) - - const primaryBtn = screen.getByRole('button', { - name: 'View recovery options', - }) - - fireEvent.click(primaryBtn) - - expect(mockProceedNextStep).toHaveBeenCalled() - }) -}) diff --git a/app/src/organisms/ErrorRecoveryFlows/__tests__/ErrorRecoveryWizard.test.tsx b/app/src/organisms/ErrorRecoveryFlows/__tests__/ErrorRecoveryWizard.test.tsx index a3656163f20..9d9e7586d00 100644 --- a/app/src/organisms/ErrorRecoveryFlows/__tests__/ErrorRecoveryWizard.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/__tests__/ErrorRecoveryWizard.test.tsx @@ -11,7 +11,6 @@ import { useERWizard, } from '../ErrorRecoveryWizard' import { RECOVERY_MAP } from '../constants' -import { BeforeBeginning } from '../BeforeBeginning' import { SelectRecoveryOption, RetryStep, @@ -29,7 +28,6 @@ import { RecoveryError } from '../RecoveryError' import type { Mock } from 'vitest' -vi.mock('../BeforeBeginning') vi.mock('../RecoveryOptions') vi.mock('../RecoveryInProgress') vi.mock('../RecoveryError') @@ -72,7 +70,6 @@ const renderRecoveryContent = ( describe('ErrorRecoveryContent', () => { const { OPTION_SELECTION, - BEFORE_BEGINNING, RETRY_FAILED_COMMAND, ROBOT_CANCELING, ROBOT_RESUMING, @@ -99,7 +96,6 @@ describe('ErrorRecoveryContent', () => { vi.mocked(SelectRecoveryOption).mockReturnValue(
MOCK_SELECT_RECOVERY_OPTION
) - vi.mocked(BeforeBeginning).mockReturnValue(
MOCK_BEFORE_BEGINNING
) vi.mocked(RetryStep).mockReturnValue(
MOCK_RESUME_RUN
) vi.mocked(RecoveryInProgress).mockReturnValue(
MOCK_IN_PROGRESS
) vi.mocked(CancelRun).mockReturnValue(
MOCK_CANCEL_RUN
) @@ -125,19 +121,6 @@ describe('ErrorRecoveryContent', () => { screen.getByText('MOCK_SELECT_RECOVERY_OPTION') }) - it(`returns BeforeBeginning when the route is ${BEFORE_BEGINNING.ROUTE}`, () => { - props = { - ...props, - recoveryMap: { - ...props.recoveryMap, - route: BEFORE_BEGINNING.ROUTE, - }, - } - renderRecoveryContent(props) - - screen.getByText('MOCK_BEFORE_BEGINNING') - }) - it(`returns ResumeRun when the route is ${RETRY_FAILED_COMMAND.ROUTE}`, () => { props = { ...props, diff --git a/app/src/organisms/ErrorRecoveryFlows/__tests__/util.ts b/app/src/organisms/ErrorRecoveryFlows/__tests__/util.ts new file mode 100644 index 00000000000..2e4f673f02b --- /dev/null +++ b/app/src/organisms/ErrorRecoveryFlows/__tests__/util.ts @@ -0,0 +1,6 @@ +import { screen, fireEvent } from '@testing-library/react' + +export function clickButtonLabeled(label: string): void { + const buttons = screen.getAllByRole('button', { name: label }) + fireEvent.click(buttons[0]) +} diff --git a/app/src/organisms/ErrorRecoveryFlows/constants.ts b/app/src/organisms/ErrorRecoveryFlows/constants.ts index c551af98b20..7e9a9ab2a9a 100644 --- a/app/src/organisms/ErrorRecoveryFlows/constants.ts +++ b/app/src/organisms/ErrorRecoveryFlows/constants.ts @@ -24,12 +24,6 @@ export const ERROR_KINDS = { // TODO(jh, 06-14-24): Consolidate motion routes to a single route with several steps. // Valid recovery routes and steps. export const RECOVERY_MAP = { - BEFORE_BEGINNING: { - ROUTE: 'before-beginning', - STEPS: { - RECOVERY_DESCRIPTION: 'recovery-description', - }, - }, DROP_TIP_FLOWS: { ROUTE: 'drop-tip', STEPS: { @@ -139,7 +133,6 @@ export const RECOVERY_MAP = { } as const const { - BEFORE_BEGINNING, OPTION_SELECTION, RETRY_FAILED_COMMAND, ROBOT_CANCELING, @@ -162,7 +155,6 @@ const { // The deterministic ordering of steps for a given route. export const STEP_ORDER: StepOrder = { - [BEFORE_BEGINNING.ROUTE]: [BEFORE_BEGINNING.STEPS.RECOVERY_DESCRIPTION], [OPTION_SELECTION.ROUTE]: [OPTION_SELECTION.STEPS.SELECT], [RETRY_FAILED_COMMAND.ROUTE]: [RETRY_FAILED_COMMAND.STEPS.CONFIRM_RETRY], [RETRY_NEW_TIPS.ROUTE]: [ diff --git a/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useRecoveryRouting.test.ts b/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useRecoveryRouting.test.ts index 5e5543ca268..0bc23d79ee7 100644 --- a/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useRecoveryRouting.test.ts +++ b/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useRecoveryRouting.test.ts @@ -20,8 +20,8 @@ describe('useRecoveryRouting', () => { const { result } = renderHook(() => useRecoveryRouting()) const newRecoveryMap = { - route: RECOVERY_MAP.BEFORE_BEGINNING.ROUTE, - step: RECOVERY_MAP.BEFORE_BEGINNING.STEPS.RECOVERY_DESCRIPTION, + route: RECOVERY_MAP.ERROR_WHILE_RECOVERING.ROUTE, + step: RECOVERY_MAP.ERROR_WHILE_RECOVERING.STEPS.RECOVERY_ACTION_FAILED, } as IRecoveryMap act(() => { diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/LeftColumnLabwareInfo.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/LeftColumnLabwareInfo.tsx index 0229cf93dab..2c38ec645c6 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/LeftColumnLabwareInfo.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/LeftColumnLabwareInfo.tsx @@ -15,7 +15,6 @@ type LeftColumnLabwareInfoProps = RecoveryContentProps & { export function LeftColumnLabwareInfo({ title, failedLabwareUtils, - isOnDevice, type, bannerText, }: LeftColumnLabwareInfoProps): JSX.Element | null { diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/RecoveryContentWrapper.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/RecoveryContentWrapper.tsx index f4a8fdbbaad..344caae0590 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/RecoveryContentWrapper.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/RecoveryContentWrapper.tsx @@ -25,7 +25,6 @@ export function RecoveryContentWrapper({ }: SingleColumnContentWrapperProps): JSX.Element { return ( void /* The "Go back" button */ secondaryBtnOnClick?: () => void @@ -28,46 +30,47 @@ interface RecoveryFooterButtonProps { export function RecoveryFooterButtons( props: RecoveryFooterButtonProps ): JSX.Element | null { - const { isOnDevice, secondaryBtnOnClick } = props - const { t } = useTranslation('error_recovery') + return ( + + + + + ) +} +function RecoveryGoBackButton({ + secondaryBtnOnClick, +}: RecoveryFooterButtonProps): JSX.Element | null { const showGoBackBtn = secondaryBtnOnClick != null - - if (isOnDevice) { - return ( - - - {showGoBackBtn ? ( - - ) : null} - - - - ) - } else { - return null - } + const { t } = useTranslation('error_recovery') + return showGoBackBtn ? ( + + + + {t('go_back')} + + + ) : null } function PrimaryButtonGroup(props: RecoveryFooterButtonProps): JSX.Element { - const { tertiaryBtnDisabled, tertiaryBtnOnClick, tertiaryBtnText } = props + const { tertiaryBtnOnClick, tertiaryBtnText } = props const renderTertiaryBtn = tertiaryBtnOnClick != null || tertiaryBtnText != null - const tertiaryBtnDefaultOnClick = (): null => null - if (!renderTertiaryBtn) { return ( @@ -77,12 +80,7 @@ function PrimaryButtonGroup(props: RecoveryFooterButtonProps): JSX.Element { } else { return ( - + ) @@ -97,15 +95,57 @@ function RecoveryPrimaryBtn({ const { t } = useTranslation('error_recovery') return ( - + <> + + + {primaryBtnTextOverride ?? t('continue')} + + + ) +} + +function RecoveryTertiaryBtn({ + tertiaryBtnOnClick, + tertiaryBtnText, + tertiaryBtnDisabled, +}: RecoveryFooterButtonProps): JSX.Element { + const tertiaryBtnDefaultOnClick = (): null => null + + return ( + <> + + + {tertiaryBtnText} + + ) } @@ -124,3 +164,15 @@ const PRESSED_LOADING_STATE = css` background-color: ${COLORS.blue60}; } ` + +const ODD_ONLY_BUTTON = css` + @media not (${RESPONSIVENESS.touchscreenMediaQuerySpecs}) { + display: none; + } +` + +const DESKTOP_ONLY_BUTTON = css` + @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { + display: none; + } +` diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/RecoveryInterventionModal.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/RecoveryInterventionModal.tsx index 5855357b4a3..fdfe23e5ec6 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/RecoveryInterventionModal.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/RecoveryInterventionModal.tsx @@ -2,7 +2,7 @@ import * as React from 'react' import { createPortal } from 'react-dom' import { css } from 'styled-components' -import { Flex, RESPONSIVENESS } from '@opentrons/components' +import { Flex, RESPONSIVENESS, SPACING } from '@opentrons/components' import { InterventionModal } from '../../../molecules/InterventionModal' import { getModalPortalEl } from '../../../App/portal' @@ -36,6 +36,7 @@ export function RecoveryInterventionModal({ ? SMALL_MODAL_STYLE : LARGE_MODAL_STYLE } + padding={SPACING.spacing32} > {children} diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/ReplaceTips.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/ReplaceTips.tsx index dc0bece20f6..e577abb7bb1 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/ReplaceTips.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/ReplaceTips.tsx @@ -12,7 +12,6 @@ import type { RecoveryContentProps } from '../types' export function ReplaceTips(props: RecoveryContentProps): JSX.Element | null { const { - isOnDevice, routeUpdateActions, failedPipetteInfo, failedLabwareUtils, @@ -36,27 +35,20 @@ export function ReplaceTips(props: RecoveryContentProps): JSX.Element | null { } } - if (isOnDevice) { - return ( - - - - - - - - + + - - ) - } else { - return null - } + + + + + + + ) } diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/SelectTips.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/SelectTips.tsx index 4f5716198a5..f51d9d2ddd6 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/SelectTips.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/SelectTips.tsx @@ -12,12 +12,7 @@ import { TipSelection } from './TipSelection' import type { RecoveryContentProps } from '../types' export function SelectTips(props: RecoveryContentProps): JSX.Element | null { - const { - failedPipetteInfo, - isOnDevice, - routeUpdateActions, - recoveryCommands, - } = props + const { failedPipetteInfo, routeUpdateActions, recoveryCommands } = props const { ROBOT_PICKING_UP_TIPS } = RECOVERY_MAP const { pickUpTips } = recoveryCommands const { @@ -38,39 +33,34 @@ export function SelectTips(props: RecoveryContentProps): JSX.Element | null { setShowTipSelectModal(!showTipSelectModal) } - if (isOnDevice) { - return ( - <> - {showTipSelectModal && ( - + {showTipSelectModal && ( + + )} + + + - )} - - - - - - - - - ) - } else { - return null - } + + + + + + ) } diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/TwoColTextAndFailedStepNextStep.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/TwoColTextAndFailedStepNextStep.tsx index a6185768294..a9bdd50399f 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/TwoColTextAndFailedStepNextStep.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/TwoColTextAndFailedStepNextStep.tsx @@ -38,7 +38,6 @@ export function TwoColTextAndFailedStepNextStep({ primaryBtnOnClick, secondaryBtnOnClickOverride, secondaryBtnOnClickCopyOverride, - isOnDevice, routeUpdateActions, failedCommand, stepCounts, @@ -114,7 +113,6 @@ export function TwoColTextAndFailedStepNextStep({ /> { mockSecondaryBtnOnClick = vi.fn() mockTertiaryBtnOnClick = vi.fn() props = { - isOnDevice: true, primaryBtnOnClick: mockPrimaryBtnOnClick, secondaryBtnOnClick: mockSecondaryBtnOnClick, } @@ -36,21 +35,32 @@ describe('RecoveryFooterButtons', () => { it('renders default button copy and click behavior', () => { render(props) - const primaryBtn = screen.getByRole('button', { name: 'Continue' }) - const secondaryBtn = screen.getByRole('button', { name: 'Go back' }) + const primaryBtns = screen.getAllByRole('button', { name: 'Continue' }) + const secondaryBtns = screen.getAllByRole('button', { name: 'Go back' }) + expect(primaryBtns.length).toBe(2) + expect(secondaryBtns.length).toBe(2) - fireEvent.click(primaryBtn) - fireEvent.click(secondaryBtn) + primaryBtns.forEach(btn => { + mockPrimaryBtnOnClick.mockReset() + fireEvent.click(btn) + expect(mockPrimaryBtnOnClick).toHaveBeenCalled() + }) - expect(mockPrimaryBtnOnClick).toHaveBeenCalled() - expect(mockSecondaryBtnOnClick).toHaveBeenCalled() + secondaryBtns.forEach(btn => { + mockSecondaryBtnOnClick.mockReset() + fireEvent.click(btn) + expect(mockSecondaryBtnOnClick).toHaveBeenCalled() + }) }) it('renders alternative button text when supplied', () => { props = { ...props, primaryBtnTextOverride: 'MOCK_OVERRIDE_TEXT' } render(props) - screen.getByRole('button', { name: 'MOCK_OVERRIDE_TEXT' }) + const secondaries = screen.getAllByRole('button', { + name: 'MOCK_OVERRIDE_TEXT', + }) + expect(secondaries.length).toBe(2) }) it('does not render the secondary button if no on click handler is supplied', () => { @@ -66,30 +76,36 @@ describe('RecoveryFooterButtons', () => { props = { ...props, isLoadingPrimaryBtnAction: true } render(props) - const primaryBtn = screen.getByRole('button', { + const primaryBtns = screen.getAllByRole('button', { name: 'loading indicator Continue', }) screen.getByLabelText('loading indicator') - expect(primaryBtn).toHaveStyle(`background-color: ${COLORS.blue60}`) + primaryBtns.forEach(btn => { + expect(btn).toHaveStyle(`background-color: ${COLORS.blue60}`) + }) }) it('renders the tertiary button when tertiaryBtnOnClick is provided', () => { props = { ...props, tertiaryBtnOnClick: mockTertiaryBtnOnClick } render(props) - const tertiaryBtn = screen.getByRole('button', { name: '' }) + const tertiaryBtns = screen.getAllByRole('button', { name: '' }) + expect(tertiaryBtns.length).toBe(2) - fireEvent.click(tertiaryBtn) - - expect(mockTertiaryBtnOnClick).toHaveBeenCalled() + tertiaryBtns.forEach(btn => { + mockTertiaryBtnOnClick.mockReset() + fireEvent.click(btn) + expect(mockTertiaryBtnOnClick).toHaveBeenCalled() + }) }) it('renders the tertiary button with custom text when tertiaryBtnText is provided', () => { props = { ...props, tertiaryBtnText: 'Hey' } render(props) - screen.getByRole('button', { name: 'Hey' }) + const tertiaryBtns = screen.getAllByRole('button', { name: 'Hey' }) + expect(tertiaryBtns.length).toBe(2) }) it('renders the tertiary button as disabled when tertiaryBtnDisabled is true', () => { @@ -101,8 +117,10 @@ describe('RecoveryFooterButtons', () => { } render(props) - const tertiaryBtn = screen.getByRole('button', { name: 'Hi' }) + const tertiaryBtns = screen.getAllByRole('button', { name: 'Hi' }) - expect(tertiaryBtn).toBeDisabled() + tertiaryBtns.forEach(btn => { + expect(btn).toBeDisabled() + }) }) }) diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/SelectTips.test.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/SelectTips.test.tsx index 36e486e2504..15afe841639 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/SelectTips.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/SelectTips.test.tsx @@ -59,7 +59,7 @@ describe('SelectTips', () => { it('renders the TipSelectionModal when showTipSelectModal is true', () => { render(props) - fireEvent.click(screen.getByText('Change location')) + fireEvent.click(screen.getAllByText('Change location')[0]) expect(screen.getByText('MOCK TIP SELECTION MODAL')).toBeInTheDocument() }) @@ -84,7 +84,7 @@ describe('SelectTips', () => { routeUpdateActions: mockRouteUpdateActions, }) - const primaryBtn = screen.getByText('Pick up tips') + const primaryBtn = screen.getAllByText('Pick up tips')[0] fireEvent.click(primaryBtn) await waitFor(() => { @@ -117,7 +117,7 @@ describe('SelectTips', () => { it('calls goBackPrevStep when the secondary button is clicked', () => { render(props) - fireEvent.click(screen.getByText('Go back')) + fireEvent.click(screen.getAllByText('Go back')[0]) expect(mockGoBackPrevStep).toHaveBeenCalled() }) @@ -133,7 +133,9 @@ describe('SelectTips', () => { } render(props) - const tertiaryBtn = screen.getByRole('button', { name: 'Change location' }) - expect(tertiaryBtn).toBeDisabled() + const tertiaryBtn = screen.getAllByRole('button', { + name: 'Change location', + }) + expect(tertiaryBtn[0]).toBeDisabled() }) }) From 0902ff4436f71278cd83f8841a2cbc3df2fe4a36 Mon Sep 17 00:00:00 2001 From: Max Marrone Date: Fri, 12 Jul 2024 17:29:02 -0400 Subject: [PATCH 12/78] refactor(api): Split CommandStore.handle_action() (#15643) --- .../protocol_engine/state/commands.py | 382 +++++++++--------- 1 file changed, 195 insertions(+), 187 deletions(-) diff --git a/api/src/opentrons/protocol_engine/state/commands.py b/api/src/opentrons/protocol_engine/state/commands.py index 209cb5872a1..a558210cbff 100644 --- a/api/src/opentrons/protocol_engine/state/commands.py +++ b/api/src/opentrons/protocol_engine/state/commands.py @@ -232,221 +232,229 @@ def __init__( stopped_by_estop=False, ) - def handle_action(self, action: Action) -> None: # noqa: C901 + def handle_action(self, action: Action) -> None: """Modify state in reaction to an action.""" - if isinstance(action, QueueCommandAction): - # TODO(mc, 2021-06-22): mypy has trouble with this automatic - # request > command mapping, figure out how to type precisely - # (or wait for a future mypy version that can figure it out). - queued_command = action.request._CommandCls.construct( - id=action.command_id, - key=( - action.request.key - if action.request.key is not None - else (action.request_hash or action.command_id) - ), - createdAt=action.created_at, - params=action.request.params, # type: ignore[arg-type] - intent=action.request.intent, - status=CommandStatus.QUEUED, - failedCommandId=action.failed_command_id, - ) + match action: + case QueueCommandAction(): + self._handle_queue_command_action(action) + case RunCommandAction(): + self._handle_run_command_action(action) + case SucceedCommandAction(): + self._handle_succeed_command_action(action) + case FailCommandAction(): + self._handle_fail_command_action(action) + case PlayAction(): + self._handle_play_action(action) + case PauseAction(): + self._handle_pause_action(action) + case ResumeFromRecoveryAction(): + self._handle_resume_from_recovery_action(action) + case StopAction(): + self._handle_stop_action(action) + case FinishAction(): + self._handle_finish_action(action) + case HardwareStoppedAction(): + self._handle_hardware_stopped_action(action) + case DoorChangeAction(): + self._handle_door_change_action(action) + case _: + pass + + def _handle_queue_command_action(self, action: QueueCommandAction) -> None: + # TODO(mc, 2021-06-22): mypy has trouble with this automatic + # request > command mapping, figure out how to type precisely + # (or wait for a future mypy version that can figure it out). + queued_command = action.request._CommandCls.construct( + id=action.command_id, + key=( + action.request.key + if action.request.key is not None + else (action.request_hash or action.command_id) + ), + createdAt=action.created_at, + params=action.request.params, # type: ignore[arg-type] + intent=action.request.intent, + status=CommandStatus.QUEUED, + failedCommandId=action.failed_command_id, + ) - self._state.command_history.append_queued_command(queued_command) + self._state.command_history.append_queued_command(queued_command) - if action.request_hash is not None: - self._state.latest_protocol_command_hash = action.request_hash + if action.request_hash is not None: + self._state.latest_protocol_command_hash = action.request_hash - elif isinstance(action, RunCommandAction): - prev_entry = self._state.command_history.get(action.command_id) + def _handle_run_command_action(self, action: RunCommandAction) -> None: + prev_entry = self._state.command_history.get(action.command_id) - running_command = prev_entry.command.copy( - update={ - "status": CommandStatus.RUNNING, - "startedAt": action.started_at, - } - ) + running_command = prev_entry.command.copy( + update={ + "status": CommandStatus.RUNNING, + "startedAt": action.started_at, + } + ) + + self._state.command_history.set_command_running(running_command) - self._state.command_history.set_command_running(running_command) + def _handle_succeed_command_action(self, action: SucceedCommandAction) -> None: + succeeded_command = action.command + self._state.command_history.set_command_succeeded(succeeded_command) - elif isinstance(action, SucceedCommandAction): - succeeded_command = action.command - self._state.command_history.set_command_succeeded(succeeded_command) + def _handle_fail_command_action(self, action: FailCommandAction) -> None: + prev_entry = self.state.command_history.get(action.command_id) - elif isinstance(action, FailCommandAction): - if isinstance(action.error, EnumeratedError): - public_error_occurrence = ErrorOccurrence.from_failed( - id=action.error_id, - createdAt=action.failed_at, - error=action.error, + if isinstance(action.error, EnumeratedError): + public_error_occurrence = ErrorOccurrence.from_failed( + id=action.error_id, + createdAt=action.failed_at, + error=action.error, + ) + else: + public_error_occurrence = action.error.public + + self._update_to_failed( + command_id=action.command_id, + failed_at=action.failed_at, + error_occurrence=public_error_occurrence, + error_recovery_type=action.type, + notes=action.notes, + ) + self._state.failed_command = self._state.command_history.get(action.command_id) + + other_command_ids_to_fail: List[str] + if prev_entry.command.intent == CommandIntent.SETUP: + other_command_ids_to_fail = list( + self._state.command_history.get_setup_queue_ids() + ) + elif prev_entry.command.intent == CommandIntent.FIXIT: + other_command_ids_to_fail = list( + self._state.command_history.get_fixit_queue_ids() + ) + elif ( + prev_entry.command.intent == CommandIntent.PROTOCOL + or prev_entry.command.intent is None + ): + if action.type == ErrorRecoveryType.FAIL_RUN: + other_command_ids_to_fail = list( + self._state.command_history.get_queue_ids() ) + elif action.type == ErrorRecoveryType.WAIT_FOR_RECOVERY: + other_command_ids_to_fail = [] else: - public_error_occurrence = action.error.public - - prev_entry = self.state.command_history.get(action.command_id) + assert_never(action.type) + else: + assert_never(prev_entry.command.intent) + for command_id in other_command_ids_to_fail: + # TODO(mc, 2022-06-06): add new "cancelled" status or similar self._update_to_failed( - command_id=action.command_id, + command_id=command_id, failed_at=action.failed_at, - error_occurrence=public_error_occurrence, - error_recovery_type=action.type, - notes=action.notes, + error_occurrence=None, + error_recovery_type=None, + notes=None, ) - self._state.failed_command = self._state.command_history.get( - action.command_id - ) + if ( + prev_entry.command.intent in (CommandIntent.PROTOCOL, None) + and action.type == ErrorRecoveryType.WAIT_FOR_RECOVERY + ): + self._state.queue_status = QueueStatus.AWAITING_RECOVERY + self._state.recovery_target_command_id = action.command_id - if prev_entry.command.intent == CommandIntent.SETUP: - other_command_ids_to_fail = list( - # Copy to avoid it mutating as we remove elements below. - self._state.command_history.get_setup_queue_ids() - ) - for command_id in other_command_ids_to_fail: - # TODO(mc, 2022-06-06): add new "cancelled" status or similar - self._update_to_failed( - command_id=command_id, - failed_at=action.failed_at, - error_occurrence=None, - error_recovery_type=None, - notes=None, + def _handle_play_action(self, action: PlayAction) -> None: + if not self._state.run_result: + self._state.run_started_at = ( + self._state.run_started_at or action.requested_at + ) + match self._state.queue_status: + case QueueStatus.SETUP: + self._state.queue_status = ( + QueueStatus.PAUSED + if self._state.is_door_blocking + else QueueStatus.RUNNING ) - elif ( - prev_entry.command.intent == CommandIntent.PROTOCOL - or prev_entry.command.intent is None - ): - if action.type == ErrorRecoveryType.WAIT_FOR_RECOVERY: + case QueueStatus.AWAITING_RECOVERY_PAUSED: self._state.queue_status = QueueStatus.AWAITING_RECOVERY - self._state.recovery_target_command_id = action.command_id - elif action.type == ErrorRecoveryType.FAIL_RUN: - other_command_ids_to_fail = list( - # Copy to avoid it mutating as we remove elements below. - self._state.command_history.get_queue_ids() - ) - for command_id in other_command_ids_to_fail: - # TODO(mc, 2022-06-06): add new "cancelled" status or similar - self._update_to_failed( - command_id=command_id, - failed_at=action.failed_at, - error_occurrence=None, - error_recovery_type=None, - notes=None, - ) - else: - assert_never(action.type) - elif prev_entry.command.intent == CommandIntent.FIXIT: - other_command_ids_to_fail = list( - # Copy to avoid it mutating as we remove elements below. - self._state.command_history.get_fixit_queue_ids() - ) - for command_id in other_command_ids_to_fail: - # TODO(mc, 2022-06-06): add new "cancelled" status or similar - self._update_to_failed( - command_id=command_id, - failed_at=action.failed_at, - error_occurrence=None, - error_recovery_type=None, - notes=None, - ) - else: - assert_never(prev_entry.command.intent) + case QueueStatus.PAUSED: + self._state.queue_status = QueueStatus.RUNNING + case QueueStatus.RUNNING | QueueStatus.AWAITING_RECOVERY: + # Nothing for the play action to do. No-op. + pass - elif isinstance(action, PlayAction): - if not self._state.run_result: - self._state.run_started_at = ( - self._state.run_started_at or action.requested_at - ) - match self._state.queue_status: - case QueueStatus.SETUP: - self._state.queue_status = ( - QueueStatus.PAUSED - if self._state.is_door_blocking - else QueueStatus.RUNNING - ) - case QueueStatus.AWAITING_RECOVERY_PAUSED: - self._state.queue_status = QueueStatus.AWAITING_RECOVERY - case QueueStatus.PAUSED: - self._state.queue_status = QueueStatus.RUNNING - case QueueStatus.RUNNING | QueueStatus.AWAITING_RECOVERY: - # Nothing for the play action to do. No-op. - pass + def _handle_pause_action(self, action: PauseAction) -> None: + self._state.queue_status = QueueStatus.PAUSED - elif isinstance(action, PauseAction): - self._state.queue_status = QueueStatus.PAUSED + def _handle_resume_from_recovery_action( + self, action: ResumeFromRecoveryAction + ) -> None: + self._state.queue_status = QueueStatus.RUNNING + self._state.recovery_target_command_id = None - elif isinstance(action, ResumeFromRecoveryAction): - self._state.queue_status = QueueStatus.RUNNING + def _handle_stop_action(self, action: StopAction) -> None: + if not self._state.run_result: self._state.recovery_target_command_id = None - elif isinstance(action, StopAction): - if not self._state.run_result: - self._state.recovery_target_command_id = None - - self._state.queue_status = QueueStatus.PAUSED - if action.from_estop: - self._state.stopped_by_estop = True - self._state.run_result = RunResult.FAILED - else: - self._state.run_result = RunResult.STOPPED - - elif isinstance(action, FinishAction): - if not self._state.run_result: - self._state.queue_status = QueueStatus.PAUSED - if action.set_run_status: - self._state.run_result = ( - RunResult.SUCCEEDED - if not action.error_details - else RunResult.FAILED - ) - else: - self._state.run_result = RunResult.STOPPED - - if not self._state.run_error and action.error_details: - self._state.run_error = self._map_run_exception_to_error_occurrence( - action.error_details.error_id, - action.error_details.created_at, - action.error_details.error, - ) + self._state.queue_status = QueueStatus.PAUSED + if action.from_estop: + self._state.stopped_by_estop = True + self._state.run_result = RunResult.FAILED else: - # HACK(sf): There needs to be a better way to set - # an estop error than this else clause - if self._state.stopped_by_estop and action.error_details: - self._state.run_error = self._map_run_exception_to_error_occurrence( - action.error_details.error_id, - action.error_details.created_at, - action.error_details.error, - ) + self._state.run_result = RunResult.STOPPED - elif isinstance(action, HardwareStoppedAction): + def _handle_finish_action(self, action: FinishAction) -> None: + if not self._state.run_result: self._state.queue_status = QueueStatus.PAUSED - self._state.run_result = self._state.run_result or RunResult.STOPPED - self._state.run_completed_at = ( - self._state.run_completed_at or action.completed_at - ) + if action.set_run_status: + self._state.run_result = ( + RunResult.SUCCEEDED + if not action.error_details + else RunResult.FAILED + ) + else: + self._state.run_result = RunResult.STOPPED - if action.finish_error_details: - self._state.finish_error = ( - self._map_finish_exception_to_error_occurrence( - action.finish_error_details.error_id, - action.finish_error_details.created_at, - action.finish_error_details.error, - ) + if not self._state.run_error and action.error_details: + self._state.run_error = self._map_run_exception_to_error_occurrence( + action.error_details.error_id, + action.error_details.created_at, + action.error_details.error, + ) + else: + # HACK(sf): There needs to be a better way to set + # an estop error than this else clause + if self._state.stopped_by_estop and action.error_details: + self._state.run_error = self._map_run_exception_to_error_occurrence( + action.error_details.error_id, + action.error_details.created_at, + action.error_details.error, ) - elif isinstance(action, DoorChangeAction): - if self._config.block_on_door_open: - if action.door_state == DoorState.OPEN: - self._state.is_door_blocking = True - match self._state.queue_status: - case QueueStatus.SETUP: - pass - case QueueStatus.RUNNING | QueueStatus.PAUSED: - self._state.queue_status = QueueStatus.PAUSED - case QueueStatus.AWAITING_RECOVERY | QueueStatus.AWAITING_RECOVERY_PAUSED: - self._state.queue_status = ( - QueueStatus.AWAITING_RECOVERY_PAUSED - ) - elif action.door_state == DoorState.CLOSED: - self._state.is_door_blocking = False + def _handle_hardware_stopped_action(self, action: HardwareStoppedAction) -> None: + self._state.queue_status = QueueStatus.PAUSED + self._state.run_result = self._state.run_result or RunResult.STOPPED + self._state.run_completed_at = ( + self._state.run_completed_at or action.completed_at + ) + + if action.finish_error_details: + self._state.finish_error = self._map_finish_exception_to_error_occurrence( + action.finish_error_details.error_id, + action.finish_error_details.created_at, + action.finish_error_details.error, + ) + + def _handle_door_change_action(self, action: DoorChangeAction) -> None: + if self._config.block_on_door_open: + if action.door_state == DoorState.OPEN: + self._state.is_door_blocking = True + match self._state.queue_status: + case QueueStatus.SETUP: + pass + case QueueStatus.RUNNING | QueueStatus.PAUSED: + self._state.queue_status = QueueStatus.PAUSED + case QueueStatus.AWAITING_RECOVERY | QueueStatus.AWAITING_RECOVERY_PAUSED: + self._state.queue_status = QueueStatus.AWAITING_RECOVERY_PAUSED + elif action.door_state == DoorState.CLOSED: + self._state.is_door_blocking = False def _update_to_failed( self, From 140729b203d11e4693ec6e6177c7e5085183aa43 Mon Sep 17 00:00:00 2001 From: Jeremy Leon Date: Mon, 15 Jul 2024 09:34:59 -0400 Subject: [PATCH 13/78] feat(api): more robust CSV parsing and usage in run context for CSV parameters (#15506) adds contents and more robust CSV file parsing to in-protocol csv parameter usage --- .../parameters/csv_parameter_definition.py | 5 - .../opentrons/protocols/parameters/types.py | 56 +++++++--- .../test_csv_parameter_definition.py | 16 +-- .../test_csv_parameter_interface.py | 101 ++++++++++++++++++ 4 files changed, 145 insertions(+), 33 deletions(-) create mode 100644 api/tests/opentrons/protocols/parameters/test_csv_parameter_interface.py diff --git a/api/src/opentrons/protocols/parameters/csv_parameter_definition.py b/api/src/opentrons/protocols/parameters/csv_parameter_definition.py index 6806e03b2d7..35e0d4f2345 100644 --- a/api/src/opentrons/protocols/parameters/csv_parameter_definition.py +++ b/api/src/opentrons/protocols/parameters/csv_parameter_definition.py @@ -8,7 +8,6 @@ ) from . import validation -from .exceptions import ParameterDefinitionError from .parameter_definition import AbstractParameterDefinition from .types import CSVParameter @@ -44,10 +43,6 @@ def value(self) -> Optional[TextIO]: @value.setter def value(self, new_file: TextIO) -> None: - if not new_file.name.endswith(".csv"): - raise ParameterDefinitionError( - f"CSV parameter {self._variable_name} was given non csv file {new_file.name}" - ) self._value = new_file @property diff --git a/api/src/opentrons/protocols/parameters/types.py b/api/src/opentrons/protocols/parameters/types.py index f61a35457ad..46b47a04282 100644 --- a/api/src/opentrons/protocols/parameters/types.py +++ b/api/src/opentrons/protocols/parameters/types.py @@ -1,32 +1,62 @@ import csv -from typing import TypeVar, Union, TypedDict, TextIO, Optional, List +from typing import TypeVar, Union, TypedDict, TextIO, Optional, List, Any -from .exceptions import RuntimeParameterRequired +from .exceptions import RuntimeParameterRequired, ParameterValueError class CSVParameter: def __init__(self, csv_file: Optional[TextIO]) -> None: self._file = csv_file - self._rows = [] - if self._file is not None: - for row in csv.reader(self._file): - self._rows.append(row) - self._file.seek(0) + self._contents: Optional[str] = None @property def file(self) -> TextIO: + """Returns the file handler for the CSV file.""" if self._file is None: raise RuntimeParameterRequired( "CSV parameter needs to be set to a file for full analysis or run." ) return self._file - def rows(self) -> List[List[str]]: - if self._file is None: - raise RuntimeParameterRequired( - "CSV parameter needs to be set to a file for full analysis or run." - ) - return self._rows + @property + def contents(self) -> str: + """Returns the full contents of the CSV file as a single string.""" + if self._contents is None: + self.file.seek(0) + self._contents = self.file.read() + return self._contents + + def parse_as_csv( + self, detect_dialect: bool = True, **kwargs: Any + ) -> List[List[str]]: + """Returns a list of rows with each row represented as a list of column elements. + + If there is a header for the CSV that will be the first row in the list (i.e. `.rows()[0]`). + All elements will be represented as strings, even if they are numeric in nature. + """ + rows: List[List[str]] = [] + if detect_dialect: + try: + self.file.seek(0) + dialect = csv.Sniffer().sniff(self.file.read(1024)) + self.file.seek(0) + reader = csv.reader(self.file, dialect, **kwargs) + except (UnicodeDecodeError, csv.Error): + raise ParameterValueError( + "Cannot parse dialect or contents from provided CSV file." + ) + else: + try: + reader = csv.reader(self.file, **kwargs) + except (UnicodeDecodeError, csv.Error): + raise ParameterValueError("Cannot parse provided CSV file.") + try: + for row in reader: + rows.append(row) + except (UnicodeDecodeError, csv.Error): + raise ParameterValueError("Cannot parse provided CSV file.") + self.file.seek(0) + return rows PrimitiveAllowedTypes = Union[str, int, float, bool] diff --git a/api/tests/opentrons/protocols/parameters/test_csv_parameter_definition.py b/api/tests/opentrons/protocols/parameters/test_csv_parameter_definition.py index 9344da7daa1..70933cc326c 100644 --- a/api/tests/opentrons/protocols/parameters/test_csv_parameter_definition.py +++ b/api/tests/opentrons/protocols/parameters/test_csv_parameter_definition.py @@ -12,10 +12,7 @@ create_csv_parameter, CSVParameterDefinition, ) -from opentrons.protocols.parameters.exceptions import ( - ParameterDefinitionError, - RuntimeParameterRequired, -) +from opentrons.protocols.parameters.exceptions import RuntimeParameterRequired @pytest.fixture(autouse=True) @@ -66,17 +63,6 @@ def test_set_csv_value( assert csv_parameter_subject.value is mock_file -def test_set_csv_value_raises( - decoy: Decoy, csv_parameter_subject: CSVParameterDefinition -) -> None: - """It should raise if the file set to does not end in '.csv'.""" - mock_file = decoy.mock(cls=TextIOWrapper) - decoy.when(mock_file.name).then_return("mock.txt") - - with pytest.raises(ParameterDefinitionError): - csv_parameter_subject.value = mock_file - - def test_csv_parameter_as_protocol_engine_type( csv_parameter_subject: CSVParameterDefinition, ) -> None: diff --git a/api/tests/opentrons/protocols/parameters/test_csv_parameter_interface.py b/api/tests/opentrons/protocols/parameters/test_csv_parameter_interface.py new file mode 100644 index 00000000000..be46b61845d --- /dev/null +++ b/api/tests/opentrons/protocols/parameters/test_csv_parameter_interface.py @@ -0,0 +1,101 @@ +import pytest +from pytest_lazyfixture import lazy_fixture # type: ignore[import-untyped] + +import tempfile +from typing import TextIO + +from opentrons.protocols.parameters.types import CSVParameter + + +@pytest.fixture +def csv_file_basic() -> TextIO: + temp_file = tempfile.TemporaryFile("r+") + contents = '"x","y","z"\n"a",1,2\n"b",3,4\n"c",5,6' + temp_file.write(contents) + temp_file.seek(0) + return temp_file + + +@pytest.fixture +def csv_file_no_quotes() -> TextIO: + temp_file = tempfile.TemporaryFile("r+") + contents = "x,y,z\na,1,2\nb,3,4\nc,5,6" + temp_file.write(contents) + temp_file.seek(0) + return temp_file + + +@pytest.fixture +def csv_file_preceding_spaces() -> TextIO: + temp_file = tempfile.TemporaryFile("r+") + contents = '"x", "y", "z"\n"a", 1, 2\n"b", 3, 4\n"c", 5, 6' + temp_file.write(contents) + temp_file.seek(0) + return temp_file + + +@pytest.fixture +def csv_file_mixed_quotes() -> TextIO: + temp_file = tempfile.TemporaryFile("r+") + contents = 'head,er\n"a,b,c",def\n"""ghi""","jkl"' + temp_file.write(contents) + temp_file.seek(0) + return temp_file + + +@pytest.fixture +def csv_file_different_delimiter() -> TextIO: + temp_file = tempfile.TemporaryFile("r+") + contents = "x:y:z\na,:1,:2\nb,:3,:4\nc,:5,:6" + temp_file.write(contents) + temp_file.seek(0) + return temp_file + + +def test_csv_parameter(csv_file_basic: TextIO) -> None: + """It should load the CSV parameter and provide access to the file, contents, and rows.""" + subject = CSVParameter(csv_file_basic) + assert subject.file is csv_file_basic + assert subject.contents == '"x","y","z"\n"a",1,2\n"b",3,4\n"c",5,6' + + +@pytest.mark.parametrize( + "csv_file", + [ + lazy_fixture("csv_file_basic"), + lazy_fixture("csv_file_no_quotes"), + lazy_fixture("csv_file_preceding_spaces"), + ], +) +def test_csv_parameter_rows(csv_file: TextIO) -> None: + """It should load the rows as all strings even with no quotes or leading spaces.""" + subject = CSVParameter(csv_file) + assert len(subject.parse_as_csv()) == 4 + assert subject.parse_as_csv()[0] == ["x", "y", "z"] + assert subject.parse_as_csv()[1] == ["a", "1", "2"] + + +def test_csv_parameter_mixed_quotes(csv_file_mixed_quotes: TextIO) -> None: + """It should load the rows with no quotes, quotes and escaped quotes with double quotes.""" + subject = CSVParameter(csv_file_mixed_quotes) + assert len(subject.parse_as_csv()) == 3 + assert subject.parse_as_csv()[0] == ["head", "er"] + assert subject.parse_as_csv()[1] == ["a,b,c", "def"] + assert subject.parse_as_csv()[2] == ['"ghi"', "jkl"] + + +def test_csv_parameter_additional_kwargs(csv_file_different_delimiter: TextIO) -> None: + """It should load the rows with a different delimiter.""" + subject = CSVParameter(csv_file_different_delimiter) + rows = subject.parse_as_csv(delimiter=":") + assert len(rows) == 4 + assert rows[0] == ["x", "y", "z"] + assert rows[1] == ["a,", "1,", "2"] + + +def test_csv_parameter_dont_detect_dialect(csv_file_preceding_spaces: TextIO) -> None: + """It should load the rows without trying to detect the dialect.""" + subject = CSVParameter(csv_file_preceding_spaces) + rows = subject.parse_as_csv(detect_dialect=False) + assert rows[0] == ["x", ' "y"', ' "z"'] + assert rows[1] == ["a", " 1", " 2"] From 8b88dda01afa364464de3f1506763b1ec08adc6d Mon Sep 17 00:00:00 2001 From: koji Date: Mon, 15 Jul 2024 11:05:11 -0400 Subject: [PATCH 14/78] * fix(components, app): update useSwipe to improve swipe action detection --- app/src/pages/RunningProtocol/index.tsx | 15 +++++---- components/package.json | 2 +- components/src/hooks/useSwipe.ts | 44 ++++++++++++++++++------- yarn.lock | 4 +-- 4 files changed, 43 insertions(+), 22 deletions(-) diff --git a/app/src/pages/RunningProtocol/index.tsx b/app/src/pages/RunningProtocol/index.tsx index 5c2377f473e..b1e1426587f 100644 --- a/app/src/pages/RunningProtocol/index.tsx +++ b/app/src/pages/RunningProtocol/index.tsx @@ -93,7 +93,7 @@ export function RunningProtocol(): JSX.Element { setInterventionModalCommandKey, ] = React.useState(null) const lastAnimatedCommand = React.useRef(null) - const swipe = useSwipe() + const { ref, style, swipeType, setSwipeType } = useSwipe() const robotSideAnalysis = useMostRecentCompletedAnalysis(runId) const lastRunCommand = useLastRunCommand(runId, { refetchInterval: LIVE_RUN_COMMANDS_POLL_MS, @@ -126,20 +126,20 @@ export function RunningProtocol(): JSX.Element { React.useEffect(() => { if ( currentOption === 'CurrentRunningProtocolCommand' && - swipe.swipeType === 'swipe-left' + swipeType === 'swipe-left' ) { setCurrentOption('RunningProtocolCommandList') - swipe.setSwipeType('') + setSwipeType('') } if ( currentOption === 'RunningProtocolCommandList' && - swipe.swipeType === 'swipe-right' + swipeType === 'swipe-right' ) { setCurrentOption('CurrentRunningProtocolCommand') - swipe.setSwipeType('') + setSwipeType('') } - }, [currentOption, swipe, swipe.setSwipeType]) + }, [currentOption, swipeType, setSwipeType]) React.useEffect(() => { if ( @@ -209,7 +209,8 @@ export function RunningProtocol(): JSX.Element { /> ) : null} diff --git a/components/package.json b/components/package.json index c0090f1503d..b6d71152b6a 100644 --- a/components/package.json +++ b/components/package.json @@ -33,7 +33,7 @@ "@types/styled-components": "^5.1.26", "@types/webpack-env": "^1.16.0", "classnames": "2.2.5", - "interactjs": "^1.10.17", + "interactjs": "^1.10.27", "lodash": "4.17.21", "react-i18next": "13.5.0", "react-popper": "1.0.0", diff --git a/components/src/hooks/useSwipe.ts b/components/src/hooks/useSwipe.ts index dbf0d6823ad..b9b996206a7 100644 --- a/components/src/hooks/useSwipe.ts +++ b/components/src/hooks/useSwipe.ts @@ -16,24 +16,44 @@ export const useSwipe = (): UseSwipeResult => { const [swipeType, setSwipeType] = useState('') const [isEnabled, setIsEnabled] = useState(true) const interactiveRef = useRef(null) - const str = 'swipe' - const swipeDirs = ['up', 'down', 'left', 'right'] + const THRESHOLD = 50 + let startX = 0 + let startY = 0 const enable = (): void => { - if (interactiveRef?.current != null) { - interact((interactiveRef.current as unknown) as HTMLElement) - .draggable(true) - .on('dragend', event => { - if (!event.swipe) return + if (interactiveRef.current != null) { + interact(interactiveRef.current).draggable({ + inertia: false, + modifiers: [], + autoScroll: false, + listeners: { + start(event) { + startX = event.clientX + startY = event.clientY + }, + // Note (kk:07/11/2024) want to keep this for debugging + // move(event) { + // console.log('Drag move:', event.clientX, event.clientY) + // }, + end(event) { + const dx = event.clientX - startX + const dy = event.clientY - startY + const absX = Math.abs(dx) + const absY = Math.abs(dy) - swipeDirs.forEach(dir => { - if (event.swipe[dir] != null) setSwipeType(`${str}-${dir}`) - }) - }) + if (absX > absY && absX > THRESHOLD) { + setSwipeType(dx > 0 ? 'swipe-right' : 'swipe-left') + } else if (absY > absX && absY > THRESHOLD) { + setSwipeType(dy > 0 ? 'swipe-down' : 'swipe-up') + } + }, + }, + }) } } + const disable = (): void => { - if (interactiveRef?.current != null) { + if (interactiveRef.current != null) { interact((interactiveRef.current as unknown) as HTMLElement).unset() } } diff --git a/yarn.lock b/yarn.lock index 7d15ddae134..48295e5e53e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3295,7 +3295,7 @@ "@types/styled-components" "^5.1.26" "@types/webpack-env" "^1.16.0" classnames "2.2.5" - interactjs "^1.10.17" + interactjs "^1.10.27" lodash "4.17.21" react-i18next "13.5.0" react-popper "1.0.0" @@ -13325,7 +13325,7 @@ inline-style-parser@0.2.3: resolved "https://registry.yarnpkg.com/inline-style-parser/-/inline-style-parser-0.2.3.tgz#e35c5fb45f3a83ed7849fe487336eb7efa25971c" integrity sha512-qlD8YNDqyTKTyuITrDOffsl6Tdhv+UC4hcdAVuQsK4IMQ99nSgd1MIA/Q+jQYoh9r3hVUXhYh7urSRmXPkW04g== -interactjs@^1.10.17: +interactjs@^1.10.27: version "1.10.27" resolved "https://registry.yarnpkg.com/interactjs/-/interactjs-1.10.27.tgz#16499aba4987a5ccfdaddca7d1ba7bb1118e14d0" integrity sha512-y/8RcCftGAF24gSp76X2JS3XpHiUvDQyhF8i7ujemBz77hwiHDuJzftHx7thY8cxGogwGiPJ+o97kWB6eAXnsA== From 922aefa43482251048d6f15e2d931251d7fd36f7 Mon Sep 17 00:00:00 2001 From: Caila Marashaj <98041399+caila-marashaj@users.noreply.github.com> Date: Mon, 15 Jul 2024 11:25:01 -0400 Subject: [PATCH 15/78] fix(hardware-testing): fix call to capacitive probe (#15645) --- .../production_qc/ninety_six_assembly_qc_ot3/test_capacitance.py | 1 - 1 file changed, 1 deletion(-) diff --git a/hardware-testing/hardware_testing/production_qc/ninety_six_assembly_qc_ot3/test_capacitance.py b/hardware-testing/hardware_testing/production_qc/ninety_six_assembly_qc_ot3/test_capacitance.py index 3a3b7cc128b..f3146d54f74 100644 --- a/hardware-testing/hardware_testing/production_qc/ninety_six_assembly_qc_ot3/test_capacitance.py +++ b/hardware-testing/hardware_testing/production_qc/ninety_six_assembly_qc_ot3/test_capacitance.py @@ -173,7 +173,6 @@ async def _probe(distance: float, speed: float) -> float: NodeId.pipette_left, NodeId.head_l, distance=distance, - plunger_speed=speed, mount_speed=speed, sensor_id=sensor_id, relative_threshold_pf=default_probe_cfg.sensor_threshold_pf, From 77223c6de86a388d94ec19fd9cbb6ae1cacd93f7 Mon Sep 17 00:00:00 2001 From: Seth Foster Date: Mon, 15 Jul 2024 11:28:58 -0400 Subject: [PATCH 16/78] fix(app): odd: keep bullets on runningprotocol low (#15649) These little bullets that establish the ability to swipe left and right need to be relatively above the rest of the runningprotocol screen yes but they shouldn't be above anything else. So create an explicit z-stacking context for the content part of running protocol. I'm not quite sure why this is now necessary and wasn't previously, but the rules around what creates an implicit stacking context are complex so we probably changed something 8 components away that removed whatever stacking context existed previously. ## Testing On ODD: - Note that the bullets look correct on running protocol - Note that the bullets are not displayed on top of error recovery anymore - Note that the other running protocol modals (or "modals", since most of them don't actually use the modal root) still display correctly. This is the cancel-confirmation modal and the intervention modal. --- app/src/pages/RunningProtocol/index.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/src/pages/RunningProtocol/index.tsx b/app/src/pages/RunningProtocol/index.tsx index b1e1426587f..8b6c2fa225d 100644 --- a/app/src/pages/RunningProtocol/index.tsx +++ b/app/src/pages/RunningProtocol/index.tsx @@ -173,10 +173,15 @@ export function RunningProtocol(): JSX.Element { ) : null} {runStatus === RUN_STATUS_STOP_REQUESTED ? : null} + {/* note: this zindex is here to establish a zindex context for the bullets + so they're relatively-above this flex but not anything else like error + recovery + */} {robotSideAnalysis != null ? ( Date: Mon, 15 Jul 2024 11:48:25 -0400 Subject: [PATCH 17/78] feat(shared-data): bump gripper model version and use new force polynomial table for new motor (#15413) # Overview The manufacturer is changing the brushes on the gripper motor, so we'll need to use a new force polynomial for the new gripper models (v1.3). In order to use the new force function, you will need to make sure the serial number is in the format of `GRPV13xxxxx`. --- api/src/opentrons/config/gripper_config.py | 7 ++++- .../gripper/definitions/1/gripperV1.3.json | 29 +++++++++++++++++++ shared-data/js/constants.ts | 8 ++++- shared-data/js/gripper.ts | 10 ++++++- shared-data/js/types.ts | 2 ++ .../gripper/gripper_definition.py | 2 ++ 6 files changed, 55 insertions(+), 3 deletions(-) create mode 100644 shared-data/gripper/definitions/1/gripperV1.3.json diff --git a/api/src/opentrons/config/gripper_config.py b/api/src/opentrons/config/gripper_config.py index 0c364bc749c..1de9be1de0b 100644 --- a/api/src/opentrons/config/gripper_config.py +++ b/api/src/opentrons/config/gripper_config.py @@ -24,7 +24,12 @@ def info_num_to_model(num: str) -> GripperModel: # PVT will now be 1.2 model_map = { "0": {"0": GripperModel.v1, "1": GripperModel.v1}, - "1": {"0": GripperModel.v1, "1": GripperModel.v1_1, "2": GripperModel.v1_2}, + "1": { + "0": GripperModel.v1, + "1": GripperModel.v1_1, + "2": GripperModel.v1_2, + "3": GripperModel.v1_3, + }, } return model_map[major_model][minor_model] diff --git a/shared-data/gripper/definitions/1/gripperV1.3.json b/shared-data/gripper/definitions/1/gripperV1.3.json new file mode 100644 index 00000000000..ef26cc062ba --- /dev/null +++ b/shared-data/gripper/definitions/1/gripperV1.3.json @@ -0,0 +1,29 @@ +{ + "$otSharedSchema": "gripper/schemas/1", + "model": "gripperV1.3", + "schemaVersion": 1, + "displayName": "Flex Gripper", + "gripForceProfile": { + "polynomial": [ + [0, 3.759869], + [1, 0.81005], + [2, 0.04597701] + ], + "defaultGripForce": 15.0, + "defaultIdleForce": 10.0, + "defaultHomeForce": 12.0, + "min": 2.0, + "max": 30.0 + }, + "geometry": { + "baseOffsetFromMount": [19.5, -74.325, -94.825], + "jawCenterOffsetFromBase": [0.0, 0.0, -86.475], + "pinOneOffsetFromBase": [6.0, -54.0, -98.475], + "pinTwoOffsetFromBase": [6.0, 54.0, -98.475], + "jawWidth": { + "min": 60.0, + "max": 92.0 + }, + "maxAllowedGripError": 3.0 + } +} diff --git a/shared-data/js/constants.ts b/shared-data/js/constants.ts index d1ce19a8a56..79241e1f508 100644 --- a/shared-data/js/constants.ts +++ b/shared-data/js/constants.ts @@ -48,7 +48,13 @@ export const MAGNETIC_BLOCK_V1: 'magneticBlockV1' = 'magneticBlockV1' export const GRIPPER_V1: 'gripperV1' = 'gripperV1' export const GRIPPER_V1_1: 'gripperV1.1' = 'gripperV1.1' export const GRIPPER_V1_2: 'gripperV1.2' = 'gripperV1.2' -export const GRIPPER_MODELS = [GRIPPER_V1, GRIPPER_V1_1, GRIPPER_V1_2] +export const GRIPPER_V1_3: 'gripperV1.3' = 'gripperV1.3' +export const GRIPPER_MODELS = [ + GRIPPER_V1, + GRIPPER_V1_1, + GRIPPER_V1_2, + GRIPPER_V1_3, +] // robot display name export const OT2_DISPLAY_NAME: 'Opentrons OT-2' = 'Opentrons OT-2' diff --git a/shared-data/js/gripper.ts b/shared-data/js/gripper.ts index 15c1d3f7f7b..9bc8282421e 100644 --- a/shared-data/js/gripper.ts +++ b/shared-data/js/gripper.ts @@ -1,8 +1,14 @@ import gripperV1 from '../gripper/definitions/1/gripperV1.json' import gripperV1_1 from '../gripper/definitions/1/gripperV1.1.json' import gripperV1_2 from '../gripper/definitions/1/gripperV1.2.json' +import gripperV1_3 from '../gripper/definitions/1/gripperV1.3.json' -import { GRIPPER_V1, GRIPPER_V1_1, GRIPPER_V1_2 } from './constants' +import { + GRIPPER_V1, + GRIPPER_V1_1, + GRIPPER_V1_2, + GRIPPER_V1_3, +} from './constants' import type { GripperModel, GripperDefinition } from './types' @@ -16,6 +22,8 @@ export const getGripperDef = ( return gripperV1_1 as GripperDefinition case GRIPPER_V1_2: return gripperV1_2 as GripperDefinition + case GRIPPER_V1_3: + return gripperV1_3 as GripperDefinition default: console.warn( `Could not find a gripper with model ${gripperModel}, falling back to most recent definition: ${GRIPPER_V1_2}` diff --git a/shared-data/js/types.ts b/shared-data/js/types.ts index 6fed46b5b74..a744d8b645e 100644 --- a/shared-data/js/types.ts +++ b/shared-data/js/types.ts @@ -24,6 +24,7 @@ import type { GRIPPER_V1, GRIPPER_V1_1, GRIPPER_V1_2, + GRIPPER_V1_3, EXTENSION, MAGNETIC_BLOCK_V1, } from './constants' @@ -238,6 +239,7 @@ export type GripperModel = | typeof GRIPPER_V1 | typeof GRIPPER_V1_1 | typeof GRIPPER_V1_2 + | typeof GRIPPER_V1_3 export type ModuleModelWithLegacy = | ModuleModel diff --git a/shared-data/python/opentrons_shared_data/gripper/gripper_definition.py b/shared-data/python/opentrons_shared_data/gripper/gripper_definition.py index 4c4c30c623b..707d960a9ba 100644 --- a/shared-data/python/opentrons_shared_data/gripper/gripper_definition.py +++ b/shared-data/python/opentrons_shared_data/gripper/gripper_definition.py @@ -24,6 +24,7 @@ class GripperModel(str, Enum): v1 = "gripperV1" v1_1 = "gripperV1.1" v1_2 = "gripperV1.2" + v1_3 = "gripperV1.3" def __str__(self) -> str: """Model name.""" @@ -31,6 +32,7 @@ def __str__(self) -> str: self.__class__.v1: "gripperV1", self.__class__.v1_1: "gripperV1.1", self.__class__.v1_2: "gripperV1.2", + self.__class__.v1_3: "gripperV1.3", } return enum_to_str[self] From 187e079f643a2c8f73e0a40db5ebd03e88362f7b Mon Sep 17 00:00:00 2001 From: Rhyann Clarke <146747548+rclarke0@users.noreply.github.com> Date: Mon, 15 Jul 2024 12:56:01 -0400 Subject: [PATCH 18/78] Add AVG historical temp and RH conditions to error ticket (#15655) # Overview Add AVG historical temp and RH conditions to error ticket # Test Plan - Connects to ABR sheet - Connects to ABR Ambient conditions sheet - Filters out data between time stamps and robot and same protocol - Adds a string that compares found averages to the ticket description. # Changelog # Review requests # Risk assessment --- .../data_collection/abr_google_drive.py | 4 +- .../data_collection/abr_robot_error.py | 118 +++++++++++++++++- 2 files changed, 117 insertions(+), 5 deletions(-) diff --git a/abr-testing/abr_testing/data_collection/abr_google_drive.py b/abr-testing/abr_testing/data_collection/abr_google_drive.py index 1827d79cec0..31eba721503 100644 --- a/abr-testing/abr_testing/data_collection/abr_google_drive.py +++ b/abr-testing/abr_testing/data_collection/abr_google_drive.py @@ -76,13 +76,13 @@ def create_data_dictionary( start_time = datetime.strptime( file_results.get("startedAt", ""), "%Y-%m-%dT%H:%M:%S.%f%z" ) - adjusted_start_time = start_time - timedelta(hours=5) + adjusted_start_time = start_time - timedelta(hours=4) start_date = str(adjusted_start_time.date()) start_time_str = str(adjusted_start_time).split("+")[0] complete_time = datetime.strptime( file_results.get("completedAt", ""), "%Y-%m-%dT%H:%M:%S.%f%z" ) - adjusted_complete_time = complete_time - timedelta(hours=5) + adjusted_complete_time = complete_time - timedelta(hours=4) complete_time_str = str(adjusted_complete_time).split("+")[0] run_time = complete_time - start_time run_time_min = run_time.total_seconds() / 60 diff --git a/abr-testing/abr_testing/data_collection/abr_robot_error.py b/abr-testing/abr_testing/data_collection/abr_robot_error.py index 8ca606004f5..2bf9abbd1a1 100644 --- a/abr-testing/abr_testing/data_collection/abr_robot_error.py +++ b/abr-testing/abr_testing/data_collection/abr_robot_error.py @@ -7,10 +7,97 @@ import shutil import os import subprocess +from datetime import datetime, timedelta import sys import json import re import pandas as pd +from statistics import mean, StatisticsError + + +def compare_current_trh_to_average( + robot: str, + start_time: Any, + end_time: Any, + protocol_name: str, + storage_directory: str, +) -> str: + """Get average temp/rh for errored run and compare to average.""" + # Connect to ABR ambient conditions sheet + credentials_path = os.path.join(storage_directory, "credentials.json") + temprh_data_sheet = google_sheets_tool.google_sheet( + credentials_path, "ABR Ambient Conditions", 0 + ) + headers = temprh_data_sheet.get_row(1) + all_trh_data = temprh_data_sheet.get_all_data(expected_headers=headers) + # Connect to ABR-run-data sheet + abr_data = google_sheets_tool.google_sheet(credentials_path, "ABR-run-data", 0) + headers = abr_data.get_row(1) + all_run_data = abr_data.get_all_data(expected_headers=headers) + # Find average conditions of errored time period + df_all_trh = pd.DataFrame(all_trh_data) + # Convert timestamps to datetime objects + df_all_trh["Timestamp"] = pd.to_datetime( + df_all_trh["Timestamp"], format="mixed" + ).dt.tz_localize(None) + # Ensure start_time is timezone-naive + start_time = start_time.replace(tzinfo=None) + end_time = end_time.replace(tzinfo=None) + relevant_temp_rhs = df_all_trh[ + (df_all_trh["Robot"] == robot) + & (df_all_trh["Timestamp"] >= start_time) + & (df_all_trh["Timestamp"] <= end_time) + ] + try: + avg_temp = round(mean(relevant_temp_rhs["Temp (oC)"]), 2) + avg_rh = round(mean(relevant_temp_rhs["Relative Humidity (%)"]), 2) + except StatisticsError: + avg_temp = None + avg_rh = None + # Get AVG t/rh of runs w/ same robot & protocol newer than 3 wks old with no errors + weeks_ago_3 = start_time - timedelta(weeks=3) + df_all_run_data = pd.DataFrame(all_run_data) + df_all_run_data["Start_Time"] = pd.to_datetime( + df_all_run_data["Start_Time"], format="mixed" + ).dt.tz_localize(None) + df_all_run_data["Errors"] = pd.to_numeric(df_all_run_data["Errors"]) + df_all_run_data["Average Temp (oC)"] = pd.to_numeric( + df_all_run_data["Average Temp (oC)"] + ) + common_filters = ( + (df_all_run_data["Robot"] == robot) + & (df_all_run_data["Start_Time"] >= weeks_ago_3) + & (df_all_run_data["Start_Time"] <= start_time) + & (df_all_run_data["Errors"] < 1) + & (df_all_run_data["Average Temp (oC)"] > 1) + ) + + if protocol_name == "": + relevant_run_data = df_all_run_data[common_filters] + else: + relevant_run_data = df_all_run_data[ + common_filters & (df_all_run_data["Protocol_Name"] == protocol_name) + ] + # Calculate means of historical data + try: + historical_avg_temp = round( + mean(relevant_run_data["Average Temp (oC)"].astype(float)), 2 + ) + historical_avg_rh = round( + mean(relevant_run_data["Average RH(%)"].astype(float)), 2 + ) + except StatisticsError: + historical_avg_temp = None + historical_avg_rh = None + # Formats TEMP/RH message for ticket. + temp_rh_message = ( + f"{len(relevant_run_data)} runs with temp/rh data for {robot} running {protocol_name}." + f" AVG TEMP (deg C): {historical_avg_temp}. AVG RH (%): {historical_avg_rh}." + f" AVG TEMP of ERROR: {avg_temp}. AVG RH of ERROR: {avg_rh}." + ) + # Print out comparison string. + print(temp_rh_message) + return temp_rh_message def compare_lpc_to_historical_data( @@ -42,9 +129,9 @@ def compare_lpc_to_historical_data( current_x = round(labware_dict["X"], 2) current_y = round(labware_dict["Y"], 2) current_z = round(labware_dict["Z"], 2) - avg_x = round(sum(x_float) / len(x_float), 2) - avg_y = round(sum(y_float) / len(y_float), 2) - avg_z = round(sum(z_float) / len(z_float), 2) + avg_x = round(mean(x_float), 2) + avg_y = round(mean(y_float), 2) + avg_z = round(mean(z_float), 2) # Formats LPC message for ticket. lpc_message = ( @@ -195,6 +282,13 @@ def get_robot_state( components = ["Flex-RABR"] components = match_error_to_component("RABR", reported_string, components) print(components) + end_time = datetime.now() + start_time = end_time - timedelta(hours=2) + # Get current temp/rh compared to historical data + temp_rh_string = compare_current_trh_to_average( + parent, start_time, end_time, "", storage_directory + ) + description["Robot Temp and RH Comparison"] = temp_rh_string whole_description_str = ( "{" + "\n".join("{!r}: {!r},".format(k, v) for k, v in description.items()) @@ -242,6 +336,23 @@ def get_run_error_info_from_robot( description["protocol_name"] = results["protocol"]["metadata"].get( "protocolName", "" ) + # Get start and end time of run + start_time = datetime.strptime( + results.get("startedAt", ""), "%Y-%m-%dT%H:%M:%S.%f%z" + ) + adjusted_start_time = start_time - timedelta(hours=4) + complete_time = datetime.strptime( + results.get("completedAt", ""), "%Y-%m-%dT%H:%M:%S.%f%z" + ) + adjusted_complete_time = complete_time - timedelta(hours=4) + # Get average temp and rh of robot and protocol the error occurred on. + temp_rh_comparison = compare_current_trh_to_average( + parent, + adjusted_start_time, + adjusted_complete_time, + description["protocol_name"], + storage_directory, + ) # Get LPC coordinates of labware of failure lpc_dict = results["labwareOffsets"] labware_dict = results["labware"] @@ -280,6 +391,7 @@ def get_run_error_info_from_robot( if len(lpc_message) < 1: lpc_message = "No LPC coordinates found in relation to error." description["LPC Comparison"] = lpc_message + description["Robot Temp and RH Comparison"] = temp_rh_comparison all_modules = abr_google_drive.get_modules(results) whole_description = {**description, **all_modules} whole_description_str = ( From 80987bd4f2518bda7489bfe94409aa7d96dc9bb6 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 15 Jul 2024 12:59:02 -0400 Subject: [PATCH 19/78] fix(analyses-snapshot-testing): edge snapshot failure capture (#15646) This PR is an automated snapshot update request. Please review the changes and merge if they are acceptable or find your bug and fix it. Co-authored-by: y3rsh <502770+y3rsh@users.noreply.github.com> --- ...2][Flex_S_v2_16_P1000_96_TC_PartialTipPickupSingle].json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d8cb88b3b2][Flex_S_v2_16_P1000_96_TC_PartialTipPickupSingle].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d8cb88b3b2][Flex_S_v2_16_P1000_96_TC_PartialTipPickupSingle].json index f658c602a39..78da0891438 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d8cb88b3b2][Flex_S_v2_16_P1000_96_TC_PartialTipPickupSingle].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d8cb88b3b2][Flex_S_v2_16_P1000_96_TC_PartialTipPickupSingle].json @@ -3535,7 +3535,7 @@ "errors": [ { "createdAt": "TIMESTAMP", - "detail": "ValueError [line 16]: Nozzle layout configuration of style SINGLE is currently unsupported.", + "detail": "ValueError [line 16]: Nozzle layout configuration of style SINGLE is unsupported in API Versions lower than 2.20.", "errorCode": "4000", "errorInfo": {}, "errorType": "ExceptionInProtocolError", @@ -3544,10 +3544,10 @@ "wrappedErrors": [ { "createdAt": "TIMESTAMP", - "detail": "ValueError: Nozzle layout configuration of style SINGLE is currently unsupported.", + "detail": "ValueError: Nozzle layout configuration of style SINGLE is unsupported in API Versions lower than 2.20.", "errorCode": "4000", "errorInfo": { - "args": "('Nozzle layout configuration of style SINGLE is currently unsupported.',)", + "args": "('Nozzle layout configuration of style SINGLE is unsupported in API Versions lower than 2.20.',)", "class": "ValueError", "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line N, in exec_run\n exec(\"run(__context)\", new_globs)\n\n File \"\", line N, in \n\n File \"Flex_S_v2_16_P1000_96_TC_PartialTipPickupSingle.py\", line N, in run\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/api_support/util.py\", line N, in _check_version_wrapper\n return decorated_obj(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/instrument_context.py\", line N, in configure_nozzle_layout\n raise ValueError(\n" }, From 5262deb918a18153421b982f3dd49847d4715830 Mon Sep 17 00:00:00 2001 From: Nick Diehl <47604184+ncdiehl11@users.noreply.github.com> Date: Mon, 15 Jul 2024 13:33:11 -0400 Subject: [PATCH 20/78] fix(app): fix link for setting up new robot (#15656) RQA-2844 On desktop devices page, clicking 'See how to set up a new robot' opens a modal with a link to support for setting up a new robot. Instead of linking to the OT-2 setup support page, we will now link to the top level Opentrons support link. --- .../Devices/DevicesLanding/NewRobotSetupHelp.tsx | 3 +-- .../__tests__/NewRobotSetupHelp.test.tsx | 11 +++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/app/src/pages/Devices/DevicesLanding/NewRobotSetupHelp.tsx b/app/src/pages/Devices/DevicesLanding/NewRobotSetupHelp.tsx index dd02af14a64..e13f142bb71 100644 --- a/app/src/pages/Devices/DevicesLanding/NewRobotSetupHelp.tsx +++ b/app/src/pages/Devices/DevicesLanding/NewRobotSetupHelp.tsx @@ -16,8 +16,7 @@ import { getTopPortalEl } from '../../../App/portal' import { LegacyModal } from '../../../molecules/LegacyModal' import { ExternalLink } from '../../../atoms/Link/ExternalLink' -const NEW_ROBOT_SETUP_SUPPORT_ARTICLE_HREF = - 'https://support.opentrons.com/s/ot2-get-started' +const NEW_ROBOT_SETUP_SUPPORT_ARTICLE_HREF = 'https://support.opentrons.com/s/' export function NewRobotSetupHelp(): JSX.Element { const { t } = useTranslation(['devices_landing', 'shared']) diff --git a/app/src/pages/Devices/DevicesLanding/__tests__/NewRobotSetupHelp.test.tsx b/app/src/pages/Devices/DevicesLanding/__tests__/NewRobotSetupHelp.test.tsx index 51e4e994380..75933835202 100644 --- a/app/src/pages/Devices/DevicesLanding/__tests__/NewRobotSetupHelp.test.tsx +++ b/app/src/pages/Devices/DevicesLanding/__tests__/NewRobotSetupHelp.test.tsx @@ -43,4 +43,15 @@ describe('NewRobotSetupHelp', () => { expect(screen.queryByText('How to setup a new robot')).toBeFalsy() }) + + it('renders the link and it has the correct href attribute', () => { + render() + const link = screen.getByText('See how to set up a new robot') + fireEvent.click(link) + const targetLinkUrl = 'https://support.opentrons.com/s/' + const supportLink = screen.getByRole('link', { + name: 'Learn more about setting up a new robot', + }) + expect(supportLink).toHaveAttribute('href', targetLinkUrl) + }) }) From f1e6bb5afe5b46aa5fadbfce3ca9023f48fc5d33 Mon Sep 17 00:00:00 2001 From: koji Date: Mon, 15 Jul 2024 14:12:57 -0400 Subject: [PATCH 21/78] feat(app): add inline notification case to ChildNavigation storybook (#15640) * feat(app): add inline notification case to ChildNavigation storybook --- .../ChildNavigation/ChildNavigation.stories.tsx | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/app/src/organisms/ChildNavigation/ChildNavigation.stories.tsx b/app/src/organisms/ChildNavigation/ChildNavigation.stories.tsx index ef72b506ed0..ce1e4af098e 100644 --- a/app/src/organisms/ChildNavigation/ChildNavigation.stories.tsx +++ b/app/src/organisms/ChildNavigation/ChildNavigation.stories.tsx @@ -87,3 +87,14 @@ export const TitleWithTwoButtonsDisabled: Story = { ariaDisabled: true, }, } + +export const TitleWithInlineNotification: Story = { + args: { + header: 'Header', + onClickBack: () => {}, + inlineNotification: { + type: 'neutral', + heading: 'Inline notification', + }, + }, +} From ec2aa9115eb8ccd6a9b4aa682fd11e1e1c47069c Mon Sep 17 00:00:00 2001 From: Seth Foster Date: Mon, 15 Jul 2024 15:01:39 -0400 Subject: [PATCH 22/78] fix(app): odd: fix ER fundamental layout issues (#15654) I love Box. I don't know why. I always want to use it and every single time it breaks something. This is one of those times - Remove Box as the container for intervention modal because it makes all of our common tools for layout not function - Make the ODD containers 100% instead of explicit size everywhere because if you are using the right kind of container they won't overflow anyway - 3am going back for more : inRecovery FooterButtons, add a small dummy element to the button row when there is no "go back" button so the primary button is still on the right ## Testing all on ODD: - There shouldn't be scrollbars in the error recovery components anymore - If there's no "go back" button, the continue button should still be on the right --- app/src/molecules/InterventionModal/index.tsx | 7 ++++--- .../shared/RecoveryContentWrapper.tsx | 2 +- .../shared/RecoveryFooterButtons.tsx | 16 +++++++++------- .../shared/RecoveryInterventionModal.tsx | 2 -- 4 files changed, 14 insertions(+), 13 deletions(-) diff --git a/app/src/molecules/InterventionModal/index.tsx b/app/src/molecules/InterventionModal/index.tsx index 20c298c54f7..4d2de359b60 100644 --- a/app/src/molecules/InterventionModal/index.tsx +++ b/app/src/molecules/InterventionModal/index.tsx @@ -4,7 +4,6 @@ import { useSelector } from 'react-redux' import { ALIGN_CENTER, BORDERS, - Box, COLORS, Flex, Icon, @@ -15,6 +14,7 @@ import { POSITION_RELATIVE, POSITION_STICKY, SPACING, + DIRECTION_COLUMN, } from '@opentrons/components' import { getIsOnDevice } from '../../redux/config' @@ -143,8 +143,9 @@ export function InterventionModal({ return ( - { e.stopPropagation() @@ -165,7 +166,7 @@ export function InterventionModal({ {children} - + ) diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/RecoveryContentWrapper.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/RecoveryContentWrapper.tsx index 344caae0590..3c10279d50d 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/RecoveryContentWrapper.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/RecoveryContentWrapper.tsx @@ -40,6 +40,6 @@ const STYLE = css` width: 100%; @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { gap: none; - height: 29.25rem; + height: 100%; } ` diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/RecoveryFooterButtons.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/RecoveryFooterButtons.tsx index 505677fea18..3c62246ab63 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/RecoveryFooterButtons.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/RecoveryFooterButtons.tsx @@ -3,7 +3,8 @@ import { useTranslation } from 'react-i18next' import { css } from 'styled-components' import { - ALIGN_CENTER, + ALIGN_FLEX_END, + Box, Flex, JUSTIFY_SPACE_BETWEEN, SPACING, @@ -35,7 +36,7 @@ export function RecoveryFooterButtons( width="100%" height="100%" justifyContent={JUSTIFY_SPACE_BETWEEN} - alignItems={ALIGN_CENTER} + alignItems={ALIGN_FLEX_END} gridGap={SPACING.spacing8} > @@ -50,7 +51,7 @@ function RecoveryGoBackButton({ const showGoBackBtn = secondaryBtnOnClick != null const { t } = useTranslation('error_recovery') return showGoBackBtn ? ( - + - ) : null + ) : ( + + ) } function PrimaryButtonGroup(props: RecoveryFooterButtonProps): JSX.Element { @@ -73,13 +76,13 @@ function PrimaryButtonGroup(props: RecoveryFooterButtonProps): JSX.Element { if (!renderTertiaryBtn) { return ( - + ) } else { return ( - + @@ -109,7 +112,6 @@ function RecoveryPrimaryBtn({ buttonType="primary" buttonText={primaryBtnTextOverride ?? t('continue')} onClick={primaryBtnOnClick} - marginTop="auto" /> Date: Mon, 15 Jul 2024 16:29:29 -0400 Subject: [PATCH 23/78] feat(app): Implement two cols RadioButton controller (#15634) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Overview AUTH-521: Implementing functionality to control radio buttons in two columns for selecting a CSV file on robot or on USB. When users tabbed back button after they selected a file, updating parameters with file id and file name for a file selected on robot or with file path and file name for a file selected on USB. AUTH-558: Replacing the confirm selection button with an inline notification and updating back button control. # Test Plan - Clicked/tabbed on the radio buttons with file name to see if it's being selected (changing background color) - Printed out the file name in the console to check if the csv file information (id/filePath and fileName) being correctly pass back to the parameters when tabbed back button on the Choose CSV File screen. - Redirected to Parameters screen when tabbed on the back button Screenshot 2024-07-15 at 9 56 42 AM # Changelog - Added a state with type CsvFileFileType to store csv file information - Replaced the small button element with an inline notification element inside child navigation - Updated the parameter's value type to { boolean | string | number | CsvFileFileType } where CsvFileFileType contains id, file, file path, and file name - Removed csvFileInfo and setcsvFileInfo state in both chooseCsvFile and protocolSetupParameters/index files - Updated tests # Review requests # Risk assessment --------- Co-authored-by: shiyaochen Co-authored-by: Max Marrone Co-authored-by: Caila Marashaj <98041399+caila-marashaj@users.noreply.github.com> Co-authored-by: Ryan Howard Co-authored-by: Seth Foster Co-authored-by: aaron-kulkarni <107003644+aaron-kulkarni@users.noreply.github.com> Co-authored-by: Shlok Amin Co-authored-by: koji Co-authored-by: pmoegenburg Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: y3rsh <502770+y3rsh@users.noreply.github.com> Co-authored-by: Josh McVey Co-authored-by: Brayan Almonte Co-authored-by: Derek Maggio Co-authored-by: Nicholas Shiland Co-authored-by: Brent Hagen Co-authored-by: shiyaochen --- .../localization/en/protocol_setup.json | 1 + .../ProtocolSetupParameters/ChooseCsvFile.tsx | 86 +++++++++++-------- .../__tests__/ChooseCsvFile.test.tsx | 67 +++++++++++++-- .../ProtocolSetupParameters/index.tsx | 11 +-- shared-data/js/types.ts | 9 +- 5 files changed, 121 insertions(+), 53 deletions(-) diff --git a/app/src/assets/localization/en/protocol_setup.json b/app/src/assets/localization/en/protocol_setup.json index 6ac20b18775..8b495eae864 100644 --- a/app/src/assets/localization/en/protocol_setup.json +++ b/app/src/assets/localization/en/protocol_setup.json @@ -269,6 +269,7 @@ "update_deck": "Update deck", "updated": "Updated", "usb_connected_no_port_info": "USB Port Connected", + "usb_drive_notification": "Leave USB drive attached until run starts", "usb_port_connected": "USB Port {{port}}", "usb_port_number": "USB-{{port}}", "value_out_of_range_generic": "Value must be in range", diff --git a/app/src/organisms/ProtocolSetupParameters/ChooseCsvFile.tsx b/app/src/organisms/ProtocolSetupParameters/ChooseCsvFile.tsx index 23c0a7352a3..3d8a280d041 100644 --- a/app/src/organisms/ProtocolSetupParameters/ChooseCsvFile.tsx +++ b/app/src/organisms/ProtocolSetupParameters/ChooseCsvFile.tsx @@ -2,6 +2,7 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' import { useSelector } from 'react-redux' import { css } from 'styled-components' +import isEqual from 'lodash/isEqual' import last from 'lodash/last' import { @@ -20,17 +21,17 @@ import { getShellUpdateDataFiles } from '../../redux/shell' import { ChildNavigation } from '../ChildNavigation' import { EmptyFile } from './EmptyFile' -import type { CsvFileParameter } from '@opentrons/shared-data' +import type { CsvFileParameter, CsvFileFileType } from '@opentrons/shared-data' import type { CsvFileData } from '@opentrons/api-client' interface ChooseCsvFileProps { protocolId: string handleGoBack: () => void - // ToDo (kk:06/18/2024) null will be removed when implemented required part - parameter: CsvFileParameter | null - setParameter: (value: boolean | string | number, variableName: string) => void - csvFileInfo: string - setCsvFileInfo: (fileInfo: string) => void + parameter: CsvFileParameter + setParameter: ( + value: boolean | string | number | CsvFileFileType, + variableName: string + ) => void } export function ChooseCsvFile({ @@ -38,31 +39,34 @@ export function ChooseCsvFile({ handleGoBack, parameter, setParameter, - csvFileInfo, - setCsvFileInfo, }: ChooseCsvFileProps): JSX.Element { const { t } = useTranslation('protocol_setup') + const csvFilesOnUSB = useSelector(getShellUpdateDataFiles) ?? [] const csvFilesOnRobot = useAllCsvFilesQuery(protocolId).data?.data.files ?? [] - // ToDo (06/20/2024) this will removed when working on AUTH-521 - // const handleOnChange = (newValue: string | number | boolean): void => { - // setParameter(newValue, parameter?.variableName ?? 'csvFileId') - // } + const initialFileObject: CsvFileFileType = parameter.file ?? {} + const [csvFileSelected, setCsvFileSelected] = React.useState( + initialFileObject + ) - const handleConfirmSelection = (): void => { - // ToDo (kk:06/18/2024) wire up later + const handleBackButton = (): void => { + if (!isEqual(csvFileSelected, initialFileObject)) { + setParameter(csvFileSelected, parameter.variableName) + } + handleGoBack() } return ( <> {}} + buttonValue={`${csv.id}`} + onChange={() => { + setCsvFileSelected({ id: csv.id, fileName: csv.name }) + }} + isSelected={csvFileSelected?.id === csv.id} /> )) ) : ( @@ -99,23 +106,28 @@ export function ChooseCsvFile({ {csvFilesOnUSB.length !== 0 ? ( - csvFilesOnUSB.map(csv => ( - <> - {csv.length !== 0 && last(csv.split('/')) !== undefined ? ( - { - // ToDO this will be implemented AUTH-521 - // handleOnChange(option.value) - setCsvFileInfo(csv) - }} - /> - ) : null} - - )) + csvFilesOnUSB.map(csvFilePath => { + const fileName = last(csvFilePath.split('/')) + return ( + <> + {csvFilePath.length !== 0 && fileName !== undefined ? ( + { + setCsvFileSelected({ + filePath: csvFilePath, + fileName: fileName, + }) + }} + isSelected={csvFileSelected?.filePath === csvFilePath} + /> + ) : null} + + ) + }) ) : ( )} diff --git a/app/src/organisms/ProtocolSetupParameters/__tests__/ChooseCsvFile.test.tsx b/app/src/organisms/ProtocolSetupParameters/__tests__/ChooseCsvFile.test.tsx index 05c85dfc207..918f5838084 100644 --- a/app/src/organisms/ProtocolSetupParameters/__tests__/ChooseCsvFile.test.tsx +++ b/app/src/organisms/ProtocolSetupParameters/__tests__/ChooseCsvFile.test.tsx @@ -23,7 +23,6 @@ vi.mock('../EmptyFile') const mockHandleGoBack = vi.fn() const mockSetParameter = vi.fn() const mockParameter: CsvFileParameter = {} as any -const mockSetFileInfo = vi.fn() const PROTOCOL_ID = 'fake_protocol_id' const mockUsbData = [ '/media/mock-usb-drive/mock-file1.csv', @@ -64,8 +63,6 @@ describe('ChooseCsvFile', () => { handleGoBack: mockHandleGoBack, parameter: mockParameter, setParameter: mockSetParameter, - csvFileInfo: 'mockFileId', - setCsvFileInfo: mockSetFileInfo, } vi.mocked(getLocalRobot).mockReturnValue(mockConnectedRobot) vi.mocked(EmptyFile).mockReturnValue(
mock EmptyFile
) @@ -80,7 +77,7 @@ describe('ChooseCsvFile', () => { screen.getByText('Choose CSV file') screen.getByText('CSV files on robot') screen.getByText('CSV files on USB') - screen.getByText('Confirm selection') + screen.getByText('Leave USB drive attached until run starts') }) it('should render csv file names', () => { @@ -95,13 +92,71 @@ describe('ChooseCsvFile', () => { screen.getByText('mock-file3.csv') }) - it('should call a mock function when tapping back button', () => { + it('should call a mock function when tapping back button + without selecting a csv file', () => { render(props) + + fireEvent.click(screen.getAllByRole('button')[0]) + expect(props.setParameter).not.toHaveBeenCalled() + expect(mockHandleGoBack).toHaveBeenCalled() + }) + + it('should render a selected radio button in Robot side when tapped', () => { + when(useAllCsvFilesQuery) + .calledWith(PROTOCOL_ID) + .thenReturn(mockDataOnRobot as any) + render(props) + + const selectedCsvFileOnRobot = screen.getByLabelText('rtp_mock_file2.csv') + fireEvent.click(selectedCsvFileOnRobot) + expect(selectedCsvFileOnRobot).toBeChecked() + }) + + it('should render a selected radio button in USB side when tapped', () => { + render(props) + + const selectCsvFileOnUsb = screen.getByLabelText('mock-file2.csv') + fireEvent.click(selectCsvFileOnUsb) + expect(selectCsvFileOnUsb).toBeChecked() + }) + + it('call mock function (setParameter) with fileId + fileName when the selected file is a csv on Robot + tapping back button', () => { + when(useAllCsvFilesQuery) + .calledWith(PROTOCOL_ID) + .thenReturn(mockDataOnRobot as any) + render(props) + + const csvFileOnRobot = screen.getByRole('label', { + name: 'rtp_mock_file2.csv', + }) + + fireEvent.click(csvFileOnRobot) fireEvent.click(screen.getAllByRole('button')[0]) + expect(props.setParameter).toHaveBeenCalledWith( + { + id: '2', + fileName: 'rtp_mock_file2.csv', + }, + props.parameter.variableName + ) expect(mockHandleGoBack).toHaveBeenCalled() }) - it.todo('should call a mock function when tapping a csv file') + it('call mock function (setParameter) with filePath + fileName when the selected file is a csv on USB + tapping back button', () => { + render(props) + + const csvFileOnUsb = screen.getByRole('label', { name: 'mock-file1.csv' }) + + fireEvent.click(csvFileOnUsb) + fireEvent.click(screen.getAllByRole('button')[0]) + expect(props.setParameter).toHaveBeenCalledWith( + { + filePath: '/media/mock-usb-drive/mock-file1.csv', + fileName: 'mock-file1.csv', + }, + props.parameter.variableName + ) + expect(mockHandleGoBack).toHaveBeenCalled() + }) it('should render mock empty file component when there is no csv file', () => { vi.mocked(getShellUpdateDataFiles).mockReturnValue([]) diff --git a/app/src/organisms/ProtocolSetupParameters/index.tsx b/app/src/organisms/ProtocolSetupParameters/index.tsx index 239406650a6..27cc1c54402 100644 --- a/app/src/organisms/ProtocolSetupParameters/index.tsx +++ b/app/src/organisms/ProtocolSetupParameters/index.tsx @@ -35,6 +35,7 @@ import type { NumberParameter, RunTimeParameter, ValueRunTimeParameter, + CsvFileFileType, } from '@opentrons/shared-data' import type { LabwareOffsetCreateData } from '@opentrons/api-client' @@ -86,14 +87,8 @@ export function ProtocolSetupParameters({ ) const { makeSnackbar } = useToaster() - const csvFileParameter = runTimeParameters.find( - (param): param is CsvFileParameter => param.type === 'csv_file' - ) - const initialFileId: string = csvFileParameter?.file?.id ?? '' - const [csvFileInfo, setCSVFileInfo] = React.useState(initialFileId) - const updateParameters = ( - value: boolean | string | number, + value: boolean | string | number | CsvFileFileType, variableName: string ): void => { const updatedParameters = runTimeParametersOverrides.map(parameter => { @@ -289,8 +284,6 @@ export function ProtocolSetupParameters({ }} parameter={chooseCsvFileScreen} setParameter={updateParameters} - csvFileInfo={csvFileInfo} - setCsvFileInfo={setCSVFileInfo} /> ) } diff --git a/shared-data/js/types.ts b/shared-data/js/types.ts index a744d8b645e..06a1765ca3a 100644 --- a/shared-data/js/types.ts +++ b/shared-data/js/types.ts @@ -643,9 +643,16 @@ interface BooleanParameter extends BaseRunTimeParameter { value: boolean } +export interface CsvFileFileType { + id?: string + file?: File | null + filePath?: string + fileName?: string +} + export interface CsvFileParameter extends BaseRunTimeParameter { type: CsvFileParameterType - file?: { id?: string; file?: File | null } | null + file?: CsvFileFileType | null } type NumberParameterType = 'int' | 'float' From b779bf0248e28576e4884f4a169a5aa7769f9cad Mon Sep 17 00:00:00 2001 From: CaseyBatten Date: Mon, 15 Jul 2024 16:55:26 -0400 Subject: [PATCH 24/78] fix(shared-data): Implement updated Hardware Testing pipette configuration values (#15436) Closes PLAT-270 and PLAT-269 Incorporate new hardware testing values for current, speed, distance and tip overlaps for the 96ch and 8ch pipettes on the Flex, bump tip overlap to v3. --- .../core/engine/overlap_versions.py | 6 +- .../core/engine/test_instrument_core.py | 26 +- .../core/engine/test_overlap_versions.py | 19 +- .../core/engine/test_protocol_core.py | 6 +- .../2/general/eight_channel/p1000/3_4.json | 139 ++++---- .../2/general/eight_channel/p1000/3_5.json | 139 ++++---- .../2/general/eight_channel/p50/3_4.json | 69 ++-- .../2/general/eight_channel/p50/3_5.json | 69 ++-- .../general/ninety_six_channel/p1000/3_5.json | 322 ++++++++++++------ .../general/ninety_six_channel/p1000/3_6.json | 318 +++++++++++------ .../pipette/pipette_definition.py | 2 +- 11 files changed, 700 insertions(+), 415 deletions(-) diff --git a/api/src/opentrons/protocol_api/core/engine/overlap_versions.py b/api/src/opentrons/protocol_api/core/engine/overlap_versions.py index ed14859ecd3..896ba5bb774 100644 --- a/api/src/opentrons/protocol_api/core/engine/overlap_versions.py +++ b/api/src/opentrons/protocol_api/core/engine/overlap_versions.py @@ -3,7 +3,11 @@ from typing_extensions import Final from opentrons.protocols.api_support.types import APIVersion -_OVERLAP_VERSION_MAP: Final = {APIVersion(2, 0): "v0", APIVersion(2, 19): "v1"} +_OVERLAP_VERSION_MAP: Final = { + APIVersion(2, 0): "v0", + APIVersion(2, 19): "v1", + APIVersion(2, 20): "v3", +} @lru_cache(1) diff --git a/api/tests/opentrons/protocol_api/core/engine/test_instrument_core.py b/api/tests/opentrons/protocol_api/core/engine/test_instrument_core.py index 6b97040c2ec..0405d939abe 100644 --- a/api/tests/opentrons/protocol_api/core/engine/test_instrument_core.py +++ b/api/tests/opentrons/protocol_api/core/engine/test_instrument_core.py @@ -5,6 +5,7 @@ from opentrons_shared_data.errors.exceptions import PipetteLiquidNotFoundError import pytest from decoy import Decoy +from decoy import errors from opentrons_shared_data.pipette.dev_types import PipetteNameType @@ -1293,15 +1294,26 @@ def test_configure_for_volume_post_219( """Configure_for_volume should specify overlap version.""" decoy.when(mock_protocol_core.api_version).then_return(version) subject.configure_for_volume(123.0) - decoy.verify( - mock_engine_client.execute_command( - cmd.ConfigureForVolumeParams( - pipetteId=subject.pipette_id, - volume=123.0, - tipOverlapNotAfterVersion="v1", + try: + decoy.verify( + mock_engine_client.execute_command( + cmd.ConfigureForVolumeParams( + pipetteId=subject.pipette_id, + volume=123.0, + tipOverlapNotAfterVersion="v1", + ) + ) + ) + except errors.VerifyError: + decoy.verify( + mock_engine_client.execute_command( + cmd.ConfigureForVolumeParams( + pipetteId=subject.pipette_id, + volume=123.0, + tipOverlapNotAfterVersion="v3", + ) ) ) - ) @pytest.mark.parametrize("version", versions_at_or_above(APIVersion(2, 20))) diff --git a/api/tests/opentrons/protocol_api/core/engine/test_overlap_versions.py b/api/tests/opentrons/protocol_api/core/engine/test_overlap_versions.py index 9d41a431026..41ce048dd43 100644 --- a/api/tests/opentrons/protocol_api/core/engine/test_overlap_versions.py +++ b/api/tests/opentrons/protocol_api/core/engine/test_overlap_versions.py @@ -16,11 +16,18 @@ def test_all_below_219_use_v0(api_version: APIVersion) -> None: @pytest.mark.parametrize("api_version", versions_at_or_above(APIVersion(2, 19))) -def test_all_above_219_use_v1(api_version: APIVersion) -> None: - """Versions above 2.19 should use v1.""" - assert overlap_for_api_version(api_version) == "v1" +def test_above_219_below_220_use_v1(api_version: APIVersion) -> None: + """Versions above 2.19 and below 2.20 should use v1.""" + if api_version in versions_below(APIVersion(2, 20), flex_only=False): + assert overlap_for_api_version(api_version) == "v1" -def test_future_api_version_uses_v1() -> None: - """Future versions should use v1.""" - assert overlap_for_api_version(APIVersion(2, 99)) == "v1" +@pytest.mark.parametrize("api_version", versions_at_or_above(APIVersion(2, 20))) +def test_above_220_use_v3(api_version: APIVersion) -> None: + """Versions above 2.20 should use v3.""" + assert overlap_for_api_version(api_version) == "v3" + + +def test_future_api_version_uses_v3() -> None: + """Future versions should use v3.""" + assert overlap_for_api_version(APIVersion(2, 99)) == "v3" diff --git a/api/tests/opentrons/protocol_api/core/engine/test_protocol_core.py b/api/tests/opentrons/protocol_api/core/engine/test_protocol_core.py index dc10b9bbad8..18286397a76 100644 --- a/api/tests/opentrons/protocol_api/core/engine/test_protocol_core.py +++ b/api/tests/opentrons/protocol_api/core/engine/test_protocol_core.py @@ -277,8 +277,8 @@ def test_load_instrument_pre_219( assert result.pipette_id == "cool-pipette" -@pytest.mark.parametrize("api_version", versions_at_or_above(APIVersion(2, 19))) -def test_load_instrument_post_219( +@pytest.mark.parametrize("api_version", versions_at_or_above(APIVersion(2, 20))) +def test_load_instrument_post_220( decoy: Decoy, mock_sync_hardware_api: SyncHardwareAPI, mock_engine_client: EngineClient, @@ -290,7 +290,7 @@ def test_load_instrument_post_219( cmd.LoadPipetteParams( pipetteName=PipetteNameType.P300_SINGLE, mount=MountType.LEFT, - tipOverlapNotAfterVersion="v1", + tipOverlapNotAfterVersion="v3", liquidPresenceDetection=False, ) ) diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_4.json b/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_4.json index 582288c71d5..33eca65b2fc 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_4.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_4.json @@ -24,8 +24,8 @@ "SingleA1": { "default": { "speed": 10.0, - "distance": 13.0, - "current": 0.2, + "distance": 11.0, + "current": 0.15, "tipOverlaps": { "v0": { "default": 10.5, @@ -37,13 +37,13 @@ "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.17 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.42, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_tiprack_1000ul/1": 9.67, - "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.42, - "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 9.67 + "default": 10.08, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 10.1, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.08, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.26, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 10.1, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.08, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.26 } } } @@ -51,8 +51,8 @@ "SingleH1": { "default": { "speed": 10.0, - "distance": 13.0, - "current": 0.2, + "distance": 11.0, + "current": 0.15, "tipOverlaps": { "v0": { "default": 10.5, @@ -64,13 +64,13 @@ "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.17 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.42, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_tiprack_1000ul/1": 9.67, - "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.42, - "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 9.67 + "default": 9.52, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.71, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.52, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.2, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.71, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.52, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.2 } } } @@ -91,13 +91,13 @@ "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.17 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.42, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_tiprack_1000ul/1": 9.67, - "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.42, - "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 9.67 + "default": 9.24, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.52, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.24, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 9.73, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.52, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.24, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 9.73 } } } @@ -106,7 +106,7 @@ "default": { "speed": 10.0, "distance": 13.0, - "current": 0.21, + "current": 0.2, "tipOverlaps": { "v0": { "default": 10.5, @@ -118,13 +118,13 @@ "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.17 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.42, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_tiprack_1000ul/1": 9.67, - "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.42, - "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 9.67 + "default": 9.2, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.3, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.2, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 9.8, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.3, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.2, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 9.8 } } } @@ -133,7 +133,7 @@ "default": { "speed": 10.0, "distance": 13.0, - "current": 0.28, + "current": 0.35, "tipOverlaps": { "v0": { "default": 10.5, @@ -145,13 +145,13 @@ "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.17 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.42, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_tiprack_1000ul/1": 9.67, - "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.42, - "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 9.67 + "default": 9.2, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.3, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.2, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 9.8, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.3, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.2, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 9.8 } } } @@ -160,7 +160,7 @@ "default": { "speed": 10.0, "distance": 13.0, - "current": 0.34, + "current": 0.4, "tipOverlaps": { "v0": { "default": 10.5, @@ -172,13 +172,13 @@ "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.17 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.42, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_tiprack_1000ul/1": 9.67, - "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.42, - "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 9.67 + "default": 9.2, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.3, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.2, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 9.8, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.3, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.2, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 9.8 } } } @@ -187,7 +187,7 @@ "default": { "speed": 10.0, "distance": 13.0, - "current": 0.41, + "current": 0.4, "tipOverlaps": { "v0": { "default": 10.5, @@ -199,13 +199,13 @@ "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.17 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.42, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_tiprack_1000ul/1": 9.67, - "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.42, - "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 9.67 + "default": 9.2, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.3, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.2, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 9.8, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.3, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.2, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 9.8 } } } @@ -214,7 +214,7 @@ "default": { "speed": 10.0, "distance": 13.0, - "current": 0.48, + "current": 0.5, "tipOverlaps": { "v0": { "default": 10.5, @@ -226,13 +226,13 @@ "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.17 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.42, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_tiprack_1000ul/1": 9.67, - "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.42, - "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 9.67 + "default": 9.13, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.23, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.13, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 9.9, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.23, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.13, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 9.9 } } } @@ -260,6 +260,15 @@ "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.42, "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.27, "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 9.67 + }, + "v3": { + "default": 9.28, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.37, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.28, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 9.67, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.37, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.28, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 9.67 } } } diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_5.json b/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_5.json index 582288c71d5..33eca65b2fc 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_5.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_5.json @@ -24,8 +24,8 @@ "SingleA1": { "default": { "speed": 10.0, - "distance": 13.0, - "current": 0.2, + "distance": 11.0, + "current": 0.15, "tipOverlaps": { "v0": { "default": 10.5, @@ -37,13 +37,13 @@ "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.17 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.42, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_tiprack_1000ul/1": 9.67, - "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.42, - "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 9.67 + "default": 10.08, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 10.1, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.08, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.26, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 10.1, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.08, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.26 } } } @@ -51,8 +51,8 @@ "SingleH1": { "default": { "speed": 10.0, - "distance": 13.0, - "current": 0.2, + "distance": 11.0, + "current": 0.15, "tipOverlaps": { "v0": { "default": 10.5, @@ -64,13 +64,13 @@ "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.17 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.42, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_tiprack_1000ul/1": 9.67, - "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.42, - "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 9.67 + "default": 9.52, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.71, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.52, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.2, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.71, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.52, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.2 } } } @@ -91,13 +91,13 @@ "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.17 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.42, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_tiprack_1000ul/1": 9.67, - "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.42, - "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 9.67 + "default": 9.24, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.52, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.24, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 9.73, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.52, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.24, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 9.73 } } } @@ -106,7 +106,7 @@ "default": { "speed": 10.0, "distance": 13.0, - "current": 0.21, + "current": 0.2, "tipOverlaps": { "v0": { "default": 10.5, @@ -118,13 +118,13 @@ "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.17 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.42, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_tiprack_1000ul/1": 9.67, - "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.42, - "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 9.67 + "default": 9.2, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.3, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.2, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 9.8, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.3, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.2, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 9.8 } } } @@ -133,7 +133,7 @@ "default": { "speed": 10.0, "distance": 13.0, - "current": 0.28, + "current": 0.35, "tipOverlaps": { "v0": { "default": 10.5, @@ -145,13 +145,13 @@ "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.17 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.42, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_tiprack_1000ul/1": 9.67, - "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.42, - "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 9.67 + "default": 9.2, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.3, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.2, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 9.8, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.3, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.2, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 9.8 } } } @@ -160,7 +160,7 @@ "default": { "speed": 10.0, "distance": 13.0, - "current": 0.34, + "current": 0.4, "tipOverlaps": { "v0": { "default": 10.5, @@ -172,13 +172,13 @@ "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.17 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.42, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_tiprack_1000ul/1": 9.67, - "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.42, - "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 9.67 + "default": 9.2, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.3, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.2, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 9.8, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.3, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.2, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 9.8 } } } @@ -187,7 +187,7 @@ "default": { "speed": 10.0, "distance": 13.0, - "current": 0.41, + "current": 0.4, "tipOverlaps": { "v0": { "default": 10.5, @@ -199,13 +199,13 @@ "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.17 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.42, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_tiprack_1000ul/1": 9.67, - "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.42, - "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 9.67 + "default": 9.2, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.3, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.2, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 9.8, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.3, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.2, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 9.8 } } } @@ -214,7 +214,7 @@ "default": { "speed": 10.0, "distance": 13.0, - "current": 0.48, + "current": 0.5, "tipOverlaps": { "v0": { "default": 10.5, @@ -226,13 +226,13 @@ "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.17 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.42, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_tiprack_1000ul/1": 9.67, - "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.42, - "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 9.67 + "default": 9.13, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.23, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.13, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 9.9, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.23, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.13, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 9.9 } } } @@ -260,6 +260,15 @@ "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.42, "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.27, "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 9.67 + }, + "v3": { + "default": 9.28, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.37, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.28, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 9.67, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.37, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.28, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 9.67 } } } diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p50/3_4.json b/shared-data/pipette/definitions/2/general/eight_channel/p50/3_4.json index c901983a655..d62d02e1b06 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p50/3_4.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p50/3_4.json @@ -24,8 +24,8 @@ "SingleA1": { "default": { "speed": 10.0, - "distance": 13.0, - "current": 0.2, + "distance": 11.0, + "current": 0.15, "tipOverlaps": { "v0": { "default": 10.5, @@ -33,9 +33,9 @@ "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.05 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.27 + "default": 10.08, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.08, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.08 } } } @@ -43,8 +43,8 @@ "SingleH1": { "default": { "speed": 10.0, - "distance": 13.0, - "current": 0.2, + "distance": 11.0, + "current": 0.15, "tipOverlaps": { "v0": { "default": 10.5, @@ -52,9 +52,9 @@ "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.05 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.27 + "default": 9.52, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.52, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.52 } } } @@ -71,9 +71,9 @@ "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.05 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.27 + "default": 9.24, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.24, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.24 } } } @@ -90,9 +90,9 @@ "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.05 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.27 + "default": 9.2, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.2, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.2 } } } @@ -101,7 +101,7 @@ "default": { "speed": 10.0, "distance": 13.0, - "current": 0.28, + "current": 0.35, "tipOverlaps": { "v0": { "default": 10.5, @@ -109,9 +109,9 @@ "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.05 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.27 + "default": 9.2, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.2, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.2 } } } @@ -120,7 +120,7 @@ "default": { "speed": 10.0, "distance": 13.0, - "current": 0.34, + "current": 0.4, "tipOverlaps": { "v0": { "default": 10.5, @@ -128,9 +128,9 @@ "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.05 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.27 + "default": 9.2, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.2, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.2 } } } @@ -139,7 +139,7 @@ "default": { "speed": 10.0, "distance": 13.0, - "current": 0.41, + "current": 0.4, "tipOverlaps": { "v0": { "default": 10.5, @@ -147,9 +147,9 @@ "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.05 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.27 + "default": 9.2, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.2, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.2 } } } @@ -158,7 +158,7 @@ "default": { "speed": 10.0, "distance": 13.0, - "current": 0.48, + "current": 0.5, "tipOverlaps": { "v0": { "default": 10.5, @@ -166,9 +166,9 @@ "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.05 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.27 + "default": 9.13, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.13, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.13 } } } @@ -188,6 +188,11 @@ "default": 10.5, "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.27, "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.27 + }, + "v3": { + "default": 9.28, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.28, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.28 } } } diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p50/3_5.json b/shared-data/pipette/definitions/2/general/eight_channel/p50/3_5.json index c901983a655..d62d02e1b06 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p50/3_5.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p50/3_5.json @@ -24,8 +24,8 @@ "SingleA1": { "default": { "speed": 10.0, - "distance": 13.0, - "current": 0.2, + "distance": 11.0, + "current": 0.15, "tipOverlaps": { "v0": { "default": 10.5, @@ -33,9 +33,9 @@ "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.05 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.27 + "default": 10.08, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.08, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.08 } } } @@ -43,8 +43,8 @@ "SingleH1": { "default": { "speed": 10.0, - "distance": 13.0, - "current": 0.2, + "distance": 11.0, + "current": 0.15, "tipOverlaps": { "v0": { "default": 10.5, @@ -52,9 +52,9 @@ "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.05 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.27 + "default": 9.52, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.52, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.52 } } } @@ -71,9 +71,9 @@ "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.05 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.27 + "default": 9.24, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.24, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.24 } } } @@ -90,9 +90,9 @@ "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.05 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.27 + "default": 9.2, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.2, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.2 } } } @@ -101,7 +101,7 @@ "default": { "speed": 10.0, "distance": 13.0, - "current": 0.28, + "current": 0.35, "tipOverlaps": { "v0": { "default": 10.5, @@ -109,9 +109,9 @@ "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.05 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.27 + "default": 9.2, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.2, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.2 } } } @@ -120,7 +120,7 @@ "default": { "speed": 10.0, "distance": 13.0, - "current": 0.34, + "current": 0.4, "tipOverlaps": { "v0": { "default": 10.5, @@ -128,9 +128,9 @@ "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.05 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.27 + "default": 9.2, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.2, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.2 } } } @@ -139,7 +139,7 @@ "default": { "speed": 10.0, "distance": 13.0, - "current": 0.41, + "current": 0.4, "tipOverlaps": { "v0": { "default": 10.5, @@ -147,9 +147,9 @@ "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.05 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.27 + "default": 9.2, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.2, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.2 } } } @@ -158,7 +158,7 @@ "default": { "speed": 10.0, "distance": 13.0, - "current": 0.48, + "current": 0.5, "tipOverlaps": { "v0": { "default": 10.5, @@ -166,9 +166,9 @@ "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.05 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.27, - "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.27 + "default": 9.13, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.13, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.13 } } } @@ -188,6 +188,11 @@ "default": 10.5, "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.27, "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.27 + }, + "v3": { + "default": 9.28, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.28, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.28 } } } diff --git a/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_5.json b/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_5.json index 8a45197f246..9c6df88b575 100644 --- a/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_5.json +++ b/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_5.json @@ -11,6 +11,34 @@ "SingleH12": ["H12"], "Column1": ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"], "Column12": ["A12", "B12", "C12", "D12", "E12", "F12", "G12", "H12"], + "RowA": [ + "A1", + "A2", + "A3", + "A4", + "A5", + "A6", + "A7", + "A8", + "A9", + "A10", + "A11", + "A12" + ], + "RowH": [ + "H1", + "H2", + "H3", + "H4", + "H5", + "H6", + "H7", + "H8", + "H9", + "H10", + "H11", + "H12" + ], "Full": [ "A1", "A2", @@ -119,8 +147,8 @@ "SingleA1": { "default": { "speed": 10.0, - "distance": 13.0, - "current": 0.2, + "distance": 10.5, + "current": 0.4, "tipOverlaps": { "v0": { "default": 10.5, @@ -132,20 +160,20 @@ "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.5 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.16, - "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.21, - "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.74, - "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.21, - "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.74, - "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.16 + "default": 9.884, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.884, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.335, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.981, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.335, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.981, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.884 } } }, "t1000": { "speed": 10.0, - "distance": 13.0, - "current": 0.2, + "distance": 10.5, + "current": 0.4, "tipOverlaps": { "v0": { "default": 10.5, @@ -153,16 +181,16 @@ "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.5 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.21, - "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.21 + "default": 10.335, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.335, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.335 } } }, "t200": { "speed": 10.0, - "distance": 13.0, - "current": 0.2, + "distance": 10.5, + "current": 0.4, "tipOverlaps": { "v0": { "default": 10.5, @@ -170,16 +198,16 @@ "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 10.5 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.74, - "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.74 + "default": 9.981, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.981, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.981 } } }, "t50": { "speed": 10.0, - "distance": 13.0, - "current": 0.2, + "distance": 10.5, + "current": 0.4, "tipOverlaps": { "v0": { "default": 10.5, @@ -187,9 +215,9 @@ "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.5 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.16, - "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.16 + "default": 9.884, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.884, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.884 } } } @@ -197,8 +225,8 @@ "SingleH1": { "default": { "speed": 10.0, - "distance": 13.0, - "current": 0.2, + "distance": 10.5, + "current": 0.4, "tipOverlaps": { "v0": { "default": 10.5, @@ -210,20 +238,20 @@ "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.5 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.16, - "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.21, - "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.74, - "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.21, - "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.74, - "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.16 + "default": 9.884, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.884, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.335, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.981, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.335, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.981, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.884 } } }, "t1000": { "speed": 10.0, - "distance": 13.0, - "current": 0.2, + "distance": 10.5, + "current": 0.4, "tipOverlaps": { "v0": { "default": 10.5, @@ -231,16 +259,16 @@ "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.5 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.21, - "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.21 + "default": 10.335, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.335, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.335 } } }, "t200": { "speed": 10.0, - "distance": 13.0, - "current": 0.2, + "distance": 10.5, + "current": 0.4, "tipOverlaps": { "v0": { "default": 10.5, @@ -248,16 +276,16 @@ "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 10.5 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.74, - "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.74 + "default": 9.981, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.981, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.981 } } }, "t50": { "speed": 10.0, - "distance": 13.0, - "current": 0.2, + "distance": 10.5, + "current": 0.4, "tipOverlaps": { "v0": { "default": 10.5, @@ -265,9 +293,9 @@ "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.5 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.16, - "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.16 + "default": 9.884, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.884, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.884 } } } @@ -275,8 +303,8 @@ "SingleA12": { "default": { "speed": 10.0, - "distance": 13.0, - "current": 0.2, + "distance": 10.5, + "current": 0.4, "tipOverlaps": { "v0": { "default": 10.5, @@ -288,20 +316,20 @@ "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.5 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.16, - "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.21, - "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.74, - "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.21, - "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.74, - "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.16 + "default": 9.884, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.884, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.335, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.981, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.335, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.981, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.884 } } }, "t1000": { "speed": 10.0, - "distance": 13.0, - "current": 0.2, + "distance": 10.5, + "current": 0.4, "tipOverlaps": { "v0": { "default": 10.5, @@ -309,16 +337,16 @@ "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.5 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.21, - "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.21 + "default": 10.335, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.335, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.335 } } }, "t200": { "speed": 10.0, - "distance": 13.0, - "current": 0.2, + "distance": 10.5, + "current": 0.4, "tipOverlaps": { "v0": { "default": 10.5, @@ -326,16 +354,16 @@ "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 10.5 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.74, - "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.74 + "default": 9.981, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.981, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.981 } } }, "t50": { "speed": 10.0, - "distance": 13.0, - "current": 0.2, + "distance": 10.5, + "current": 0.4, "tipOverlaps": { "v0": { "default": 10.5, @@ -343,9 +371,9 @@ "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.5 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.16, - "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.16 + "default": 9.884, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.884, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.884 } } } @@ -353,8 +381,8 @@ "SingleH12": { "default": { "speed": 10.0, - "distance": 13.0, - "current": 0.2, + "distance": 10.5, + "current": 0.4, "tipOverlaps": { "v0": { "default": 10.5, @@ -366,20 +394,20 @@ "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.5 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.16, - "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.21, - "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.74, - "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.21, - "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.74, - "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.16 + "default": 9.884, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.884, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.335, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.981, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.335, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.981, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.884 } } }, "t1000": { "speed": 10.0, - "distance": 13.0, - "current": 0.2, + "distance": 10.5, + "current": 0.4, "tipOverlaps": { "v0": { "default": 10.5, @@ -387,16 +415,16 @@ "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.5 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.21, - "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.21 + "default": 10.335, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.335, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.335 } } }, "t200": { "speed": 10.0, - "distance": 13.0, - "current": 0.2, + "distance": 10.5, + "current": 0.4, "tipOverlaps": { "v0": { "default": 10.5, @@ -404,16 +432,16 @@ "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 10.5 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.74, - "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.74 + "default": 9.981, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.981, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.981 } } }, "t50": { "speed": 10.0, - "distance": 13.0, - "current": 0.2, + "distance": 10.5, + "current": 0.4, "tipOverlaps": { "v0": { "default": 10.5, @@ -421,9 +449,9 @@ "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.5 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.16, - "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.16 + "default": 9.884, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.884, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.884 } } } @@ -451,6 +479,15 @@ "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.21, "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.74, "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.16 + }, + "v3": { + "default": 9.49, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.52, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.29, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.49, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.29, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.49, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.52 } } }, @@ -472,6 +509,11 @@ "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.21, "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.74, "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.16 + }, + "v3": { + "default": 10.29, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.29, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.29 } } }, @@ -493,6 +535,11 @@ "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.21, "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.74, "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.16 + }, + "v3": { + "default": 9.49, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.49, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.49 } } }, @@ -510,6 +557,11 @@ "default": 10.5, "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.16, "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.16 + }, + "v3": { + "default": 9.52, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.52, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.52 } } } @@ -537,6 +589,15 @@ "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.21, "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.74, "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.16 + }, + "v3": { + "default": 9.49, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.52, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.29, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.49, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.29, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.49, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.52 } } }, @@ -552,8 +613,17 @@ }, "v1": { "default": 10.5, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.16, "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.21, - "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.21 + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.74, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.21, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.74, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.16 + }, + "v3": { + "default": 10.29, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.29, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.29 } } }, @@ -569,8 +639,17 @@ }, "v1": { "default": 10.5, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.16, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.21, "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.74, - "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.74 + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.21, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.74, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.16 + }, + "v3": { + "default": 9.49, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.49, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.49 } } }, @@ -588,6 +667,53 @@ "default": 10.5, "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.16, "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.16 + }, + "v3": { + "default": 9.52, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.52, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.52 + } + } + } + }, + "RowA": { + "default": { + "speed": 10.0, + "distance": 13.0, + "current": 0.55, + "tipOverlaps": { + "v0": { + "default": 10.5, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.5, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.5, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 10.5 + }, + "v1": { + "default": 9.379, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.379, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 9.87, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.6 + } + } + } + }, + "RowH": { + "default": { + "speed": 10.0, + "distance": 13.0, + "current": 0.55, + "tipOverlaps": { + "v0": { + "default": 10.5, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.5, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.5, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 10.5 + }, + "v1": { + "default": 9.401, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.401, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 9.876, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.415 } } } diff --git a/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_6.json b/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_6.json index 949593f74c6..7bcfb04e4f0 100644 --- a/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_6.json +++ b/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_6.json @@ -147,65 +147,77 @@ "SingleA1": { "default": { "speed": 10.0, - "distance": 13.0, - "current": 0.15, + "distance": 10.5, + "current": 0.4, "tipOverlaps": { "v0": { "default": 10.5, "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.5, "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.5, - "opentrons/opentrons_flex_96_tiprack_200ul/1": 10.5 + "opentrons/opentrons_flex_96_tiprack_200ul/1": 10.5, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.5, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 10.5, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.5 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.16, - "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.21, - "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.74 + "default": 9.884, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.884, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.335, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.981, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.335, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.981, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.884 } } }, "t1000": { "speed": 10.0, - "distance": 13.0, - "current": 0.15, + "distance": 10.5, + "current": 0.4, "tipOverlaps": { "v0": { "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.5 + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.5, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.5 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.21 + "default": 10.335, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.335, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.335 } } }, "t200": { "speed": 10.0, - "distance": 13.0, - "current": 0.2, + "distance": 10.5, + "current": 0.4, "tipOverlaps": { "v0": { "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_200ul/1": 10.5 + "opentrons/opentrons_flex_96_tiprack_200ul/1": 10.5, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 10.5 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.74 + "default": 9.981, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.981, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.981 } } }, "t50": { "speed": 10.0, - "distance": 13.0, - "current": 0.2, + "distance": 10.5, + "current": 0.4, "tipOverlaps": { "v0": { "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.5 + "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.5, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.5 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.16 + "default": 9.884, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.884, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.884 } } } @@ -213,65 +225,77 @@ "SingleH1": { "default": { "speed": 10.0, - "distance": 13.0, - "current": 0.2, + "distance": 10.5, + "current": 0.4, "tipOverlaps": { "v0": { "default": 10.5, "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.5, "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.5, - "opentrons/opentrons_flex_96_tiprack_200ul/1": 10.5 + "opentrons/opentrons_flex_96_tiprack_200ul/1": 10.5, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.5, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 10.5, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.5 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.16, - "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.21, - "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.74 + "default": 9.884, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.884, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.335, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.981, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.335, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.981, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.884 } } }, - "1000": { + "t1000": { "speed": 10.0, - "distance": 13.0, - "current": 0.2, + "distance": 10.5, + "current": 0.4, "tipOverlaps": { "v0": { "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.5 + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.5, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.5 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.21 + "default": 10.335, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.335, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.335 } } }, "t200": { "speed": 10.0, - "distance": 13.0, - "current": 0.2, + "distance": 10.5, + "current": 0.4, "tipOverlaps": { "v0": { "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_200ul/1": 10.5 + "opentrons/opentrons_flex_96_tiprack_200ul/1": 10.5, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 10.5 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.74 + "default": 9.981, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.981, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.981 } } }, "t50": { "speed": 10.0, - "distance": 13.0, - "current": 0.2, + "distance": 10.5, + "current": 0.4, "tipOverlaps": { "v0": { "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.5 + "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.5, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.5 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.16 + "default": 9.884, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.884, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.884 } } } @@ -279,65 +303,77 @@ "SingleA12": { "default": { "speed": 10.0, - "distance": 13.0, - "current": 0.2, + "distance": 10.5, + "current": 0.4, "tipOverlaps": { "v0": { "default": 10.5, "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.5, "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.5, - "opentrons/opentrons_flex_96_tiprack_200ul/1": 10.5 + "opentrons/opentrons_flex_96_tiprack_200ul/1": 10.5, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.5, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 10.5, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.5 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.16, - "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.21, - "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.74 + "default": 9.884, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.884, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.335, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.981, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.335, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.981, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.884 } } }, "t1000": { "speed": 10.0, - "distance": 13.0, - "current": 0.2, + "distance": 10.5, + "current": 0.4, "tipOverlaps": { "v0": { "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.5 + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.5, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.5 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.21 + "default": 10.335, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.335, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.335 } } }, "t200": { "speed": 10.0, - "distance": 13.0, - "current": 0.2, + "distance": 10.5, + "current": 0.4, "tipOverlaps": { "v0": { "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_200ul/1": 10.5 + "opentrons/opentrons_flex_96_tiprack_200ul/1": 10.5, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 10.5 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.74 + "default": 9.981, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.981, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.981 } } }, "t50": { "speed": 10.0, - "distance": 13.0, - "current": 0.2, + "distance": 10.5, + "current": 0.4, "tipOverlaps": { "v0": { "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.5 + "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.5, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.5 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.16 + "default": 9.884, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.884, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.884 } } } @@ -345,65 +381,77 @@ "SingleH12": { "default": { "speed": 10.0, - "distance": 13.0, - "current": 0.2, + "distance": 10.5, + "current": 0.4, "tipOverlaps": { "v0": { "default": 10.5, "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.5, "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.5, - "opentrons/opentrons_flex_96_tiprack_200ul/1": 10.5 + "opentrons/opentrons_flex_96_tiprack_200ul/1": 10.5, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.5, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 10.5, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.5 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.16, - "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.21, - "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.74 + "default": 9.884, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.884, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.335, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.981, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.335, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.981, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.884 } } }, "t1000": { "speed": 10.0, - "distance": 13.0, - "current": 0.2, + "distance": 10.5, + "current": 0.4, "tipOverlaps": { "v0": { "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.5 + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.5, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.5 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.21 + "default": 10.335, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.335, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.335 } } }, "t200": { "speed": 10.0, - "distance": 13.0, - "current": 0.2, + "distance": 10.5, + "current": 0.4, "tipOverlaps": { "v0": { "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_200ul/1": 10.5 + "opentrons/opentrons_flex_96_tiprack_200ul/1": 10.5, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 10.5 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.74 + "default": 9.981, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.981, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.981 } } }, "t50": { "speed": 10.0, - "distance": 13.0, - "current": 0.2, + "distance": 10.5, + "current": 0.4, "tipOverlaps": { "v0": { "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.5 + "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.5, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.5 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.16 + "default": 9.884, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.884, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.884 } } } @@ -418,13 +466,25 @@ "default": 10.5, "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.5, "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.5, - "opentrons/opentrons_flex_96_tiprack_200ul/1": 10.5 + "opentrons/opentrons_flex_96_tiprack_200ul/1": 10.5, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.5, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 10.5, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.5 }, "v1": { "default": 10.5, "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.16, "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.21, "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.74 + }, + "v3": { + "default": 9.49, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.52, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.29, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.49, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.29, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.49, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.52 } } }, @@ -435,11 +495,17 @@ "tipOverlaps": { "v0": { "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.5 + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.5, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.5 }, "v1": { "default": 10.5, "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.21 + }, + "v3": { + "default": 10.29, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.29, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.29 } } }, @@ -450,11 +516,17 @@ "tipOverlaps": { "v0": { "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_200ul/1": 10.5 + "opentrons/opentrons_flex_96_tiprack_200ul/1": 10.5, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 10.5 }, "v1": { "default": 10.5, "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.74 + }, + "v3": { + "default": 9.49, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.49, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.49 } } }, @@ -465,11 +537,17 @@ "tipOverlaps": { "v0": { "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.5 + "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.5, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.5 }, "v1": { "default": 10.5, "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.16 + }, + "v3": { + "default": 9.52, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.52, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.52 } } } @@ -484,13 +562,25 @@ "default": 10.5, "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.5, "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.5, - "opentrons/opentrons_flex_96_tiprack_200ul/1": 10.5 + "opentrons/opentrons_flex_96_tiprack_200ul/1": 10.5, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.5, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 10.5, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.5 }, "v1": { "default": 10.5, "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.16, "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.21, "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.74 + }, + "v3": { + "default": 9.49, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.52, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.29, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.49, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.29, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.49, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.52 } } }, @@ -501,11 +591,17 @@ "tipOverlaps": { "v0": { "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.5 + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.5, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.5 }, "v1": { "default": 10.5, "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.21 + }, + "v3": { + "default": 10.29, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.29, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.29 } } }, @@ -516,11 +612,17 @@ "tipOverlaps": { "v0": { "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_200ul/1": 10.5 + "opentrons/opentrons_flex_96_tiprack_200ul/1": 10.5, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 10.5 }, "v1": { "default": 10.5, "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.74 + }, + "v3": { + "default": 9.49, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.49, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.49 } } }, @@ -531,11 +633,17 @@ "tipOverlaps": { "v0": { "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.5 + "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.5, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.5 }, "v1": { "default": 10.5, "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.16 + }, + "v3": { + "default": 9.52, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.52, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.52 } } } @@ -553,10 +661,10 @@ "opentrons/opentrons_flex_96_tiprack_200ul/1": 10.5 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.16, - "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.21, - "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.74 + "default": 9.379, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.379, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 9.87, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.6 } } } @@ -574,10 +682,10 @@ "opentrons/opentrons_flex_96_tiprack_200ul/1": 10.5 }, "v1": { - "default": 10.5, - "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.16, - "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.21, - "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.74 + "default": 9.401, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.401, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 9.876, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.415 } } } diff --git a/shared-data/python/opentrons_shared_data/pipette/pipette_definition.py b/shared-data/python/opentrons_shared_data/pipette/pipette_definition.py index 61e8f94143e..f37051b69a2 100644 --- a/shared-data/python/opentrons_shared_data/pipette/pipette_definition.py +++ b/shared-data/python/opentrons_shared_data/pipette/pipette_definition.py @@ -8,7 +8,7 @@ # The highest and lowest existing overlap version values. TIP_OVERLAP_VERSION_MINIMUM = 0 -TIP_OVERLAP_VERSION_MAXIMUM = 1 +TIP_OVERLAP_VERSION_MAXIMUM = 3 PLUNGER_CURRENT_MINIMUM = 0.1 PLUNGER_CURRENT_MAXIMUM = 1.5 From 29ebcba7f9a1aaac5fb5f57c857c0253d6914731 Mon Sep 17 00:00:00 2001 From: Ryan Howard Date: Tue, 16 Jul 2024 08:59:05 -0400 Subject: [PATCH 25/78] fix(hardware_control): Fixing liquid probe bugs found in end to end testing (#15619) # Overview These changes came from testing the end to end implementation of liquid level detection on the ot3 and fixing the bugs that came up. # Test Plan Changed the unit tests to match new functionality. Tested all new methods for liquid level detection on the ot3. # Changelog Changes include: * Deleting unnecessary checks in ot3controller.py * Editing ot3api.py to issue the correct probe movements * Standardizing the attribute names for liquid_presence_detection * Conditionally adding a require_liquid_presence command before aspirates * Redoing movement in liquid_probe so that it actually takes a sensible path * Editing tests accordingly # Review requests We are aware of a bug that even though we handle LLD exceptions at the protocol_context level, protocol_engine commands will still be marked as failed, so all future commands will fail. As far as I know for this PR, this only affects detect_liquid_presence on a well with no liquid. Another PR will be coming soon that addresses this. # Risk assessment Medium? I'm not sure. --------- Co-authored-by: aaron-kulkarni Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: aaron-kulkarni <107003644+aaron-kulkarni@users.noreply.github.com> --- api/src/opentrons/config/defaults_ot3.py | 6 ++-- .../backends/ot3controller.py | 12 -------- api/src/opentrons/hardware_control/ot3api.py | 11 +++++--- .../protocol_api/core/engine/instrument.py | 12 +++++--- .../opentrons/protocol_api/core/instrument.py | 8 ++++-- .../core/legacy/legacy_instrument_core.py | 8 ++++-- .../legacy_instrument_core.py | 8 ++++-- .../protocol_api/instrument_context.py | 28 ++++++++++--------- .../protocol_engine/commands/liquid_probe.py | 12 ++------ .../protocol_engine/execution/pipetting.py | 10 +++---- .../protocol_engine/state/pipettes.py | 2 +- .../hardware_control/test_ot3_api.py | 5 +--- .../core/engine/test_instrument_core.py | 24 +++------------- .../protocol_api/test_instrument_context.py | 18 +++++++++--- .../commands/test_liquid_probe.py | 16 ++--------- 15 files changed, 81 insertions(+), 99 deletions(-) diff --git a/api/src/opentrons/config/defaults_ot3.py b/api/src/opentrons/config/defaults_ot3.py index e6d91cab081..b09235ce35b 100644 --- a/api/src/opentrons/config/defaults_ot3.py +++ b/api/src/opentrons/config/defaults_ot3.py @@ -23,8 +23,8 @@ DEFAULT_MODULE_OFFSET = [0.0, 0.0, 0.0] DEFAULT_LIQUID_PROBE_SETTINGS: Final[LiquidProbeSettings] = LiquidProbeSettings( - mount_speed=10, - plunger_speed=5, + mount_speed=5, + plunger_speed=20, plunger_impulse_time=0.2, sensor_threshold_pascals=15, output_option=OutputOptions.sync_buffer_to_csv, @@ -328,7 +328,7 @@ def _build_default_liquid_probe( or output_option is OutputOptions.stream_to_csv ): data_files = _build_log_files_with_default( - from_conf.get("data_files", {}), default.data_files + from_conf.get("data_files", None), default.data_files ) return LiquidProbeSettings( mount_speed=from_conf.get("mount_speed", default.mount_speed), diff --git a/api/src/opentrons/hardware_control/backends/ot3controller.py b/api/src/opentrons/hardware_control/backends/ot3controller.py index 766125ea1ea..5bdb1621066 100644 --- a/api/src/opentrons/hardware_control/backends/ot3controller.py +++ b/api/src/opentrons/hardware_control/backends/ot3controller.py @@ -194,7 +194,6 @@ PipetteLiquidNotFoundError, CommunicationError, PythonException, - UnsupportedHardwareCommand, ) from .subsystem_manager import SubsystemManager @@ -1363,17 +1362,6 @@ async def liquid_probe( probe: InstrumentProbeType = InstrumentProbeType.PRIMARY, force_both_sensors: bool = False, ) -> float: - if output_option == OutputOptions.sync_buffer_to_csv: - if ( - self._subsystem_manager.device_info[ - SubSystem.of_mount(mount) - ].revision.tertiary - != "1" - ): - raise UnsupportedHardwareCommand( - "Liquid Probe not supported on this pipette firmware" - ) - head_node = axis_to_node(Axis.by_mount(mount)) tool = sensor_node_for_pipette(OT3Mount(mount.value)) csv_output = bool(output_option.value & OutputOptions.stream_to_csv.value) diff --git a/api/src/opentrons/hardware_control/ot3api.py b/api/src/opentrons/hardware_control/ot3api.py index 1c03b49fc6c..32f640269fe 100644 --- a/api/src/opentrons/hardware_control/ot3api.py +++ b/api/src/opentrons/hardware_control/ot3api.py @@ -2636,10 +2636,13 @@ async def liquid_probe( pos = await self.gantry_position(checked_mount, refresh=True) while (probe_start_pos.z - pos.z) < max_z_dist: # safe distance so we don't accidentally aspirate liquid if we're already close to liquid - safe_plunger_pos = pos._replace(z=(pos.z + 2)) + safe_plunger_pos = top_types.Point(pos.x, pos.y, pos.z + 2) # overlap amount we want to use between passes - pass_start_pos = pos._replace(z=(pos.z + 0.5)) - + pass_start_pos = top_types.Point(pos.x, pos.y, pos.z + 0.5) + max_z_time = ( + max_z_dist - (probe_start_pos.z - safe_plunger_pos.z) + ) / probe_settings.mount_speed + pass_travel = min(max_z_time * probe_settings.plunger_speed, p_travel) # Prep the plunger await self.move_to(checked_mount, safe_plunger_pos) if probe_settings.aspirate_while_sensing: @@ -2655,7 +2658,7 @@ async def liquid_probe( checked_mount, probe_settings, probe if probe else InstrumentProbeType.PRIMARY, - p_travel, + pass_travel, ) # if we made it here without an error we found the liquid error = None diff --git a/api/src/opentrons/protocol_api/core/engine/instrument.py b/api/src/opentrons/protocol_api/core/engine/instrument.py index fcf853067fc..71bc5784671 100644 --- a/api/src/opentrons/protocol_api/core/engine/instrument.py +++ b/api/src/opentrons/protocol_api/core/engine/instrument.py @@ -838,13 +838,12 @@ def retract(self) -> None: z_axis = self._engine_client.state.pipettes.get_z_axis(self._pipette_id) self._engine_client.execute_command(cmd.HomeParams(axes=[z_axis])) - def liquid_probe_with_recovery(self, well_core: WellCore) -> None: + def liquid_probe_with_recovery(self, well_core: WellCore, loc: Location) -> None: labware_id = well_core.labware_id well_name = well_core.get_name() well_location = WellLocation( origin=WellOrigin.TOP, offset=WellOffset(x=0, y=0, z=0) ) - self._engine_client.execute_command( cmd.LiquidProbeParams( labwareId=labware_id, @@ -854,13 +853,16 @@ def liquid_probe_with_recovery(self, well_core: WellCore) -> None: ) ) - def liquid_probe_without_recovery(self, well_core: WellCore) -> float: + self._protocol_core.set_last_location(location=loc, mount=self.get_mount()) + + def liquid_probe_without_recovery( + self, well_core: WellCore, loc: Location + ) -> float: labware_id = well_core.labware_id well_name = well_core.get_name() well_location = WellLocation( origin=WellOrigin.TOP, offset=WellOffset(x=0, y=0, z=0) ) - result = self._engine_client.execute_command_without_recovery( cmd.LiquidProbeParams( labwareId=labware_id, @@ -870,5 +872,7 @@ def liquid_probe_without_recovery(self, well_core: WellCore) -> float: ) ) + self._protocol_core.set_last_location(location=loc, mount=self.get_mount()) + if result is not None and isinstance(result, LiquidProbeResult): return result.z_position diff --git a/api/src/opentrons/protocol_api/core/instrument.py b/api/src/opentrons/protocol_api/core/instrument.py index 4108753a325..2a66b8e513f 100644 --- a/api/src/opentrons/protocol_api/core/instrument.py +++ b/api/src/opentrons/protocol_api/core/instrument.py @@ -306,12 +306,16 @@ def retract(self) -> None: ... @abstractmethod - def liquid_probe_with_recovery(self, well_core: WellCoreType) -> None: + def liquid_probe_with_recovery( + self, well_core: WellCoreType, loc: types.Location + ) -> None: """Do a liquid probe to detect the presence of liquid in the well.""" ... @abstractmethod - def liquid_probe_without_recovery(self, well_core: WellCoreType) -> float: + def liquid_probe_without_recovery( + self, well_core: WellCoreType, loc: types.Location + ) -> float: """Do a liquid probe to find the level of the liquid in the well.""" ... diff --git a/api/src/opentrons/protocol_api/core/legacy/legacy_instrument_core.py b/api/src/opentrons/protocol_api/core/legacy/legacy_instrument_core.py index 2d7dfb87da8..adcc5137f93 100644 --- a/api/src/opentrons/protocol_api/core/legacy/legacy_instrument_core.py +++ b/api/src/opentrons/protocol_api/core/legacy/legacy_instrument_core.py @@ -566,10 +566,14 @@ def retract(self) -> None: """Retract this instrument to the top of the gantry.""" self._protocol_interface.get_hardware.retract(self._mount) # type: ignore [attr-defined] - def liquid_probe_with_recovery(self, well_core: WellCore) -> None: + def liquid_probe_with_recovery( + self, well_core: WellCore, loc: types.Location + ) -> None: """This will never be called because it was added in API 2.20.""" assert False, "liquid_probe_with_recovery only supported in API 2.20 & later" - def liquid_probe_without_recovery(self, well_core: WellCore) -> float: + def liquid_probe_without_recovery( + self, well_core: WellCore, loc: types.Location + ) -> float: """This will never be called because it was added in API 2.20.""" assert False, "liquid_probe_without_recovery only supported in API 2.20 & later" diff --git a/api/src/opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py b/api/src/opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py index 4734d40949d..9938fd35ec7 100644 --- a/api/src/opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py +++ b/api/src/opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py @@ -484,10 +484,14 @@ def retract(self) -> None: """Retract this instrument to the top of the gantry.""" self._protocol_interface.get_hardware.retract(self._mount) # type: ignore [attr-defined] - def liquid_probe_with_recovery(self, well_core: WellCore) -> None: + def liquid_probe_with_recovery( + self, well_core: WellCore, loc: types.Location + ) -> None: """This will never be called because it was added in API 2.20.""" assert False, "liquid_probe_with_recovery only supported in API 2.20 & later" - def liquid_probe_without_recovery(self, well_core: WellCore) -> float: + def liquid_probe_without_recovery( + self, well_core: WellCore, loc: types.Location + ) -> float: """This will never be called because it was added in API 2.20.""" assert False, "liquid_probe_without_recovery only supported in API 2.20 & later" diff --git a/api/src/opentrons/protocol_api/instrument_context.py b/api/src/opentrons/protocol_api/instrument_context.py index 10aa9933346..c2bf863cb19 100644 --- a/api/src/opentrons/protocol_api/instrument_context.py +++ b/api/src/opentrons/protocol_api/instrument_context.py @@ -9,7 +9,6 @@ CommandParameterLimitViolated, UnexpectedTipRemovalError, ) -from opentrons.protocol_engine.errors.exceptions import WellDoesNotExistError from opentrons.legacy_broker import LegacyBroker from opentrons.hardware_control.dev_types import PipetteDict from opentrons import types @@ -226,7 +225,6 @@ def aspirate( well: Optional[labware.Well] = None move_to_location: types.Location - last_location = self._get_last_location_by_api_version() try: target = validation.validate_location( @@ -264,6 +262,14 @@ def aspirate( c_vol = self._core.get_available_volume() if not volume else volume flow_rate = self._core.get_aspirate_flow_rate(rate) + if ( + self.api_version >= APIVersion(2, 20) + and well is not None + and self.liquid_presence_detection + ): + self.require_liquid_presence(well=well) + self.prepare_to_aspirate() + with publisher.publish_context( broker=self.broker, command=cmds.aspirate( @@ -2105,11 +2111,11 @@ def detect_liquid_presence(self, well: labware.Well) -> bool: :returns: A boolean. """ - if not isinstance(well, labware.Well): - raise WellDoesNotExistError("You must provide a valid well to check.") + loc = well.top() try: - self._core.liquid_probe_without_recovery(well._core) + self._core.liquid_probe_without_recovery(well._core, loc) except ProtocolCommandFailedError as e: + # if we handle the error, we change the protocl state from error to valid if isinstance(e.original_error, LiquidNotFoundError): return False raise e @@ -2122,10 +2128,8 @@ def require_liquid_presence(self, well: labware.Well) -> None: :returns: None. """ - if not isinstance(well, labware.Well): - raise WellDoesNotExistError("You must provide a valid well to check.") - - self._core.liquid_probe_with_recovery(well._core) + loc = well.top() + self._core.liquid_probe_with_recovery(well._core, loc) @requires_version(2, 20) def measure_liquid_height(self, well: labware.Well) -> float: @@ -2137,8 +2141,6 @@ def measure_liquid_height(self, well: labware.Well) -> float: This is intended for Opentrons internal use only and is not a guaranteed API. """ - if not isinstance(well, labware.Well): - raise WellDoesNotExistError("You must provide a valid well to check.") - - height = self._core.liquid_probe_without_recovery(well._core) + loc = well.top() + height = self._core.liquid_probe_without_recovery(well._core, loc) return height diff --git a/api/src/opentrons/protocol_engine/commands/liquid_probe.py b/api/src/opentrons/protocol_engine/commands/liquid_probe.py index 46606415792..abac5537e73 100644 --- a/api/src/opentrons/protocol_engine/commands/liquid_probe.py +++ b/api/src/opentrons/protocol_engine/commands/liquid_probe.py @@ -10,7 +10,7 @@ from pydantic import Field -from ..types import CurrentWell, DeckPoint +from ..types import DeckPoint from .pipetting_common import ( LiquidNotFoundError, LiquidNotFoundErrorInternalData, @@ -77,8 +77,9 @@ async def execute(self, params: LiquidProbeParams) -> _ExecuteReturn: Return the z-position of the found liquid. Raises: - TipNotAttachedError: if there is not tip attached to the pipette + TipNotAttachedError: if there is no tip attached to the pipette MustHomeError: if the plunger is not in a valid position + TipNotEmptyError: if the tip starts with liquid in it LiquidNotFoundError: if liquid is not found during the probe process. """ pipette_id = params.pipetteId @@ -99,19 +100,12 @@ async def execute(self, params: LiquidProbeParams) -> _ExecuteReturn: message="Current position of pipette is invalid. Please home." ) - current_well = CurrentWell( - pipette_id=pipette_id, - labware_id=labware_id, - well_name=well_name, - ) - # liquid_probe process start position position = await self._movement.move_to_well( pipette_id=pipette_id, labware_id=labware_id, well_name=well_name, well_location=params.wellLocation, - current_well=current_well, ) try: diff --git a/api/src/opentrons/protocol_engine/execution/pipetting.py b/api/src/opentrons/protocol_engine/execution/pipetting.py index ccf515e1226..24e45f6c3ad 100644 --- a/api/src/opentrons/protocol_engine/execution/pipetting.py +++ b/api/src/opentrons/protocol_engine/execution/pipetting.py @@ -30,7 +30,7 @@ class PipettingHandler(TypingProtocol): """Liquid handling commands.""" def get_is_empty(self, pipette_id: str) -> bool: - """Get whether a pipette has a working volume equal to 0.""" + """Get whether a pipette has an aspirated volume equal to 0.""" def get_is_ready_to_aspirate(self, pipette_id: str) -> bool: """Get whether a pipette is ready to aspirate.""" @@ -81,8 +81,8 @@ def __init__(self, state_view: StateView, hardware_api: HardwareControlAPI) -> N self._hardware_api = hardware_api def get_is_empty(self, pipette_id: str) -> bool: - """Get whether a pipette has a working volume equal to 0.""" - return self._state_view.pipettes.get_working_volume(pipette_id) == 0 + """Get whether a pipette has an aspirated volume equal to 0.""" + return self._state_view.pipettes.get_aspirated_volume(pipette_id) == 0 def get_is_ready_to_aspirate(self, pipette_id: str) -> bool: """Get whether a pipette is ready to aspirate.""" @@ -234,8 +234,8 @@ def __init__( self._state_view = state_view def get_is_empty(self, pipette_id: str) -> bool: - """Get whether a pipette has a working volume equal to 0.""" - return self._state_view.pipettes.get_working_volume(pipette_id) == 0 + """Get whether a pipette has an aspirated volume equal to 0.""" + return self._state_view.pipettes.get_aspirated_volume(pipette_id) == 0 def get_is_ready_to_aspirate(self, pipette_id: str) -> bool: """Get whether a pipette is ready to aspirate.""" diff --git a/api/src/opentrons/protocol_engine/state/pipettes.py b/api/src/opentrons/protocol_engine/state/pipettes.py index 748786d9bda..cab42ac7238 100644 --- a/api/src/opentrons/protocol_engine/state/pipettes.py +++ b/api/src/opentrons/protocol_engine/state/pipettes.py @@ -637,7 +637,7 @@ def get_current_tip_lld_settings(self, pipette_id: str) -> float: if attached_tip is None or attached_tip.volume is None: return 0 lld_settings = self.get_pipette_lld_settings(pipette_id) - tipVolume = str(attached_tip.volume) + tipVolume = "t" + str(int(attached_tip.volume)) if ( lld_settings is None or lld_settings[tipVolume] is None diff --git a/api/tests/opentrons/hardware_control/test_ot3_api.py b/api/tests/opentrons/hardware_control/test_ot3_api.py index fe9f4085436..020e3d0dc3c 100644 --- a/api/tests/opentrons/hardware_control/test_ot3_api.py +++ b/api/tests/opentrons/hardware_control/test_ot3_api.py @@ -806,9 +806,6 @@ async def test_liquid_probe( ) await ot3_hardware.cache_pipette(mount, instr_data, None) pipette = ot3_hardware.hardware_pipettes[mount.to_mount()] - plunger_positions = ot3_hardware._pipette_handler.get_pipette( - mount - ).plunger_positions assert pipette await ot3_hardware.add_tip(mount, 100) @@ -841,7 +838,7 @@ async def test_liquid_probe( mock_move_to_plunger_bottom.assert_called_once() mock_liquid_probe.assert_called_once_with( mount, - plunger_positions.bottom - plunger_positions.top, + 3.0, fake_settings_aspirate.mount_speed, (fake_settings_aspirate.plunger_speed * -1), fake_settings_aspirate.sensor_threshold_pascals, diff --git a/api/tests/opentrons/protocol_api/core/engine/test_instrument_core.py b/api/tests/opentrons/protocol_api/core/engine/test_instrument_core.py index 0405d939abe..6baba13757c 100644 --- a/api/tests/opentrons/protocol_api/core/engine/test_instrument_core.py +++ b/api/tests/opentrons/protocol_api/core/engine/test_instrument_core.py @@ -1,7 +1,6 @@ """Test for the ProtocolEngine-based instrument API core.""" from typing import cast, Optional, Union -from opentrons.protocol_engine.commands.liquid_probe import LiquidProbeResult from opentrons_shared_data.errors.exceptions import PipetteLiquidNotFoundError import pytest from decoy import Decoy @@ -1340,30 +1339,14 @@ def test_liquid_probe_without_recovery( ) ) ).then_raise(PipetteLiquidNotFoundError()) + loc = Location(Point(0, 0, 0), None) try: - subject.liquid_probe_without_recovery(well_core=well_core) + subject.liquid_probe_without_recovery(well_core=well_core, loc=loc) except PipetteLiquidNotFoundError: assert True else: assert False - decoy.reset() - - lpr = LiquidProbeResult(z_position=5.0) - decoy.when( - mock_engine_client.execute_command_without_recovery( - cmd.LiquidProbeParams( - pipetteId=subject.pipette_id, - wellLocation=WellLocation( - origin=WellOrigin.TOP, offset=WellOffset(x=0, y=0, z=0) - ), - wellName=well_core.get_name(), - labwareId=well_core.labware_id, - ) - ) - ).then_return(lpr) - assert subject.liquid_probe_without_recovery(well_core=well_core) == 5.0 - @pytest.mark.parametrize("version", versions_at_or_above(APIVersion(2, 20))) def test_liquid_probe_with_recovery( @@ -1377,7 +1360,8 @@ def test_liquid_probe_with_recovery( well_core = WellCore( name="my cool well", labware_id="123abc", engine_client=mock_engine_client ) - subject.liquid_probe_with_recovery(well_core=well_core) + loc = Location(Point(0, 0, 0), None) + subject.liquid_probe_with_recovery(well_core=well_core, loc=loc) decoy.verify( mock_engine_client.execute_command( cmd.LiquidProbeParams( diff --git a/api/tests/opentrons/protocol_api/test_instrument_context.py b/api/tests/opentrons/protocol_api/test_instrument_context.py index f25923acad1..bde90981c93 100644 --- a/api/tests/opentrons/protocol_api/test_instrument_context.py +++ b/api/tests/opentrons/protocol_api/test_instrument_context.py @@ -1295,7 +1295,9 @@ def test_detect_liquid_presence( message=f"{lnfe.errorType}: {lnfe.detail}", ) decoy.when( - mock_instrument_core.liquid_probe_without_recovery(mock_well._core) + mock_instrument_core.liquid_probe_without_recovery( + mock_well._core, mock_well.top() + ) ).then_raise(errorToRaise) result = subject.detect_liquid_presence(mock_well) assert isinstance(result, bool) @@ -1316,10 +1318,16 @@ def test_require_liquid_presence( original_error=lnfe, message=f"{lnfe.errorType}: {lnfe.detail}", ) - decoy.when(mock_instrument_core.liquid_probe_with_recovery(mock_well._core)) + decoy.when( + mock_instrument_core.liquid_probe_with_recovery( + mock_well._core, mock_well.top() + ) + ) subject.require_liquid_presence(mock_well) decoy.when( - mock_instrument_core.liquid_probe_with_recovery(mock_well._core) + mock_instrument_core.liquid_probe_with_recovery( + mock_well._core, mock_well.top() + ) ).then_raise(errorToRaise) with pytest.raises(ProtocolCommandFailedError) as pcfe: subject.require_liquid_presence(mock_well) @@ -1341,7 +1349,9 @@ def test_measure_liquid_height( message=f"{lnfe.errorType}: {lnfe.detail}", ) decoy.when( - mock_instrument_core.liquid_probe_without_recovery(mock_well._core) + mock_instrument_core.liquid_probe_without_recovery( + mock_well._core, mock_well.top() + ) ).then_raise(errorToRaise) with pytest.raises(ProtocolCommandFailedError) as pcfe: subject.measure_liquid_height(mock_well) diff --git a/api/tests/opentrons/protocol_engine/commands/test_liquid_probe.py b/api/tests/opentrons/protocol_engine/commands/test_liquid_probe.py index a6cea0ab40b..dddf1ca5e06 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_liquid_probe.py +++ b/api/tests/opentrons/protocol_engine/commands/test_liquid_probe.py @@ -33,7 +33,7 @@ PipettingHandler, ) from opentrons.protocol_engine.resources.model_utils import ModelUtils -from opentrons.protocol_engine.types import CurrentWell, LoadedPipette +from opentrons.protocol_engine.types import LoadedPipette @pytest.fixture @@ -58,7 +58,6 @@ async def test_liquid_probe_implementation_no_prep( ) -> None: """A Liquid Probe should have an execution implementation without preparing to aspirate.""" location = WellLocation(origin=WellOrigin.BOTTOM, offset=WellOffset(x=0, y=0, z=1)) - current_well = CurrentWell(pipette_id="abc", labware_id="123", well_name="A3") data = LiquidProbeParams( pipetteId="abc", @@ -75,7 +74,6 @@ async def test_liquid_probe_implementation_no_prep( labware_id="123", well_name="A3", well_location=location, - current_well=current_well, ), ).then_return(Point(x=1, y=2, z=3)) @@ -104,7 +102,6 @@ async def test_liquid_probe_implementation_with_prep( ) -> None: """A Liquid Probe should have an execution implementation with preparing to aspirate.""" location = WellLocation(origin=WellOrigin.TOP, offset=WellOffset(x=0, y=0, z=0)) - current_well = CurrentWell(pipette_id="abc", labware_id="123", well_name="A3") data = LiquidProbeParams( pipetteId="abc", @@ -122,11 +119,7 @@ async def test_liquid_probe_implementation_with_prep( ) decoy.when( await movement.move_to_well( - pipette_id="abc", - labware_id="123", - well_name="A3", - well_location=location, - current_well=current_well, + pipette_id="abc", labware_id="123", well_name="A3", well_location=location ), ).then_return(Point(x=1, y=2, z=3)) @@ -151,7 +144,6 @@ async def test_liquid_probe_implementation_with_prep( labware_id="123", well_name="A3", well_location=WellLocation(origin=WellOrigin.TOP), - current_well=current_well, ), ) @@ -170,9 +162,6 @@ async def test_liquid_not_found_error( well_location = WellLocation( origin=WellOrigin.BOTTOM, offset=WellOffset(x=0, y=0, z=1) ) - current_well = CurrentWell( - pipette_id=pipette_id, labware_id=labware_id, well_name=well_name - ) position = Point(x=1, y=2, z=3) @@ -196,7 +185,6 @@ async def test_liquid_not_found_error( labware_id=labware_id, well_name=well_name, well_location=well_location, - current_well=current_well, ), ).then_return(position) From ff15d00873a0569e2a504544c85b1f8d1ae500c9 Mon Sep 17 00:00:00 2001 From: Nick Diehl <47604184+ncdiehl11@users.noreply.github.com> Date: Tue, 16 Jul 2024 10:01:25 -0400 Subject: [PATCH 26/78] fix(app): fix links for setting up new robot (#15660) RQA-2844 On desktop devices page, clicking 'See how to set up a new robot' opens a modal with a link to support for setting up a new robot. Instead of linking to the OT-2 setup support page, we will now link to the robot's specific quickstart guide. --- .../localization/en/devices_landing.json | 7 ++++-- .../DevicesLanding/NewRobotSetupHelp.tsx | 14 ++++++++---- .../__tests__/NewRobotSetupHelp.test.tsx | 22 +++++++++++++------ 3 files changed, 30 insertions(+), 13 deletions(-) diff --git a/app/src/assets/localization/en/devices_landing.json b/app/src/assets/localization/en/devices_landing.json index b0a3307ace1..dfd92d23030 100644 --- a/app/src/assets/localization/en/devices_landing.json +++ b/app/src/assets/localization/en/devices_landing.json @@ -11,7 +11,7 @@ "forget_unavailable_robot": "Forget unavailable robot", "go_to_run": "Go to Run", "home_gantry": "Home gantry", - "how_to_setup_a_robot": "How to setup a new robot", + "how_to_setup_a_robot": "How to set up a new robot", "idle": "Idle", "if_connecting_via_usb": "If connecting via USB", "if_connecting_wirelessly": "If connecting wirelessly", @@ -23,11 +23,14 @@ "lights_on": "lights on", "loading": "loading", "looking_for_robots": "Looking for robots", - "ninety_six_mount": "Left + Right Mount", "make_sure_robot_is_connected": "Make sure the robot is connected to this computer", "modules": "Modules", + "new_robot_instructions": "When setting up a new Flex, follow the instructions on the touchscreen. For more information, consult the Quickstart Guide for your robot.", + "ninety_six_mount": "Left + Right Mount", "no_robots_found": "No robots found", "not_available": "Not available ({{count}})", + "opentrons_flex_quickstart_guide": "Opentrons Flex Quickstart Guide", + "ot2_quickstart_guide": "OT-2 Quickstart Guide", "refresh": "Refresh", "restart_the_app": "Restart the app", "restart_the_robot": "Restart the robot", diff --git a/app/src/pages/Devices/DevicesLanding/NewRobotSetupHelp.tsx b/app/src/pages/Devices/DevicesLanding/NewRobotSetupHelp.tsx index e13f142bb71..a453f07935c 100644 --- a/app/src/pages/Devices/DevicesLanding/NewRobotSetupHelp.tsx +++ b/app/src/pages/Devices/DevicesLanding/NewRobotSetupHelp.tsx @@ -16,7 +16,10 @@ import { getTopPortalEl } from '../../../App/portal' import { LegacyModal } from '../../../molecules/LegacyModal' import { ExternalLink } from '../../../atoms/Link/ExternalLink' -const NEW_ROBOT_SETUP_SUPPORT_ARTICLE_HREF = 'https://support.opentrons.com/s/' +const NEW_FLEX_SETUP_SUPPORT_ARTICLE_HREF = + 'https://insights.opentrons.com/hubfs/Products/Flex/Opentrons%20Flex%20Quickstart%20Guide.pdf' +const NEW_OT2_SETUP_SUPPORT_ARTICLE_HREF = + 'https://insights.opentrons.com/hubfs/Products/OT-2/OT-2%20Quick%20Start%20Guide.pdf' export function NewRobotSetupHelp(): JSX.Element { const { t } = useTranslation(['devices_landing', 'shared']) @@ -45,10 +48,13 @@ export function NewRobotSetupHelp(): JSX.Element { > - {t('use_usb_cable_for_new_robot')} + {t('new_robot_instructions')} - - {t('learn_more_about_new_robot_setup')} + + {t('opentrons_flex_quickstart_guide')} + + + {t('ot2_quickstart_guide')} { diff --git a/app/src/pages/Devices/DevicesLanding/__tests__/NewRobotSetupHelp.test.tsx b/app/src/pages/Devices/DevicesLanding/__tests__/NewRobotSetupHelp.test.tsx index 75933835202..fb4cbadfd42 100644 --- a/app/src/pages/Devices/DevicesLanding/__tests__/NewRobotSetupHelp.test.tsx +++ b/app/src/pages/Devices/DevicesLanding/__tests__/NewRobotSetupHelp.test.tsx @@ -24,7 +24,7 @@ describe('NewRobotSetupHelp', () => { const link = screen.getByText('See how to set up a new robot') fireEvent.click(link) - screen.getByText('How to setup a new robot') + screen.getByText('How to set up a new robot') const closeButton = screen.getByRole('button', { name: 'close' }) fireEvent.click(closeButton) @@ -36,22 +36,30 @@ describe('NewRobotSetupHelp', () => { const link = screen.getByText('See how to set up a new robot') fireEvent.click(link) - expect(screen.getByText('How to setup a new robot')).toBeInTheDocument() + expect(screen.getByText('How to set up a new robot')).toBeInTheDocument() const xButton = screen.getByRole('button', { name: '' }) fireEvent.click(xButton) - expect(screen.queryByText('How to setup a new robot')).toBeFalsy() + expect(screen.queryByText('How to set up a new robot')).toBeFalsy() }) it('renders the link and it has the correct href attribute', () => { render() const link = screen.getByText('See how to set up a new robot') fireEvent.click(link) - const targetLinkUrl = 'https://support.opentrons.com/s/' - const supportLink = screen.getByRole('link', { - name: 'Learn more about setting up a new robot', + const targetLinkUrlFlex = + 'https://insights.opentrons.com/hubfs/Products/Flex/Opentrons%20Flex%20Quickstart%20Guide.pdf' + const supportLinkFlex = screen.getByRole('link', { + name: 'Opentrons Flex Quickstart Guide', + }) + expect(supportLinkFlex).toHaveAttribute('href', targetLinkUrlFlex) + + const targetLinkUrlOt2 = + 'https://insights.opentrons.com/hubfs/Products/OT-2/OT-2%20Quick%20Start%20Guide.pdf' + const supportLinkOt2 = screen.getByRole('link', { + name: 'OT-2 Quickstart Guide', }) - expect(supportLink).toHaveAttribute('href', targetLinkUrl) + expect(supportLinkOt2).toHaveAttribute('href', targetLinkUrlOt2) }) }) From 6a68b88accbae416c9a69156e0fb28b121c74624 Mon Sep 17 00:00:00 2001 From: Seth Foster Date: Tue, 16 Jul 2024 10:06:38 -0400 Subject: [PATCH 27/78] refactor(app): TextOnlyButton (#15668) This is a new button that is in the app components. Its existence is a bit tricky. This is a kind of button that is just text of a particular color with particular hover states - no background color, no border, etc. It's meant to be the lowest-visual-priority interactable it can be. We have a component that does this, for the ODD only. It's the tertiaryLowLight state of SmallButton, which you can see in the ODD/Atoms/Buttons tree in storybook. This component definitely wasn't and isn't responsive to desktop styles. Separately, we heavily used buttons of this style in the modern wizards: PipetteWizard, GripperWizard, LPC. These wizards all define their content bodies via GenericWizardTile, whose stories are under App/Molecules/GenericWizardTile, and _that_ component had a completely separate definition of this button style as a styled-component inline in its implementation. Now, we want to use a button of that style on desktop, but in Error Recovery, which is a flow that doesn't use GenericWizardTile (because it needs the border look&feel of InterventionModal). So the button needs to be defined outside of GenericWizardTile, and also it needs to be responsive. That means we could do one of the following: 1. Make all the ODD buttons responsive, somehow dealing with the case(s) that have no semantic equivalent on desktop 2. Make just that one ODD button responsive, but for all of its possible style types, see above 3. Make just that one ODD button in that one style responsive, but then passing a different style as a prop would result in a weird render 4. Just make a new dang component I picked 4. I don't really feel that great about it. Also it's used in ER footer and so that should look correct now. ## Testing - ~Do GenericWizardTile stories still look right~ well, uh, [probably not that useful](https://s3-us-west-2.amazonaws.com/opentrons-components/edge/index.html?path=/docs/app-molecules-genericwizardtile--docs) - [x] Do the pipette flows still look right - [x] Does the go-back button in ER on both desktop and ODD look right --- app/src/atoms/buttons/TextOnlyButton.tsx | 50 +++++++++++++++++++ app/src/atoms/buttons/buttons.stories.tsx | 17 +++++++ app/src/atoms/buttons/index.ts | 1 + app/src/molecules/GenericWizardTile/index.tsx | 44 +++------------- .../shared/RecoveryFooterButtons.tsx | 13 +---- .../__tests__/RecoveryFooterButtons.test.tsx | 2 +- 6 files changed, 78 insertions(+), 49 deletions(-) create mode 100644 app/src/atoms/buttons/TextOnlyButton.tsx diff --git a/app/src/atoms/buttons/TextOnlyButton.tsx b/app/src/atoms/buttons/TextOnlyButton.tsx new file mode 100644 index 00000000000..45174218cd9 --- /dev/null +++ b/app/src/atoms/buttons/TextOnlyButton.tsx @@ -0,0 +1,50 @@ +import * as React from 'react' +import { Btn, StyledText, COLORS, RESPONSIVENESS } from '@opentrons/components' +import type { StyleProps } from '@opentrons/components' +import { css } from 'styled-components' + +const GO_BACK_BUTTON_STYLE = css` + color: ${COLORS.grey50}; + + &:hover { + opacity: 70%; + } + + @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { + &:hover { + opacity: 100%; + } + &:active { + opacity: 70%; + } + } +` + +const GO_BACK_BUTTON_DISABLED_STYLE = css` + color: ${COLORS.grey60}; +` + +interface TextOnlyButtonProps extends StyleProps { + onClick: () => void + buttonText: React.ReactNode + disabled?: boolean +} + +export function TextOnlyButton({ + onClick, + buttonText, + disabled = false, + ...styleProps +}: TextOnlyButtonProps): JSX.Element { + return ( + + + {buttonText} + + + ) +} diff --git a/app/src/atoms/buttons/buttons.stories.tsx b/app/src/atoms/buttons/buttons.stories.tsx index bc1c30a3614..ac730f2cd86 100644 --- a/app/src/atoms/buttons/buttons.stories.tsx +++ b/app/src/atoms/buttons/buttons.stories.tsx @@ -13,6 +13,7 @@ import { QuaternaryButton, SubmitPrimaryButton, ToggleButton, + TextOnlyButton, } from './index' import type { Story, Meta } from '@storybook/react' @@ -132,3 +133,19 @@ export const LongPress = LongPressButtonTemplate.bind({}) LongPress.args = { children: 'long press - 2sec / tap', } + +const TextOnlyButtonTemplate: Story< + React.ComponentProps +> = () => { + const [count, setCount] = React.useState(0) + return ( + { + setCount(prev => prev + 1) + }} + buttonText={`You clicked me ${count} times`} + /> + ) +} + +export const TextOnly = TextOnlyButtonTemplate.bind({}) diff --git a/app/src/atoms/buttons/index.ts b/app/src/atoms/buttons/index.ts index 00c18fc07b7..e5d63c0c767 100644 --- a/app/src/atoms/buttons/index.ts +++ b/app/src/atoms/buttons/index.ts @@ -8,3 +8,4 @@ export { SmallButton } from './SmallButton' export { SubmitPrimaryButton } from './SubmitPrimaryButton' export { TertiaryButton } from './TertiaryButton' export { ToggleButton } from './ToggleButton' +export { TextOnlyButton } from './TextOnlyButton' diff --git a/app/src/molecules/GenericWizardTile/index.tsx b/app/src/molecules/GenericWizardTile/index.tsx index bbeccd13192..1d231931e96 100644 --- a/app/src/molecules/GenericWizardTile/index.tsx +++ b/app/src/molecules/GenericWizardTile/index.tsx @@ -5,8 +5,6 @@ import { useTranslation } from 'react-i18next' import { ALIGN_CENTER, ALIGN_FLEX_END, - Btn, - COLORS, DIRECTION_COLUMN, DIRECTION_ROW, DISPLAY_INLINE_BLOCK, @@ -18,14 +16,13 @@ import { PrimaryButton, RESPONSIVENESS, SPACING, - LegacyStyledText, TYPOGRAPHY, useHoverTooltip, } from '@opentrons/components' import { getIsOnDevice } from '../../redux/config' import { Tooltip } from '../../atoms/Tooltip' import { NeedHelpLink } from '../../organisms/CalibrationPanels' -import { SmallButton } from '../../atoms/buttons' +import { SmallButton, TextOnlyButton } from '../../atoms/buttons' const ALIGN_BUTTONS = css` align-items: ${ALIGN_FLEX_END}; @@ -40,29 +37,7 @@ const CAPITALIZE_FIRST_LETTER_STYLE = css` text-transform: ${TYPOGRAPHY.textTransformCapitalize}; } ` -const GO_BACK_BUTTON_STYLE = css` - ${TYPOGRAPHY.pSemiBold}; - color: ${COLORS.grey50}; - &:hover { - opacity: 70%; - } - - @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { - font-weight: ${TYPOGRAPHY.fontWeightSemiBold}; - font-size: ${TYPOGRAPHY.fontSize22}; - &:hover { - opacity: 100%; - } - &:active { - opacity: 70%; - } - } -` -const GO_BACK_BUTTON_DISABLED_STYLE = css` - ${TYPOGRAPHY.pSemiBold}; - color: ${COLORS.grey60}; -` const Title = styled.h1` ${TYPOGRAPHY.h1Default}; @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { @@ -148,17 +123,12 @@ export function GenericWizardTile(props: GenericWizardTileProps): JSX.Element { {back != null ? ( - - - {t('go_back')} - - + ) : null} {getHelp != null ? : null} {proceed != null && proceedButton == null ? ( diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/RecoveryFooterButtons.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/RecoveryFooterButtons.tsx index 3c62246ab63..245755ddaee 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/RecoveryFooterButtons.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/RecoveryFooterButtons.tsx @@ -14,7 +14,7 @@ import { RESPONSIVENESS, } from '@opentrons/components' -import { SmallButton } from '../../../atoms/buttons' +import { SmallButton, TextOnlyButton } from '../../../atoms/buttons' interface RecoveryFooterButtonProps { primaryBtnOnClick: () => void @@ -52,16 +52,7 @@ function RecoveryGoBackButton({ const { t } = useTranslation('error_recovery') return showGoBackBtn ? ( - - - {t('go_back')} - + ) : ( diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/RecoveryFooterButtons.test.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/RecoveryFooterButtons.test.tsx index 42d6eb134de..f4921afd646 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/RecoveryFooterButtons.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/RecoveryFooterButtons.test.tsx @@ -38,7 +38,7 @@ describe('RecoveryFooterButtons', () => { const primaryBtns = screen.getAllByRole('button', { name: 'Continue' }) const secondaryBtns = screen.getAllByRole('button', { name: 'Go back' }) expect(primaryBtns.length).toBe(2) - expect(secondaryBtns.length).toBe(2) + expect(secondaryBtns.length).toBe(1) primaryBtns.forEach(btn => { mockPrimaryBtnOnClick.mockReset() From 6f1c11f1df21f4d5336f26f91e27be789788b338 Mon Sep 17 00:00:00 2001 From: Jamey Huffnagle Date: Tue, 16 Jul 2024 10:31:58 -0400 Subject: [PATCH 28/78] refactor(app): update desktop recovery intervention modal heights (#15673) --- .../ErrorRecoveryFlows/shared/RecoveryInterventionModal.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/RecoveryInterventionModal.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/RecoveryInterventionModal.tsx index 2f9c65a69bf..6b9ba60c43b 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/RecoveryInterventionModal.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/RecoveryInterventionModal.tsx @@ -52,10 +52,10 @@ const ODD_STYLE = ` ` const SMALL_MODAL_STYLE = css` - height: 25.25rem; + height: 22rem; ${ODD_STYLE} ` const LARGE_MODAL_STYLE = css` - height: 30rem; + height: 26.75rem; ${ODD_STYLE} ` From f78dfd43c02294a9ee6935df7c822fc540464441 Mon Sep 17 00:00:00 2001 From: Josh McVey Date: Tue, 16 Jul 2024 12:53:46 -0500 Subject: [PATCH 29/78] ci(app builds): fail fast false on build-app job matrix (#15675) ## Use [fail-fast:false](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstrategyfail-fast) > Currently, with Windows signing blocked, that job will always fail and then cancels the Mac and Linux builds if they have not completed. This does not unblock release but it makes sure the Mac and Linux build jobs are allowed to finish so that their assets are available as an attachment to the workflow. - `fail-fast: false` on the `build-app` job will stop the matrix cancelling active jobs upon any failure. - `deploy-release-app` will not run because it has `build-app` in its `needs` array. All matrix jobs must be successful for `build-app` to have a success outcome and satisfy the `needs` condition. --- .github/workflows/app-test-build-deploy.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/app-test-build-deploy.yaml b/.github/workflows/app-test-build-deploy.yaml index bc848da6a89..99c26379816 100644 --- a/.github/workflows/app-test-build-deploy.yaml +++ b/.github/workflows/app-test-build-deploy.yaml @@ -207,6 +207,7 @@ jobs: needs: [determine-build-type] if: needs.determine-build-type.outputs.variants != '[]' strategy: + fail-fast: false matrix: os: ['windows-2022', 'ubuntu-22.04', 'macos-latest'] variant: ${{fromJSON(needs.determine-build-type.outputs.variants)}} From 68f8844b33c1d461c156f4d3a02d3a8f2ebe5c9b Mon Sep 17 00:00:00 2001 From: Andy Sigler Date: Tue, 16 Jul 2024 13:59:23 -0400 Subject: [PATCH 30/78] chore(shared-data): P50S function for v3.6 pipettes is ready for production (#15677) --- .../single_channel/p50/default/3_6.json | 76 +++++++++---------- .../p50/lowVolumeDefault/3_6.json | 70 ++++++++--------- 2 files changed, 74 insertions(+), 72 deletions(-) diff --git a/shared-data/pipette/definitions/2/liquid/single_channel/p50/default/3_6.json b/shared-data/pipette/definitions/2/liquid/single_channel/p50/default/3_6.json index 7fe702bc97d..474bfca8df5 100644 --- a/shared-data/pipette/definitions/2/liquid/single_channel/p50/default/3_6.json +++ b/shared-data/pipette/definitions/2/liquid/single_channel/p50/default/3_6.json @@ -21,50 +21,50 @@ "aspirate": { "default": { "1": [ - [0.462, 0.5646, 0.0415], - [0.648, 0.3716, 0.1307], - [1.032, 0.2742, 0.1938], - [1.37, 0.1499, 0.3221], - [2.014, 0.1044, 0.3845], - [2.772, 0.0432, 0.5076], - [3.05, -0.0809, 0.8517], - [3.4, 0.0256, 0.5268], - [3.962, 0.0612, 0.4057], - [4.438, 0.0572, 0.4217], - [5.164, 0.018, 0.5955], - [5.966, 0.0095, 0.6393], - [7.38, 0.0075, 0.6514], - [9.128, 0.0049, 0.6705], - [10.16, 0.0033, 0.6854], - [13.812, 0.0024, 0.6948], - [27.204, 0.0008, 0.7165], - [50.614, 0.0002, 0.7328], - [53.046, -0.0005, 0.7676] + [0.5175, 0.200812, 0.234783], + [0.675, 0.306434, 0.180124], + [0.965, 0.202975, 0.249958], + [1.3175, 0.174205, 0.277721], + [1.9175, 0.098216, 0.377837], + [2.71, 0.059576, 0.451928], + [3.0675, -0.013969, 0.651237], + [3.4125, 0.02245, 0.539521], + [3.8375, 0.027713, 0.52156], + [4.39, 0.072919, 0.348082], + [5.14, 0.022924, 0.567563], + [5.97, 0.013633, 0.615316], + [7.415, 0.009346, 0.640908], + [9.1925, 0.005809, 0.667137], + [10.235, 0.003508, 0.688285], + [13.885, 0.001976, 0.703972], + [27.41, 0.000922, 0.718603], + [51.005, 0.000207, 0.738189], + [53.4675, -0.000417, 0.770047] ] } }, "dispense": { "default": { "1": [ - [0.462, 0.5646, 0.0415], - [0.648, 0.3716, 0.1307], - [1.032, 0.2742, 0.1938], - [1.37, 0.1499, 0.3221], - [2.014, 0.1044, 0.3845], - [2.772, 0.0432, 0.5076], - [3.05, -0.0809, 0.8517], - [3.4, 0.0256, 0.5268], - [3.962, 0.0612, 0.4057], - [4.438, 0.0572, 0.4217], - [5.164, 0.018, 0.5955], - [5.966, 0.0095, 0.6393], - [7.38, 0.0075, 0.6514], - [9.128, 0.0049, 0.6705], - [10.16, 0.0033, 0.6854], - [13.812, 0.0024, 0.6948], - [27.204, 0.0008, 0.7165], - [50.614, 0.0002, 0.7328], - [53.046, -0.0005, 0.7676] + [0.5175, 0.200812, 0.234783], + [0.675, 0.306434, 0.180124], + [0.965, 0.202975, 0.249958], + [1.3175, 0.174205, 0.277721], + [1.9175, 0.098216, 0.377837], + [2.71, 0.059576, 0.451928], + [3.0675, -0.013969, 0.651237], + [3.4125, 0.02245, 0.539521], + [3.8375, 0.027713, 0.52156], + [4.39, 0.072919, 0.348082], + [5.14, 0.022924, 0.567563], + [5.97, 0.013633, 0.615316], + [7.415, 0.009346, 0.640908], + [9.1925, 0.005809, 0.667137], + [10.235, 0.003508, 0.688285], + [13.885, 0.001976, 0.703972], + [27.41, 0.000922, 0.718603], + [51.005, 0.000207, 0.738189], + [53.4675, -0.000417, 0.770047] ] } }, diff --git a/shared-data/pipette/definitions/2/liquid/single_channel/p50/lowVolumeDefault/3_6.json b/shared-data/pipette/definitions/2/liquid/single_channel/p50/lowVolumeDefault/3_6.json index 33e1410ce99..e27cb962b70 100644 --- a/shared-data/pipette/definitions/2/liquid/single_channel/p50/lowVolumeDefault/3_6.json +++ b/shared-data/pipette/definitions/2/liquid/single_channel/p50/lowVolumeDefault/3_6.json @@ -21,46 +21,48 @@ "aspirate": { "default": { "1": [ - [0.11, 0.207815, 0.040201], - [0.65, 0.43933, 0.014735], - [1.04, 0.256666, 0.133466], - [1.67, 0.147126, 0.247388], - [2.45, 0.078774, 0.361536], - [2.89, 0.042387, 0.450684], - [3.2, 0.014781, 0.530464], - [3.79, 0.071819, 0.347944], - [4.22, 0.051592, 0.424605], - [4.93, 0.021219, 0.552775], - [5.81, 0.023461, 0.541725], - [7.21, 0.008959, 0.625982], - [8.93, 0.005456, 0.651235], - [10.0, 0.007108, 0.636489], - [13.61, 0.002591, 0.681656], - [26.99, 0.001163, 0.701094], - [45.25, 0.000207, 0.726887] + [0.541667, 0.302563, 0.190632], + [0.668333, 0.225982, 0.232113], + [1.028333, 0.255401, 0.212451], + [1.365, 0.149807, 0.321038], + [1.965, 0.091112, 0.401156], + [2.78, 0.060163, 0.46197], + [3.136667, -0.019962, 0.684718], + [3.471667, 0.014059, 0.578005], + [3.916667, 0.031571, 0.51721], + [4.458333, 0.069665, 0.368009], + [5.173333, 0.015715, 0.608534], + [5.99, 0.011271, 0.631526], + [7.39, 0.006269, 0.661487], + [9.18, 0.006559, 0.659347], + [10.193333, 0.001667, 0.704254], + [13.87, 0.002548, 0.695269], + [27.368333, 0.000899, 0.718151], + [45.855, 0.000182, 0.737772] ] } }, "dispense": { "default": { "1": [ - [0.11, 0.207815, 0.040201], - [0.65, 0.43933, 0.014735], - [1.04, 0.256666, 0.133466], - [1.67, 0.147126, 0.247388], - [2.45, 0.078774, 0.361536], - [2.89, 0.042387, 0.450684], - [3.2, 0.014781, 0.530464], - [3.79, 0.071819, 0.347944], - [4.22, 0.051592, 0.424605], - [4.93, 0.021219, 0.552775], - [5.81, 0.023461, 0.541725], - [7.21, 0.008959, 0.625982], - [8.93, 0.005456, 0.651235], - [10.0, 0.007108, 0.636489], - [13.61, 0.002591, 0.681656], - [26.99, 0.001163, 0.701094], - [45.25, 0.000207, 0.726887] + [0.541667, 0.302563, 0.190632], + [0.668333, 0.225982, 0.232113], + [1.028333, 0.255401, 0.212451], + [1.365, 0.149807, 0.321038], + [1.965, 0.091112, 0.401156], + [2.78, 0.060163, 0.46197], + [3.136667, -0.019962, 0.684718], + [3.471667, 0.014059, 0.578005], + [3.916667, 0.031571, 0.51721], + [4.458333, 0.069665, 0.368009], + [5.173333, 0.015715, 0.608534], + [5.99, 0.011271, 0.631526], + [7.39, 0.006269, 0.661487], + [9.18, 0.006559, 0.659347], + [10.193333, 0.001667, 0.704254], + [13.87, 0.002548, 0.695269], + [27.368333, 0.000899, 0.718151], + [45.855, 0.000182, 0.737772] ] } }, From 6b7ec80a6841d5f1634b6a2e438706de2df94f84 Mon Sep 17 00:00:00 2001 From: Nick Diehl <47604184+ncdiehl11@users.noreply.github.com> Date: Tue, 16 Jul 2024 15:20:21 -0400 Subject: [PATCH 31/78] feat(app, shared-data): implement CSV runtime parameter functionality on ODD (#15625) Closes AUTH-469 Update ODD ProtocolSetupParameters to accommodate file runtime parameter overrides. In the case that the selected file already exists on the robot server, we simply add the existing file ID to `runTimeParameterValues`. In the case that the file ID does not yet exist and we only have a filePath from the selected file on a USB drive, we first send the filePath to /dataFiles to create a file on the robot server, and send the returned ID with `runTimeParameterValues` --- .../ChooseProtocolSlideout/index.tsx | 2 +- .../organisms/ChooseRobotSlideout/index.tsx | 2 +- app/src/organisms/Devices/utils.ts | 6 + .../ProtocolSetupParameters/ChooseCsvFile.tsx | 16 ++- .../ProtocolSetupParameters.test.tsx | 5 + .../ProtocolSetupParameters/index.tsx | 128 ++++++++++++------ app/src/pages/ProtocolSetup/index.tsx | 8 +- shared-data/js/types.ts | 49 +++++-- 8 files changed, 154 insertions(+), 62 deletions(-) diff --git a/app/src/organisms/ChooseProtocolSlideout/index.tsx b/app/src/organisms/ChooseProtocolSlideout/index.tsx index 624064744fc..41e10b092ed 100644 --- a/app/src/organisms/ChooseProtocolSlideout/index.tsx +++ b/app/src/organisms/ChooseProtocolSlideout/index.tsx @@ -285,7 +285,7 @@ export function ChooseProtocolSlideoutComponent( } return parameter }) - setRunTimeParametersOverrides?.(clone) + setRunTimeParametersOverrides?.(clone as RunTimeParameter[]) }} title={runtimeParam.displayName} width="100%" diff --git a/app/src/organisms/ChooseRobotSlideout/index.tsx b/app/src/organisms/ChooseRobotSlideout/index.tsx index 95e1754f3b8..092c90e0eb9 100644 --- a/app/src/organisms/ChooseRobotSlideout/index.tsx +++ b/app/src/organisms/ChooseRobotSlideout/index.tsx @@ -392,7 +392,7 @@ export function ChooseRobotSlideout( } return parameter }) - setRunTimeParametersOverrides?.(clone) + setRunTimeParametersOverrides?.(clone as RunTimeParameter[]) }} title={runtimeParam.displayName} width="100%" diff --git a/app/src/organisms/Devices/utils.ts b/app/src/organisms/Devices/utils.ts index 50c2c5eeb19..01b6c704d89 100644 --- a/app/src/organisms/Devices/utils.ts +++ b/app/src/organisms/Devices/utils.ts @@ -92,6 +92,12 @@ export function getShowPipetteCalibrationWarning( ) } +/** + * prepares object to send to endpoints requiring RunTimeParameterCreateData + * @param {RunTimeParameter[]} runTimeParameters array of updated RunTimeParameter overrides + * @param {Record} [fileIdMap] mapping of variable name to file ID created and returned by robot server + * @returns {RunTimeParameterCreateData} object mapping variable name to value or file information + */ export function getRunTimeParameterValuesForRun( runTimeParameters: RunTimeParameter[], fileIdMap?: Record diff --git a/app/src/organisms/ProtocolSetupParameters/ChooseCsvFile.tsx b/app/src/organisms/ProtocolSetupParameters/ChooseCsvFile.tsx index 3d8a280d041..e45b2f1309f 100644 --- a/app/src/organisms/ProtocolSetupParameters/ChooseCsvFile.tsx +++ b/app/src/organisms/ProtocolSetupParameters/ChooseCsvFile.tsx @@ -21,7 +21,10 @@ import { getShellUpdateDataFiles } from '../../redux/shell' import { ChildNavigation } from '../ChildNavigation' import { EmptyFile } from './EmptyFile' -import type { CsvFileParameter, CsvFileFileType } from '@opentrons/shared-data' +import type { + CsvFileParameter, + CsvFileParameterFileData, +} from '@opentrons/shared-data' import type { CsvFileData } from '@opentrons/api-client' interface ChooseCsvFileProps { @@ -29,7 +32,7 @@ interface ChooseCsvFileProps { handleGoBack: () => void parameter: CsvFileParameter setParameter: ( - value: boolean | string | number | CsvFileFileType, + value: boolean | string | number | CsvFileParameterFileData, variableName: string ) => void } @@ -46,10 +49,11 @@ export function ChooseCsvFile({ const csvFilesOnRobot = useAllCsvFilesQuery(protocolId).data?.data.files ?? [] - const initialFileObject: CsvFileFileType = parameter.file ?? {} - const [csvFileSelected, setCsvFileSelected] = React.useState( - initialFileObject - ) + const initialFileObject: CsvFileParameterFileData = parameter.file ?? {} + const [ + csvFileSelected, + setCsvFileSelected, + ] = React.useState(initialFileObject) const handleBackButton = (): void => { if (!isEqual(csvFileSelected, initialFileObject)) { diff --git a/app/src/organisms/ProtocolSetupParameters/__tests__/ProtocolSetupParameters.test.tsx b/app/src/organisms/ProtocolSetupParameters/__tests__/ProtocolSetupParameters.test.tsx index 8a660fcf818..6cc96f3bf1e 100644 --- a/app/src/organisms/ProtocolSetupParameters/__tests__/ProtocolSetupParameters.test.tsx +++ b/app/src/organisms/ProtocolSetupParameters/__tests__/ProtocolSetupParameters.test.tsx @@ -6,6 +6,7 @@ import { useCreateProtocolAnalysisMutation, useCreateRunMutation, useHost, + useUploadCsvFileMutation, } from '@opentrons/react-api-client' import { COLORS } from '@opentrons/components' @@ -43,6 +44,7 @@ vi.mock('../../../redux/config') const MOCK_HOST_CONFIG: HostConfig = { hostname: 'MOCK_HOST' } const mockCreateProtocolAnalysis = vi.fn() +const mockUploadCsvFile = vi.fn() const mockCreateRun = vi.fn() const mockMostRecentAnalysis = ({ commands: [], @@ -79,6 +81,9 @@ describe('ProtocolSetupParameters', () => { when(vi.mocked(useCreateRunMutation)) .calledWith(expect.anything()) .thenReturn({ createRun: mockCreateRun } as any) + when(vi.mocked(useUploadCsvFileMutation)) + .calledWith(expect.anything(), expect.anything()) + .thenReturn({ uploadCsvFile: mockUploadCsvFile } as any) when(vi.mocked(useFeatureFlag)) .calledWith('enableCsvFile') .thenReturn(false) diff --git a/app/src/organisms/ProtocolSetupParameters/index.tsx b/app/src/organisms/ProtocolSetupParameters/index.tsx index 27cc1c54402..f62946dff13 100644 --- a/app/src/organisms/ProtocolSetupParameters/index.tsx +++ b/app/src/organisms/ProtocolSetupParameters/index.tsx @@ -5,6 +5,7 @@ import { useCreateProtocolAnalysisMutation, useCreateRunMutation, useHost, + useUploadCsvFileMutation, } from '@opentrons/react-api-client' import { useQueryClient } from 'react-query' import { @@ -18,7 +19,6 @@ import { sortRuntimeParameters, } from '@opentrons/shared-data' -import { ProtocolSetupStep } from '../../pages/ProtocolSetup' import { getRunTimeParameterValuesForRun } from '../Devices/utils' import { ChildNavigation } from '../ChildNavigation' import { ResetValuesModal } from './ResetValuesModal' @@ -27,7 +27,7 @@ import { ChooseNumber } from './ChooseNumber' import { ChooseCsvFile } from './ChooseCsvFile' import { useFeatureFlag } from '../../redux/config' import { useToaster } from '../ToasterOven' - +import { ProtocolSetupStep } from '../../pages/ProtocolSetup' import type { CompletedProtocolAnalysis, ChoiceParameter, @@ -35,9 +35,10 @@ import type { NumberParameter, RunTimeParameter, ValueRunTimeParameter, - CsvFileFileType, + CsvFileParameterFileData, } from '@opentrons/shared-data' -import type { LabwareOffsetCreateData } from '@opentrons/api-client' +import type { ProtocolSetupStepStatus } from '../../pages/ProtocolSetup' +import type { FileData, LabwareOffsetCreateData } from '@opentrons/api-client' interface ProtocolSetupParametersProps { protocolId: string @@ -85,15 +86,24 @@ export function ProtocolSetupParameters({ ({ ...parameter, value: parameter.default } as ValueRunTimeParameter) ) ) + const hasMissingFileParam = + runTimeParametersOverrides?.some( + parameter => + parameter.type === 'csv_file' && + ((parameter.file?.id == null && parameter.file?.file == null) || + parameter.file?.filePath == null) + ) ?? false const { makeSnackbar } = useToaster() const updateParameters = ( - value: boolean | string | number | CsvFileFileType, + value: boolean | string | number | CsvFileParameterFileData, variableName: string ): void => { const updatedParameters = runTimeParametersOverrides.map(parameter => { if (parameter.variableName === variableName) { - return { ...parameter, value } + return parameter.type === 'csv_file' + ? { ...parameter, file: value } + : { ...parameter, value } } return parameter }) @@ -138,6 +148,8 @@ export function ProtocolSetupParameters({ host ) + const { uploadCsvFile } = useUploadCsvFileMutation({}, host) + const { createRun, isLoading } = useCreateRunMutation({ onSuccess: data => { queryClient.invalidateQueries([host, 'runs']).catch((e: Error) => { @@ -146,11 +158,56 @@ export function ProtocolSetupParameters({ }, }) const handleConfirmValues = (): void => { - if ( - enableCsvFile && - mostRecentAnalysis?.result === 'parameter-value-required' - ) { - makeSnackbar(t('protocol_requires_csv') as string) + if (enableCsvFile) { + if (hasMissingFileParam) { + makeSnackbar(t('protocol_requires_csv') as string) + } else { + const dataFilesForProtocolMap = runTimeParametersOverrides.reduce< + Record + >((acc, parameter) => { + // create {variableName: FileData} map for sending to /dataFiles endpoint + if ( + parameter.type === 'csv_file' && + parameter.file?.id == null && + parameter.file?.file != null + ) { + return { [parameter.variableName]: parameter.file.file } + } else if ( + parameter.type === 'csv_file' && + parameter.file?.id == null && + parameter.file?.filePath != null + ) { + return { [parameter.variableName]: parameter.file.filePath } + } + return acc + }, {}) + void Promise.all( + Object.entries(dataFilesForProtocolMap).map(([key, fileData]) => { + const fileResponse = uploadCsvFile(fileData) + const varName = Promise.resolve(key) + return Promise.all([fileResponse, varName]) + }) + ).then(responseTuples => { + const mappedResolvedCsvVariableToFileId = responseTuples.reduce< + Record + >((acc, [uploadedFileResponse, variableName]) => { + return { ...acc, [variableName]: uploadedFileResponse.data.id } + }, {}) + const runTimeParameterValues = getRunTimeParameterValuesForRun( + runTimeParametersOverrides, + mappedResolvedCsvVariableToFileId + ) + createProtocolAnalysis({ + protocolKey: protocolId, + runTimeParameterValues, + }) + createRun({ + protocolId, + labwareOffsets, + runTimeParameterValues, + }) + }) + } } else { setStartSetup(true) createProtocolAnalysis({ @@ -191,14 +248,8 @@ export function ProtocolSetupParameters({ }} onClickButton={handleConfirmValues} buttonText={t('confirm_values')} - ariaDisabled={ - enableCsvFile && - mostRecentAnalysis?.result === 'parameter-value-required' - } - buttonIsDisabled={ - enableCsvFile && - mostRecentAnalysis?.result === 'parameter-value-required' - } + ariaDisabled={enableCsvFile && hasMissingFileParam} + buttonIsDisabled={enableCsvFile && hasMissingFileParam} iconName={isLoading || startSetup ? 'ot-spinner' : undefined} iconPlacement="startIcon" secondaryButtonProps={{ @@ -220,26 +271,19 @@ export function ProtocolSetupParameters({ > {sortRuntimeParameters(runTimeParametersOverrides).map( (parameter, index) => { - const detailLabelForCsv = - mostRecentAnalysis?.result === 'parameter-value-required' - ? t('required') - : parameter.displayName - - let setupStatus: 'ready' | 'not ready' | 'general' | 'inform' = - 'inform' - if ( - enableCsvFile && - parameter.type === 'csv_file' && - mostRecentAnalysis?.result === 'parameter-value-required' - ) { - setupStatus = 'not ready' - } - if ( - enableCsvFile && - parameter.type === 'csv_file' && - mostRecentAnalysis?.result === 'ok' - ) { - setupStatus = 'ready' + let detail: string = '' + let setupStatus: ProtocolSetupStepStatus + if (enableCsvFile && parameter.type === 'csv_file') { + if (parameter.file?.fileName == null) { + detail = t('required') + setupStatus = 'not ready' + } else { + detail = parameter.file.fileName + setupStatus = 'ready' + } + } else { + detail = formatRunTimeParameterValue(parameter, t) + setupStatus = 'inform' } return ( @@ -255,11 +299,7 @@ export function ProtocolSetupParameters({ onClickSetupStep={() => { handleSetParameter(parameter) }} - detail={ - enableCsvFile && parameter.type === 'csv_file' - ? detailLabelForCsv - : formatRunTimeParameterValue(parameter, t) - } + detail={detail} description={ parameter.type === 'csv_file' ? null : parameter.description } diff --git a/app/src/pages/ProtocolSetup/index.tsx b/app/src/pages/ProtocolSetup/index.tsx index 0956d9e9d5c..02ab6990070 100644 --- a/app/src/pages/ProtocolSetup/index.tsx +++ b/app/src/pages/ProtocolSetup/index.tsx @@ -102,9 +102,15 @@ import type { import type { ProtocolModuleInfo } from '../../organisms/Devices/ProtocolRun/utils/getProtocolModulesInfo' const FETCH_DURATION_MS = 5000 + +export type ProtocolSetupStepStatus = + | 'ready' + | 'not ready' + | 'general' + | 'inform' interface ProtocolSetupStepProps { onClickSetupStep: () => void - status: 'ready' | 'not ready' | 'general' | 'inform' + status: ProtocolSetupStepStatus title: string // first line of detail text detail?: string | null diff --git a/shared-data/js/types.ts b/shared-data/js/types.ts index 06a1765ca3a..c347042f4e4 100644 --- a/shared-data/js/types.ts +++ b/shared-data/js/types.ts @@ -625,25 +625,56 @@ export interface NumberParameter extends BaseRunTimeParameter { value: number } -export interface Choice { +export interface NumberChoice { displayName: string - value: number | boolean | string + value: number +} + +export interface BooleanChoice { + displayName: string + value: boolean +} + +export interface StringChoice { + displayName: string + value: string +} + +export type Choice = NumberChoice | BooleanChoice | StringChoice + +interface NumberChoiceParameter extends BaseRunTimeParameter { + type: NumberParameterType + choices: NumberChoice[] + default: number + value: number +} + +interface BooleanChoiceParameter extends BaseRunTimeParameter { + type: BooleanParameterType + choices: BooleanChoice[] + default: boolean + value: boolean } -export interface ChoiceParameter extends BaseRunTimeParameter { - type: NumberParameterType | BooleanParameterType | StringParameterType - choices: Choice[] - default: number | boolean | string - value: number | boolean | string +interface StringChoiceParameter extends BaseRunTimeParameter { + type: StringParameterType + choices: StringChoice[] + default: string + value: string } +export type ChoiceParameter = + | NumberChoiceParameter + | BooleanChoiceParameter + | StringChoiceParameter + interface BooleanParameter extends BaseRunTimeParameter { type: BooleanParameterType default: boolean value: boolean } -export interface CsvFileFileType { +export interface CsvFileParameterFileData { id?: string file?: File | null filePath?: string @@ -652,7 +683,7 @@ export interface CsvFileFileType { export interface CsvFileParameter extends BaseRunTimeParameter { type: CsvFileParameterType - file?: CsvFileFileType | null + file?: CsvFileParameterFileData | null } type NumberParameterType = 'int' | 'float' From 13f05cbd854bed44bfdeeee5d5f05248a26e4830 Mon Sep 17 00:00:00 2001 From: Jamey Huffnagle Date: Tue, 16 Jul 2024 15:25:45 -0400 Subject: [PATCH 32/78] feat(app): Error Recovery: Handle door state (#15679) Closes EXEC-565 Implement the "door open" view during Error Recovery for the desktop and ODD. --- .../localization/en/error_recovery.json | 7 +- .../assets/localization/en/run_details.json | 52 +++++----- .../Devices/ProtocolRun/ProtocolRunHeader.tsx | 17 +++- .../ErrorRecoveryWizard.tsx | 36 ++++--- .../ErrorRecoveryFlows/RecoveryDoorOpen.tsx | 97 +++++++++++++++++++ .../ErrorRecoveryFlows/RunPausedSplash.tsx | 15 ++- .../ErrorRecoveryFlows/__fixtures__/index.ts | 4 + .../__tests__/ErrorRecoveryFlows.test.tsx | 20 +++- .../__tests__/ErrorRecoveryWizard.test.tsx | 74 +++++++++++++- .../__tests__/RecoveryDoorOpen.test.tsx | 53 ++++++++++ .../__tests__/RunPausedSplash.test.tsx | 23 +++-- .../useRecoveryActionMutation.test.ts | 55 +++++++++++ .../hooks/__tests__/useShowDoorInfo.test.ts | 51 ++++++++++ .../ErrorRecoveryFlows/hooks/index.ts | 1 + .../ErrorRecoveryFlows/hooks/useERUtils.ts | 6 ++ .../hooks/useRecoveryActionMutation.ts | 20 ++++ .../hooks/useShowDoorInfo.ts | 38 ++++++++ .../organisms/ErrorRecoveryFlows/index.tsx | 22 +++-- .../shared/RecoveryFooterButtons.tsx | 22 ++++- .../shared/RecoveryInterventionModal.tsx | 6 +- .../__tests__/RecoveryFooterButtons.test.tsx | 16 +++ .../__tests__/RunningProtocol.test.tsx | 9 ++ app/src/pages/RunningProtocol/index.tsx | 5 +- 23 files changed, 573 insertions(+), 76 deletions(-) create mode 100644 app/src/organisms/ErrorRecoveryFlows/RecoveryDoorOpen.tsx create mode 100644 app/src/organisms/ErrorRecoveryFlows/__tests__/RecoveryDoorOpen.test.tsx create mode 100644 app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useRecoveryActionMutation.test.ts create mode 100644 app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useShowDoorInfo.test.ts create mode 100644 app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryActionMutation.ts create mode 100644 app/src/organisms/ErrorRecoveryFlows/hooks/useShowDoorInfo.ts diff --git a/app/src/assets/localization/en/error_recovery.json b/app/src/assets/localization/en/error_recovery.json index 3a6003fe638..c08db68b61f 100644 --- a/app/src/assets/localization/en/error_recovery.json +++ b/app/src/assets/localization/en/error_recovery.json @@ -10,6 +10,7 @@ "change_location": "Change location", "change_tip_pickup_location": "Change tip pick-up location", "choose_a_recovery_action": "Choose a recovery action", + "close_the_robot_door": "Close the robot door, and then resume the recovery action.", "confirm": "Confirm", "continue": "Continue", "continue_run_now": "Continue run now", @@ -23,8 +24,6 @@ "if_tips_are_attached": "If tips are attached, you can choose to blow out any aspirated liquid and drop tips before the run is terminated.", "ignore_all_errors_of_this_type": "Ignore all errors of this type", "ignore_error_and_skip": "Ignore error and skip to next step", - "skipping_to_step_succeeded": "Skipping to step {{step}} succeeded", - "retrying_step_succeeded": "Retrying step {{step}} succeeded", "ignore_only_this_error": "Ignore only this error", "ignore_similar_errors_later_in_run": "Ignore similar errors later in the run?", "launch_recovery_mode": "Launch Recovery Mode", @@ -44,12 +43,15 @@ "replace_tips_and_select_location": "It's best to replace tips and select the last location used for tip pickup.", "replace_used_tips_in_rack_location": "Replace used tips in rack location {{location}}", "replace_with_new_tip_rack": "Replace with new tip rack", + "resume": "Resume", "retry_now": "Retry now", "retry_step": "Retry step", "retry_with_new_tips": "Retry with new tips", "retry_with_same_tips": "Retry with same tips", + "retrying_step_succeeded": "Retrying step {{step}} succeeded", "return_to_menu": "Return to menu", "return_to_the_menu": "Return to the menu to choose how to proceed.", + "robot_door_is_open": "Robot door is open", "robot_will_not_check_for_liquid": "The robot will not check for liquid again. The run will continue from the next step.Close the robot door before proceeding.", "robot_will_retry_with_new_tips": "The robot will retry the failed step with the new tips.Close the robot door before proceeding.", "robot_will_retry_with_same_tips": "The robot will retry the failed step with the same tips.Close the robot door before proceeding.", @@ -60,6 +62,7 @@ "skip_to_next_step": "Skip to next step", "skip_to_next_step_new_tips": "Skip to next step with new tips", "skip_to_next_step_same_tips": "Skip to next step with same tips", + "skipping_to_step_succeeded": "Skipping to step {{step}} succeeded", "stand_back": "Stand back, robot is in motion", "stand_back_picking_up_tips": "Stand back, picking up tips", "stand_back_resuming": "Stand back, resuming current step", diff --git a/app/src/assets/localization/en/run_details.json b/app/src/assets/localization/en/run_details.json index 18127568031..2a3e27cf0f3 100644 --- a/app/src/assets/localization/en/run_details.json +++ b/app/src/assets/localization/en/run_details.json @@ -1,10 +1,11 @@ { "analysis_failure_on_robot": "An error occurred while attempting to analyze {{protocolName}} on {{robotName}}. Fix the following error and try running this protocol again.", "analyzing_on_robot": "Analyzing on robot", - "anticipated_step": "Anticipated steps", "anticipated": "Anticipated steps", + "anticipated_step": "Anticipated steps", "apply_stored_data": "Apply stored data", "apply_stored_labware_offset_data": "Apply stored Labware Offset data?", + "cancel_run": "Cancel run", "cancel_run_alert_info_flex": "Doing so will terminate this run and home your robot.", "cancel_run_alert_info_ot2": "Doing so will terminate this run, drop any attached tips in the trash container, and home your robot.", "cancel_run_and_restart": "Cancel the run and restart setup to edit", @@ -12,20 +13,19 @@ "cancel_run_modal_confirm": "Yes, cancel run", "cancel_run_modal_heading": "Are you sure you want to cancel?", "cancel_run_module_info": "Additionally, any hardware modules used within the protocol will remain active and maintain their current states until deactivated.", - "cancel_run": "Cancel run", - "canceling_run_dot": "canceling run...", "canceling_run": "Canceling Run", - "clear_protocol_to_make_available": "Clear protocol from robot to make it available.", + "canceling_run_dot": "canceling run...", "clear_protocol": "Clear protocol", - "close_door_to_resume": "Close robot door to resume run", + "clear_protocol_to_make_available": "Clear protocol from robot to make it available.", "close_door": "Close robot door", + "close_door_to_resume": "Close robot door to resume run", "closing_protocol": "Closing Protocol", - "comment_step": "Comment", "comment": "Comment", + "comment_step": "Comment", "complete_protocol_to_download": "Complete the protocol to download the run log", - "current_step_pause_timer": "Timer", - "current_step_pause": "Current Step - Paused by User", "current_step": "Current Step", + "current_step_pause": "Current Step - Paused by User", + "current_step_pause_timer": "Timer", "current_temperature": "Current: {{temperature}} °C", "custom_values": "Custom values", "data_out_of_date": "This data is likely out of date", @@ -37,31 +37,31 @@ "downloading_run_log": "Downloading run log", "drop_tip": "Dropping tip in {{well_name}} of {{labware}} in {{labware_location}}", "duration": "Duration", + "end": "End", "end_of_protocol": "End of protocol", "end_step_time": "End", - "end": "End", "error_info": "Error {{errorCode}}: {{errorType}}", "error_type": "Error: {{errorType}}", "failed_step": "Failed step", "final_step": "Final Step", "ignore_stored_data": "Ignore stored data", - "labware_offset_data": "labware offset data", "labware": "labware", + "labware_offset_data": "labware offset data", "left": "Left", "listed_values": "Listed values are view-only", + "load_labware_info_protocol_setup": "Load {{labware}} in {{module_name}} in Slot {{slot_name}}", + "load_labware_info_protocol_setup_adapter": "Load {{labware}} in {{adapter_name}} in Slot {{slot_name}}", "load_labware_info_protocol_setup_adapter_module": "Load {{labware}} in {{adapter_name}} in {{module_name}} in Slot {{slot_name}}", "load_labware_info_protocol_setup_adapter_off_deck": "Load {{labware}} in {{adapter_name}} off deck", - "load_labware_info_protocol_setup_adapter": "Load {{labware}} in {{adapter_name}} in Slot {{slot_name}}", "load_labware_info_protocol_setup_no_module": "Load {{labware}} in Slot {{slot_name}}", "load_labware_info_protocol_setup_off_deck": "Load {{labware}} off deck", "load_labware_info_protocol_setup_plural": "Load {{labware}} in {{module_name}}", - "load_labware_info_protocol_setup": "Load {{labware}} in {{module_name}} in Slot {{slot_name}}", "load_liquids_info_protocol_setup": "Load {{liquid}} into {{labware}}", - "load_module_protocol_setup_plural": "Load {{module}}", "load_module_protocol_setup": "Load {{module}} in Slot {{slot_name}}", + "load_module_protocol_setup_plural": "Load {{module}}", "load_pipette_protocol_setup": "Load {{pipette_name}} in {{mount_name}} Mount", - "loading_protocol": "Loading Protocol", "loading_data": "Loading data...", + "loading_protocol": "Loading Protocol", "location": "location", "module_controls": "Module Controls", "module_slot_number": "Slot {{slot_number}}", @@ -74,9 +74,9 @@ "not_started_yet": "Not started yet", "off_deck": "Off deck", "parameters": "Parameters", + "pause": "Pause", "pause_protocol": "Pause protocol", "pause_run": "Pause run", - "pause": "Pause", "paused_for": "Paused For", "pickup_tip": "Picking up tip from {{well_name}} of {{labware}} in {{labware_location}}", "plus_more": "+{{count}} more", @@ -99,32 +99,35 @@ "right": "Right", "robot_has_previous_offsets": "This robot has stored Labware Offset data from previous protocol runs. Do you want to apply that data to this protocol run? You can still adjust any offsets with Labware Position Check.", "robot_was_recalibrated": "This robot was recalibrated after this Labware Offset data was stored.", + "run": "Run", "run_again": "Run again", - "run_canceled_splash": "Run canceled", "run_canceled": "Run canceled.", - "run_complete_splash": "Run completed", + "run_canceled_splash": "Run canceled", "run_complete": "Run completed", + "run_complete_splash": "Run completed", "run_completed": "Run completed.", "run_cta_disabled": "Complete required steps on Protocol tab before starting the run", + "run_failed": "Run failed.", "run_failed_modal_body": "Error occurred when protocol was {{command}}", "run_failed_modal_header": "{{errorName}}: {{errorCode}} at protocol step {{count}}", "run_failed_modal_title": "Run failed", "run_failed_splash": "Run failed", - "run_failed": "Run failed.", "run_has_diverged_from_predicted": "Run has diverged from predicted state. Cannot anticipate new steps.", "run_preview": "Run Preview", "run_protocol": "Run Protocol", "run_status": "Status: {{status}}", "run_time": "Run Time", - "run": "Run", - "setup_incomplete": "Complete required steps in Setup tab", "setup": "Setup", + "setup_incomplete": "Complete required steps in Setup tab", "slot": "Slot {{slotName}}", + "start": "Start", "start_run": "Start run", "start_step_time": "Start", "start_time": "Start Time", - "start": "Start", + "status": "Status", "status_awaiting-recovery": "Awaiting recovery", + "status_awaiting-recovery-blocked-by-open-door": "Paused - door open", + "status_awaiting-recovery-paused": "Paused", "status_blocked-by-open-door": "Paused - door open", "status_failed": "Failed", "status_finishing": "Finishing", @@ -134,7 +137,6 @@ "status_stop-requested": "Stop requested", "status_stopped": "Canceled", "status_succeeded": "Completed", - "status": "Status", "step": "Step", "step_failed": "Step failed", "step_number": "Step {{step_number}}:", @@ -144,11 +146,11 @@ "temperature_not_available": "{{temperature_type}}: n/a", "thermocycler_error_tooltip": "Module encountered an anomaly, please contact support", "total_elapsed_time": "Total elapsed time", - "total_step_count_plural": "{{count}} steps total", "total_step_count": "{{count}} step total", + "total_step_count_plural": "{{count}} steps total", "unable_to_determine_steps": "Unable to determine steps", "view_analysis_error_details": "View error details", "view_current_step": "View current step", - "view_error_details": "View error details", - "view_error": "View error" + "view_error": "View error", + "view_error_details": "View error details" } diff --git a/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader.tsx b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader.tsx index 0ec4b35bcea..42b86e3d4d1 100644 --- a/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader.tsx +++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader.tsx @@ -298,6 +298,7 @@ export function ProtocolRunHeader({ {isERActive ? ( 0 && ( )} - {runStatus === RUN_STATUS_BLOCKED_BY_OPEN_DOOR || - runStatus === RUN_STATUS_AWAITING_RECOVERY_BLOCKED_BY_OPEN_DOOR ? ( + {runStatus === RUN_STATUS_BLOCKED_BY_OPEN_DOOR ? ( {t('close_door_to_resume')} @@ -545,11 +545,16 @@ const RUN_AGAIN_STATUSES: RunStatus[] = [ RUN_STATUS_FAILED, RUN_STATUS_SUCCEEDED, ] +const RECOVERY_STATUSES: RunStatus[] = [ + RUN_STATUS_AWAITING_RECOVERY, + RUN_STATUS_AWAITING_RECOVERY_BLOCKED_BY_OPEN_DOOR, + RUN_STATUS_AWAITING_RECOVERY_PAUSED, +] const DISABLED_STATUSES: RunStatus[] = [ RUN_STATUS_FINISHING, RUN_STATUS_STOP_REQUESTED, RUN_STATUS_BLOCKED_BY_OPEN_DOOR, - RUN_STATUS_AWAITING_RECOVERY_BLOCKED_BY_OPEN_DOOR, + ...RECOVERY_STATUSES, ] interface ActionButtonProps { runId: string @@ -633,7 +638,6 @@ function ActionButton(props: ActionButtonProps): JSX.Element { // For before running a protocol, "close door to begin". (isDoorOpen && runStatus !== RUN_STATUS_BLOCKED_BY_OPEN_DOOR && - runStatus !== RUN_STATUS_AWAITING_RECOVERY_BLOCKED_BY_OPEN_DOOR && runStatus != null && CANCELLABLE_STATUSES.includes(runStatus)) const robot = useRobot(robotName) @@ -694,7 +698,10 @@ function ActionButton(props: ActionButtonProps): JSX.Element { if (isProtocolAnalyzing) { buttonIconName = 'ot-spinner' buttonText = t('analyzing_on_robot') - } else if (runStatus === RUN_STATUS_RUNNING) { + } else if ( + runStatus === RUN_STATUS_RUNNING || + (runStatus != null && RECOVERY_STATUSES.includes(runStatus)) + ) { buttonIconName = 'pause' buttonText = t('pause_run') handleButtonClick = (): void => { diff --git a/app/src/organisms/ErrorRecoveryFlows/ErrorRecoveryWizard.tsx b/app/src/organisms/ErrorRecoveryFlows/ErrorRecoveryWizard.tsx index a8da5b77038..adadd2ad1ce 100644 --- a/app/src/organisms/ErrorRecoveryFlows/ErrorRecoveryWizard.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/ErrorRecoveryWizard.tsx @@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next' import { LegacyStyledText } from '@opentrons/components' import { RecoveryError } from './RecoveryError' +import { RecoveryDoorOpen } from './RecoveryDoorOpen' import { SelectRecoveryOption, RetryStep, @@ -27,11 +28,7 @@ import { RECOVERY_MAP } from './constants' import type { RobotType } from '@opentrons/shared-data' import type { RecoveryContentProps } from './types' -import type { - useRouteUpdateActions, - useRecoveryCommands, - ERUtilsResults, -} from './hooks' +import type { ERUtilsResults } from './hooks' import type { ErrorRecoveryFlowsProps } from '.' interface UseERWizardResult { @@ -60,6 +57,7 @@ export type ErrorRecoveryWizardProps = ErrorRecoveryFlowsProps & ERUtilsResults & { robotType: RobotType isOnDevice: boolean + isDoorOpen: boolean } export function ErrorRecoveryWizard( @@ -85,14 +83,13 @@ export function ErrorRecoveryWizard( export function ErrorRecoveryComponent( props: RecoveryContentProps ): JSX.Element { - const { route, step } = props.recoveryMap + const { recoveryMap, hasLaunchedRecovery, isDoorOpen, isOnDevice } = props + const { route, step } = recoveryMap const { t } = useTranslation('error_recovery') const { showModal, toggleModal } = useErrorDetailsModal() const buildTitleHeading = (): JSX.Element => { - const titleText = props.hasLaunchedRecovery - ? t('recovery_mode') - : t('cancel_run') + const titleText = hasLaunchedRecovery ? t('recovery_mode') : t('cancel_run') return {titleText} } @@ -102,9 +99,20 @@ export function ErrorRecoveryComponent( ) + // TODO(jh, 07-16-24): Revisit making RecoveryDoorOpen a route. + const buildInterventionContent = (): JSX.Element => { + if (isDoorOpen) { + return + } else { + return + } + } + const isLargeDesktopStyle = + !isDoorOpen && route === RECOVERY_MAP.DROP_TIP_FLOWS.ROUTE && step !== RECOVERY_MAP.DROP_TIP_FLOWS.STEPS.BEGIN_REMOVAL + return ( {showModal ? ( ) : null} - + {buildInterventionContent()} ) } @@ -169,6 +178,7 @@ export function ErrorRecoveryContent(props: RecoveryContentProps): JSX.Element { const buildIgnoreErrorSkipStep = (): JSX.Element => { return } + switch (props.recoveryMap.route) { case RECOVERY_MAP.OPTION_SELECTION.ROUTE: return buildSelectRecoveryOption() @@ -204,9 +214,9 @@ export function ErrorRecoveryContent(props: RecoveryContentProps): JSX.Element { } } interface UseInitialPipetteHomeParams { - hasLaunchedRecovery: boolean - recoveryCommands: ReturnType - routeUpdateActions: ReturnType + hasLaunchedRecovery: ErrorRecoveryWizardProps['hasLaunchedRecovery'] + recoveryCommands: ErrorRecoveryWizardProps['recoveryCommands'] + routeUpdateActions: ErrorRecoveryWizardProps['routeUpdateActions'] } // Home the Z-axis of all attached pipettes on Error Recovery launch. export function useInitialPipetteHome({ diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryDoorOpen.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryDoorOpen.tsx new file mode 100644 index 00000000000..2683a8e3abe --- /dev/null +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryDoorOpen.tsx @@ -0,0 +1,97 @@ +import * as React from 'react' +import { css } from 'styled-components' +import { useTranslation } from 'react-i18next' + +import { + COLORS, + DIRECTION_COLUMN, + Flex, + Icon, + StyledText, + SPACING, + ALIGN_CENTER, + JUSTIFY_END, + RESPONSIVENESS, + TEXT_ALIGN_CENTER, +} from '@opentrons/components' +import { RUN_STATUS_AWAITING_RECOVERY_BLOCKED_BY_OPEN_DOOR } from '@opentrons/api-client' + +import { RecoveryContentWrapper, RecoveryFooterButtons } from './shared' + +import type { RecoveryContentProps } from './types' + +export function RecoveryDoorOpen({ + recoveryActionMutationUtils, + runStatus, +}: RecoveryContentProps): JSX.Element { + const { + resumeRecovery, + isResumeRecoveryLoading, + } = recoveryActionMutationUtils + const { t } = useTranslation('error_recovery') + + return ( + + + + + + {t('robot_door_is_open')} + + + {t('close_the_robot_door')} + + + + + + + + ) +} + +const TEXT_STYLE = css` + flex-direction: ${DIRECTION_COLUMN}; + grid-gap: ${SPACING.spacing8}; + align-items: ${ALIGN_CENTER}; + text-align: ${TEXT_ALIGN_CENTER}; + + @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { + grid-gap: ${SPACING.spacing4}; + } +` + +const ICON_STYLE = css` + height: ${SPACING.spacing40}; + width: ${SPACING.spacing40}; + color: ${COLORS.yellow50}; + + @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { + height: ${SPACING.spacing60}; + width: ${SPACING.spacing60}; + } +` diff --git a/app/src/organisms/ErrorRecoveryFlows/RunPausedSplash.tsx b/app/src/organisms/ErrorRecoveryFlows/RunPausedSplash.tsx index 9b12e75e173..23633a8b20b 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RunPausedSplash.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RunPausedSplash.tsx @@ -39,9 +39,13 @@ import type { ErrorRecoveryFlowsProps } from '.' import type { ERUtilsResults } from './hooks' import { useHost } from '@opentrons/react-api-client' -export function useRunPausedSplash(showERWizard: boolean): boolean { - // Don't show the splash when the ER wizard is active. - return !showERWizard +export function useRunPausedSplash( + isOnDevice: boolean, + showERWizard: boolean +): boolean { + // Don't show the splash when desktop ER wizard is active, + // but always show it on the ODD (with or without the wizard rendered above it). + return !(!isOnDevice && showERWizard) } type RunPausedSplashProps = ERUtilsResults & { @@ -54,7 +58,7 @@ type RunPausedSplashProps = ERUtilsResults & { export function RunPausedSplash( props: RunPausedSplashProps ): JSX.Element | null { - const { toggleERWiz, routeUpdateActions, failedCommand } = props + const { isOnDevice, toggleERWiz, routeUpdateActions, failedCommand } = props const { t } = useTranslation('error_recovery') const errorKind = getErrorKind(failedCommand) const title = useErrorName(errorKind) @@ -88,7 +92,7 @@ export function RunPausedSplash( // TODO(jh 06-18-24): Instead of passing stepCount internally, we probably want to // pass it in as a prop to ErrorRecoveryFlows to ameliorate blippy "step = ? -> step = 24" behavior. - if (props.isOnDevice) { + if (isOnDevice) { return ( { beforeEach(() => { props = { + runStatus: RUN_STATUS_AWAITING_RECOVERY, failedCommand: mockFailedCommand, runId: 'MOCK_RUN_ID', isFlex: true, @@ -139,6 +144,7 @@ describe('ErrorRecovery', () => { }) vi.mocked(useRunPausedSplash).mockReturnValue(true) vi.mocked(useERUtils).mockReturnValue({ routeUpdateActions: {} } as any) + vi.mocked(useShowDoorInfo).mockReturnValue(false) }) it('renders the wizard when the wizard is toggled on', () => { @@ -146,6 +152,18 @@ describe('ErrorRecovery', () => { screen.getByText('MOCK WIZARD') }) + it('renders the wizard when isDoorOpen is true', () => { + vi.mocked(useShowDoorInfo).mockReturnValue(true) + vi.mocked(useERWizard).mockReturnValue({ + hasLaunchedRecovery: false, + toggleERWizard: () => Promise.resolve(), + showERWizard: false, + }) + + render(props) + screen.getByText('MOCK WIZARD') + }) + it('does not render the wizard when the wizard is toggled off', () => { vi.mocked(useERWizard).mockReturnValue({ hasLaunchedRecovery: true, diff --git a/app/src/organisms/ErrorRecoveryFlows/__tests__/ErrorRecoveryWizard.test.tsx b/app/src/organisms/ErrorRecoveryFlows/__tests__/ErrorRecoveryWizard.test.tsx index 9d9e7586d00..3c5174bf54d 100644 --- a/app/src/organisms/ErrorRecoveryFlows/__tests__/ErrorRecoveryWizard.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/__tests__/ErrorRecoveryWizard.test.tsx @@ -9,6 +9,7 @@ import { ErrorRecoveryContent, useInitialPipetteHome, useERWizard, + ErrorRecoveryComponent, } from '../ErrorRecoveryWizard' import { RECOVERY_MAP } from '../constants' import { @@ -25,14 +26,23 @@ import { } from '../RecoveryOptions' import { RecoveryInProgress } from '../RecoveryInProgress' import { RecoveryError } from '../RecoveryError' +import { RecoveryDoorOpen } from '../RecoveryDoorOpen' +import { useErrorDetailsModal, ErrorDetailsModal } from '../shared' import type { Mock } from 'vitest' vi.mock('../RecoveryOptions') vi.mock('../RecoveryInProgress') vi.mock('../RecoveryError') -vi.mock('../shared') - +vi.mock('../RecoveryDoorOpen') +vi.mock('../shared', async importOriginal => { + const actual = await importOriginal() + return { + ...actual, + useErrorDetailsModal: vi.fn(), + ErrorDetailsModal: vi.fn(), + } +}) describe('useERWizard', () => { it('has correct initial values', () => { const { result } = renderHook(() => useERWizard()) @@ -59,6 +69,65 @@ describe('useERWizard', () => { }) }) +const renderRecoveryComponent = ( + props: React.ComponentProps +) => { + return renderWithProviders(, { + i18nInstance: i18n, + })[0] +} + +describe('ErrorRecoveryComponent', () => { + let props: React.ComponentProps + + beforeEach(() => { + props = mockRecoveryContentProps + + vi.mocked(RecoveryDoorOpen).mockReturnValue( +
MOCK_RECOVERY_DOOR_OPEN
+ ) + vi.mocked(ErrorDetailsModal).mockReturnValue(
ERROR_DETAILS_MODAL
) + vi.mocked(useErrorDetailsModal).mockReturnValue({ + toggleModal: vi.fn(), + showModal: false, + }) + vi.mocked(SelectRecoveryOption).mockReturnValue( +
MOCK_SELECT_RECOVERY_OPTION
+ ) + }) + + it('renders appropriate header copy', () => { + renderRecoveryComponent(props) + + screen.getByText('View error details') + }) + + it('renders the error details modal when there is an error', () => { + vi.mocked(useErrorDetailsModal).mockReturnValue({ + toggleModal: vi.fn(), + showModal: true, + }) + + renderRecoveryComponent(props) + + screen.getByText('ERROR_DETAILS_MODAL') + }) + + it('renders the recovery door modal when isDoorOpen is true', () => { + props = { ...props, isDoorOpen: true } + + renderRecoveryComponent(props) + + screen.getByText('MOCK_RECOVERY_DOOR_OPEN') + }) + + it('renders recovery content when isDoorOpen is false', () => { + renderRecoveryComponent(props) + + screen.getByText('MOCK_SELECT_RECOVERY_OPTION') + }) +}) + const renderRecoveryContent = ( props: React.ComponentProps ) => { @@ -399,4 +468,3 @@ describe('useInitialPipetteHome', () => { }) }) }) -it.todo('add test for ErrorRecoveryComponent.') diff --git a/app/src/organisms/ErrorRecoveryFlows/__tests__/RecoveryDoorOpen.test.tsx b/app/src/organisms/ErrorRecoveryFlows/__tests__/RecoveryDoorOpen.test.tsx new file mode 100644 index 00000000000..e224d9cb33b --- /dev/null +++ b/app/src/organisms/ErrorRecoveryFlows/__tests__/RecoveryDoorOpen.test.tsx @@ -0,0 +1,53 @@ +import * as React from 'react' +import { describe, it, beforeEach, vi, expect } from 'vitest' +import { screen } from '@testing-library/react' +import { RUN_STATUS_AWAITING_RECOVERY_PAUSED } from '@opentrons/api-client' + +import { renderWithProviders } from '../../../__testing-utils__' +import { mockRecoveryContentProps } from '../__fixtures__' +import { i18n } from '../../../i18n' +import { RecoveryDoorOpen } from '../RecoveryDoorOpen' + +import type { Mock } from 'vitest' +import { clickButtonLabeled } from './util' + +const render = (props: React.ComponentProps) => { + return renderWithProviders(, { + i18nInstance: i18n, + })[0] +} + +describe('RecoveryDoorOpen', () => { + let props: React.ComponentProps + let mockResumeRecovery: Mock + + beforeEach(() => { + mockResumeRecovery = vi.fn() + props = { + ...mockRecoveryContentProps, + recoveryActionMutationUtils: { + resumeRecovery: mockResumeRecovery, + isResumeRecoveryLoading: false, + }, + runStatus: RUN_STATUS_AWAITING_RECOVERY_PAUSED, + } + }) + + it('renders the correct content', () => { + render(props) + + screen.getByTestId('recovery_door_alert_icon') + screen.getByText('Robot door is open') + screen.getByText( + 'Close the robot door, and then resume the recovery action.' + ) + }) + + it(`calls resumeRecovery when the primary button is clicked and the run status is ${RUN_STATUS_AWAITING_RECOVERY_PAUSED}`, () => { + render(props) + + clickButtonLabeled('Resume') + + expect(mockResumeRecovery).toHaveBeenCalledTimes(1) + }) +}) diff --git a/app/src/organisms/ErrorRecoveryFlows/__tests__/RunPausedSplash.test.tsx b/app/src/organisms/ErrorRecoveryFlows/__tests__/RunPausedSplash.test.tsx index 57f228b4830..ff3c8d1cc34 100644 --- a/app/src/organisms/ErrorRecoveryFlows/__tests__/RunPausedSplash.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/__tests__/RunPausedSplash.test.tsx @@ -34,13 +34,24 @@ describe('useRunPausedSplash', () => { ) }) - const IS_WIZARD_SHOWN = [false, true] - IS_WIZARD_SHOWN.forEach(val => { - it(`returns ${!val} if showERWizard is ${val}`, () => { - const { result } = renderHook(() => useRunPausedSplash(val), { - wrapper, + const TEST_CASES = [ + { isOnDevice: true, showERWizard: true, expected: true }, + { isOnDevice: true, showERWizard: false, expected: true }, + { isOnDevice: false, showERWizard: true, expected: false }, + { isOnDevice: false, showERWizard: false, expected: true }, + ] + + describe('useRunPausedSplash', () => { + TEST_CASES.forEach(({ isOnDevice, showERWizard, expected }) => { + it(`returns ${expected} when isOnDevice is ${isOnDevice} and showERWizard is ${showERWizard}`, () => { + const { result } = renderHook( + () => useRunPausedSplash(isOnDevice, showERWizard), + { + wrapper, + } + ) + expect(result.current).toEqual(expected) }) - expect(result.current).toEqual(!val) }) }) }) diff --git a/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useRecoveryActionMutation.test.ts b/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useRecoveryActionMutation.test.ts new file mode 100644 index 00000000000..029f7d5e239 --- /dev/null +++ b/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useRecoveryActionMutation.test.ts @@ -0,0 +1,55 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { renderHook } from '@testing-library/react' + +import { useRunActionMutations } from '@opentrons/react-api-client' + +import { useRecoveryActionMutation } from '../useRecoveryActionMutation' + +import type { Mock } from 'vitest' + +vi.mock('@opentrons/react-api-client', () => ({ + useRunActionMutations: vi.fn(), +})) + +describe('useRecoveryActionMutation', () => { + const mockRunId = 'MOCK_ID' + let mockPlayRun: Mock + let mockIsPlayRunActionLoading: boolean + + beforeEach(() => { + mockPlayRun = vi.fn() + mockIsPlayRunActionLoading = false + + vi.mocked(useRunActionMutations).mockReturnValue({ + playRun: mockPlayRun, + isPlayRunActionLoading: mockIsPlayRunActionLoading, + } as any) + }) + + it('should return resumeRecovery and isResumeRecoveryLoading', () => { + const { result } = renderHook(() => useRecoveryActionMutation(mockRunId)) + + expect(result.current).toEqual({ + resumeRecovery: mockPlayRun, + isResumeRecoveryLoading: mockIsPlayRunActionLoading, + }) + }) + + it('should return updated isResumeRecoveryLoading when it changes', () => { + const { result, rerender } = renderHook(() => + useRecoveryActionMutation(mockRunId) + ) + + expect(result.current.isResumeRecoveryLoading).toBe(false) + + mockIsPlayRunActionLoading = true + vi.mocked(useRunActionMutations).mockReturnValue({ + playRun: mockPlayRun, + isPlayRunActionLoading: mockIsPlayRunActionLoading, + } as any) + + rerender() + + expect(result.current.isResumeRecoveryLoading).toBe(true) + }) +}) diff --git a/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useShowDoorInfo.test.ts b/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useShowDoorInfo.test.ts new file mode 100644 index 00000000000..226ca7de023 --- /dev/null +++ b/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useShowDoorInfo.test.ts @@ -0,0 +1,51 @@ +import { describe, it, expect, beforeEach } from 'vitest' +import { renderHook, act } from '@testing-library/react' +import { useShowDoorInfo } from '../useShowDoorInfo' +import { + RUN_STATUS_AWAITING_RECOVERY_BLOCKED_BY_OPEN_DOOR, + RUN_STATUS_AWAITING_RECOVERY_PAUSED, + RUN_STATUS_AWAITING_RECOVERY, +} from '@opentrons/api-client' + +describe('useShowDoorInfo', () => { + let initialProps: Parameters[0] + + beforeEach(() => { + initialProps = RUN_STATUS_AWAITING_RECOVERY + }) + + it('should return false initially', () => { + const { result } = renderHook(() => useShowDoorInfo(initialProps)) + expect(result.current).toBe(false) + }) + + it(`should return true when runStatus is ${RUN_STATUS_AWAITING_RECOVERY_BLOCKED_BY_OPEN_DOOR}`, () => { + const props = RUN_STATUS_AWAITING_RECOVERY_BLOCKED_BY_OPEN_DOOR + + const { result } = renderHook(() => useShowDoorInfo(props)) + expect(result.current).toBe(true) + }) + + it(`should return true when runStatus is ${RUN_STATUS_AWAITING_RECOVERY_PAUSED}`, () => { + const props = RUN_STATUS_AWAITING_RECOVERY_PAUSED + + const { result } = renderHook(() => useShowDoorInfo(props)) + expect(result.current).toBe(true) + }) + + it(`should keep returning true when runStatus changes from ${RUN_STATUS_AWAITING_RECOVERY_BLOCKED_BY_OPEN_DOOR} to ${RUN_STATUS_AWAITING_RECOVERY_PAUSED}`, () => { + const { result, rerender } = renderHook(props => useShowDoorInfo(props), { + initialProps, + }) + + act(() => { + rerender(RUN_STATUS_AWAITING_RECOVERY_BLOCKED_BY_OPEN_DOOR) + }) + expect(result.current).toBe(true) + + act(() => { + rerender(RUN_STATUS_AWAITING_RECOVERY_PAUSED) + }) + expect(result.current).toBe(true) + }) +}) diff --git a/app/src/organisms/ErrorRecoveryFlows/hooks/index.ts b/app/src/organisms/ErrorRecoveryFlows/hooks/index.ts index 8bb482cb1aa..31d9ebb4367 100644 --- a/app/src/organisms/ErrorRecoveryFlows/hooks/index.ts +++ b/app/src/organisms/ErrorRecoveryFlows/hooks/index.ts @@ -1,6 +1,7 @@ export { useCurrentlyRecoveringFrom } from './useCurrentlyRecoveringFrom' export { useErrorMessage } from './useErrorMessage' export { useErrorName } from './useErrorName' +export { useShowDoorInfo } from './useShowDoorInfo' export { useRecoveryCommands } from './useRecoveryCommands' export { useRouteUpdateActions } from './useRouteUpdateActions' export { useERUtils } from './useERUtils' diff --git a/app/src/organisms/ErrorRecoveryFlows/hooks/useERUtils.ts b/app/src/organisms/ErrorRecoveryFlows/hooks/useERUtils.ts index 1881b4b829c..030c95f9f11 100644 --- a/app/src/organisms/ErrorRecoveryFlows/hooks/useERUtils.ts +++ b/app/src/organisms/ErrorRecoveryFlows/hooks/useERUtils.ts @@ -12,6 +12,7 @@ import { useNotifyRunQuery, } from '../../../resources/runs' import { useRecoveryOptionCopy } from './useRecoveryOptionCopy' +import { useRecoveryActionMutation } from './useRecoveryActionMutation' import { useRunningStepCounts } from '../../../resources/protocols/hooks' import type { PipetteData } from '@opentrons/api-client' @@ -23,6 +24,7 @@ import type { RecoveryTipStatusUtils } from './useRecoveryTipStatus' import type { UseFailedLabwareUtilsResult } from './useFailedLabwareUtils' import type { UseDeckMapUtilsResult } from './useDeckMapUtils' import type { CurrentRecoveryOptionUtils } from './useRecoveryRouting' +import type { RecoveryActionMutationResult } from './useRecoveryActionMutation' import type { StepCounts } from '../../../resources/protocols/hooks' type ERUtilsProps = ErrorRecoveryFlowsProps & { @@ -39,6 +41,7 @@ export interface ERUtilsResults { failedLabwareUtils: UseFailedLabwareUtilsResult deckMapUtils: UseDeckMapUtilsResult getRecoveryOptionCopy: ReturnType + recoveryActionMutationUtils: RecoveryActionMutationResult failedPipetteInfo: PipetteData | null hasLaunchedRecovery: boolean trackExternalMap: (map: Record) => void @@ -119,6 +122,8 @@ export function useERUtils({ const stepCounts = useRunningStepCounts(runId, runCommands) + const recoveryActionMutationUtils = useRecoveryActionMutation(runId) + // TODO(jh, 06-14-24): Ensure other string build utilities that are internal to ErrorRecoveryFlows are exported under // one utility object in useERUtils. const getRecoveryOptionCopy = useRecoveryOptionCopy() @@ -131,6 +136,7 @@ export function useERUtils({ recoveryMap, trackExternalMap, currentRecoveryOptionUtils, + recoveryActionMutationUtils, routeUpdateActions, recoveryCommands, hasLaunchedRecovery, diff --git a/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryActionMutation.ts b/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryActionMutation.ts new file mode 100644 index 00000000000..5f33df7941d --- /dev/null +++ b/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryActionMutation.ts @@ -0,0 +1,20 @@ +import type { ErrorRecoveryFlowsProps } from '..' +import { useRunActionMutations } from '@opentrons/react-api-client' + +export interface RecoveryActionMutationResult { + resumeRecovery: ReturnType['playRun'] + isResumeRecoveryLoading: ReturnType< + typeof useRunActionMutations + >['isPlayRunActionLoading'] +} + +export function useRecoveryActionMutation( + runId: ErrorRecoveryFlowsProps['runId'] +): RecoveryActionMutationResult { + const { + playRun: resumeRecovery, + isPlayRunActionLoading: isResumeRecoveryLoading, + } = useRunActionMutations(runId) + + return { resumeRecovery, isResumeRecoveryLoading } +} diff --git a/app/src/organisms/ErrorRecoveryFlows/hooks/useShowDoorInfo.ts b/app/src/organisms/ErrorRecoveryFlows/hooks/useShowDoorInfo.ts new file mode 100644 index 00000000000..f73f1a22ae0 --- /dev/null +++ b/app/src/organisms/ErrorRecoveryFlows/hooks/useShowDoorInfo.ts @@ -0,0 +1,38 @@ +import * as React from 'react' + +import { + RUN_STATUS_AWAITING_RECOVERY_BLOCKED_BY_OPEN_DOOR, + RUN_STATUS_AWAITING_RECOVERY_PAUSED, +} from '@opentrons/api-client' + +import type { RunStatus } from '@opentrons/api-client' +import type { ErrorRecoveryFlowsProps } from '../index' + +const DOOR_OPEN_STATUSES: RunStatus[] = [ + RUN_STATUS_AWAITING_RECOVERY_BLOCKED_BY_OPEN_DOOR, + RUN_STATUS_AWAITING_RECOVERY_PAUSED, +] + +// Whether the door is open or the user has not yet resumed the run after a door open event. +export function useShowDoorInfo( + runStatus: ErrorRecoveryFlowsProps['runStatus'] +): boolean { + const [showDoorModal, setShowDoorModal] = React.useState(false) + + React.useEffect(() => { + // TODO(jh, 07-16-24): "recovery paused" is only used for door status and therefore + // a valid way to ensure all apps show the door open prompt, however this could be problematic in the future. + // Consider restructuring this check once the takeover modals are added. + if (runStatus != null && DOOR_OPEN_STATUSES.includes(runStatus)) { + setShowDoorModal(true) + } else if ( + showDoorModal && + runStatus != null && + !DOOR_OPEN_STATUSES.includes(runStatus) + ) { + setShowDoorModal(false) + } + }, [runStatus, showDoorModal]) + + return showDoorModal +} diff --git a/app/src/organisms/ErrorRecoveryFlows/index.tsx b/app/src/organisms/ErrorRecoveryFlows/index.tsx index 3f64b644785..2a2a319499d 100644 --- a/app/src/organisms/ErrorRecoveryFlows/index.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/index.tsx @@ -18,7 +18,11 @@ import { OT2_ROBOT_TYPE } from '@opentrons/shared-data' import { getIsOnDevice } from '../../redux/config' import { ErrorRecoveryWizard, useERWizard } from './ErrorRecoveryWizard' import { RunPausedSplash, useRunPausedSplash } from './RunPausedSplash' -import { useCurrentlyRecoveringFrom, useERUtils } from './hooks' +import { + useCurrentlyRecoveringFrom, + useERUtils, + useShowDoorInfo, +} from './hooks' import type { RunStatus } from '@opentrons/api-client' import type { CompletedProtocolAnalysis } from '@opentrons/shared-data' @@ -100,6 +104,7 @@ export function useErrorRecoveryFlows( export interface ErrorRecoveryFlowsProps { runId: string + runStatus: RunStatus | null failedCommand: FailedCommand | null isFlex: boolean protocolAnalysis: CompletedProtocolAnalysis | null @@ -108,30 +113,31 @@ export interface ErrorRecoveryFlowsProps { export function ErrorRecoveryFlows( props: ErrorRecoveryFlowsProps ): JSX.Element | null { + const { protocolAnalysis, runStatus } = props + const { hasLaunchedRecovery, toggleERWizard, showERWizard } = useERWizard() + const isDoorOpen = useShowDoorInfo(runStatus) + const recoveryUtils = useERUtils({ ...props, hasLaunchedRecovery, toggleERWizard, }) - // if (!enableRunNotes) { - // return null - // } - - const { protocolAnalysis } = props const robotType = protocolAnalysis?.robotType ?? OT2_ROBOT_TYPE const isOnDevice = useSelector(getIsOnDevice) - const showSplash = useRunPausedSplash(showERWizard) + const showSplash = useRunPausedSplash(isOnDevice, showERWizard) + return ( <> - {showERWizard ? ( + {showERWizard || isDoorOpen ? ( ) : null} {showSplash ? ( diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/RecoveryFooterButtons.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/RecoveryFooterButtons.tsx index 245755ddaee..ea78376da4e 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/RecoveryFooterButtons.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/RecoveryFooterButtons.tsx @@ -4,6 +4,8 @@ import { css } from 'styled-components' import { ALIGN_FLEX_END, + ALIGN_CENTER, + Icon, Box, Flex, JUSTIFY_SPACE_BETWEEN, @@ -21,6 +23,7 @@ interface RecoveryFooterButtonProps { /* The "Go back" button */ secondaryBtnOnClick?: () => void primaryBtnTextOverride?: string + primaryBtnDisabled?: boolean /* If true, render pressed state and a spinner icon for the primary button. */ isLoadingPrimaryBtnAction?: boolean /* To the left of the primary button. */ @@ -84,6 +87,7 @@ function PrimaryButtonGroup(props: RecoveryFooterButtonProps): JSX.Element { function RecoveryPrimaryBtn({ isLoadingPrimaryBtnAction, primaryBtnOnClick, + primaryBtnDisabled, primaryBtnTextOverride, }: RecoveryFooterButtonProps): JSX.Element { const { t } = useTranslation('error_recovery') @@ -103,13 +107,25 @@ function RecoveryPrimaryBtn({ buttonType="primary" buttonText={primaryBtnTextOverride ?? t('continue')} onClick={primaryBtnOnClick} + disabled={primaryBtnDisabled} /> - {primaryBtnTextOverride ?? t('continue')} + + {isLoadingPrimaryBtnAction && ( + + )} + {primaryBtnTextOverride ?? t('continue')} + ) diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/RecoveryInterventionModal.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/RecoveryInterventionModal.tsx index 6b9ba60c43b..9332ab8766d 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/RecoveryInterventionModal.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/RecoveryInterventionModal.tsx @@ -5,7 +5,7 @@ import { css } from 'styled-components' import { Flex, RESPONSIVENESS, SPACING } from '@opentrons/components' import { InterventionModal } from '../../../molecules/InterventionModal' -import { getModalPortalEl } from '../../../App/portal' +import { getModalPortalEl, getTopPortalEl } from '../../../App/portal' import type { ModalType } from '../../../molecules/InterventionModal' @@ -15,12 +15,14 @@ export type RecoveryInterventionModalProps = Omit< > & { /* If on desktop, specifies the hard-coded dimensions height of the modal. */ desktopType: 'desktop-small' | 'desktop-large' + isOnDevice: boolean } // A wrapper around InterventionModal with Error-Recovery specific props and styling. export function RecoveryInterventionModal({ children, desktopType, + isOnDevice, ...rest }: RecoveryInterventionModalProps): JSX.Element { const restProps = { @@ -41,7 +43,7 @@ export function RecoveryInterventionModal({ {children}
, - getModalPortalEl() + isOnDevice ? getTopPortalEl() : getModalPortalEl() ) } diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/RecoveryFooterButtons.test.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/RecoveryFooterButtons.test.tsx index f4921afd646..b4e2b260715 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/RecoveryFooterButtons.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/RecoveryFooterButtons.test.tsx @@ -63,6 +63,22 @@ describe('RecoveryFooterButtons', () => { expect(secondaries.length).toBe(2) }) + it('renders the primary button as disabled when primaryBtnDisabled is true', () => { + props = { + ...props, + primaryBtnOnClick: mockPrimaryBtnOnClick, + primaryBtnDisabled: true, + primaryBtnTextOverride: 'Hi', + } + render(props) + + const primaryBtns = screen.getAllByRole('button', { name: 'Hi' }) + + primaryBtns.forEach(btn => { + expect(btn).toBeDisabled() + }) + }) + it('does not render the secondary button if no on click handler is supplied', () => { props = { ...props, secondaryBtnOnClick: undefined } render(props) diff --git a/app/src/pages/RunningProtocol/__tests__/RunningProtocol.test.tsx b/app/src/pages/RunningProtocol/__tests__/RunningProtocol.test.tsx index d4d881a9e1e..a9335b25c73 100644 --- a/app/src/pages/RunningProtocol/__tests__/RunningProtocol.test.tsx +++ b/app/src/pages/RunningProtocol/__tests__/RunningProtocol.test.tsx @@ -9,6 +9,7 @@ import { RUN_STATUS_IDLE, RUN_STATUS_STOP_REQUESTED, RUN_STATUS_AWAITING_RECOVERY, + RUN_STATUS_AWAITING_RECOVERY_BLOCKED_BY_OPEN_DOOR, } from '@opentrons/api-client' import { useProtocolAnalysesQuery, @@ -191,6 +192,14 @@ describe('RunningProtocol', () => { expect(vi.mocked(OpenDoorAlertModal)).toHaveBeenCalled() }) + it(`should render not open door alert modal, when run status is ${RUN_STATUS_AWAITING_RECOVERY_BLOCKED_BY_OPEN_DOOR}`, () => { + when(vi.mocked(useRunStatus)) + .calledWith(RUN_ID, { refetchInterval: 5000 }) + .thenReturn(RUN_STATUS_AWAITING_RECOVERY_BLOCKED_BY_OPEN_DOOR) + render(`/runs/${RUN_ID}/run`) + expect(vi.mocked(OpenDoorAlertModal)).not.toHaveBeenCalled() + }) + it(`should display a Run Paused splash screen if the run status is "${RUN_STATUS_AWAITING_RECOVERY}"`, () => { when(vi.mocked(useRunStatus)) .calledWith(RUN_ID, { refetchInterval: 5000 }) diff --git a/app/src/pages/RunningProtocol/index.tsx b/app/src/pages/RunningProtocol/index.tsx index 8b6c2fa225d..e6c42bc85cb 100644 --- a/app/src/pages/RunningProtocol/index.tsx +++ b/app/src/pages/RunningProtocol/index.tsx @@ -25,7 +25,6 @@ import { RUN_STATUS_STOP_REQUESTED, RUN_STATUS_BLOCKED_BY_OPEN_DOOR, RUN_STATUS_FINISHING, - RUN_STATUS_AWAITING_RECOVERY_BLOCKED_BY_OPEN_DOOR, } from '@opentrons/api-client' import { StepMeter } from '../../atoms/StepMeter' @@ -162,14 +161,14 @@ export function RunningProtocol(): JSX.Element { <> {isERActive ? ( ) : null} - {runStatus === RUN_STATUS_BLOCKED_BY_OPEN_DOOR || - runStatus === RUN_STATUS_AWAITING_RECOVERY_BLOCKED_BY_OPEN_DOOR ? ( + {runStatus === RUN_STATUS_BLOCKED_BY_OPEN_DOOR ? ( ) : null} {runStatus === RUN_STATUS_STOP_REQUESTED ? : null} From 0ba6c796ca917a291827f7c1484509182eee8d2f Mon Sep 17 00:00:00 2001 From: Rhyann Clarke <146747548+rclarke0@users.noreply.github.com> Date: Tue, 16 Jul 2024 16:37:29 -0400 Subject: [PATCH 33/78] fix(abr-testing): increase number of lines of logs saved during error (#15680) # Overview Increase record # for log downloading during ABR error # Test Plan # Changelog - moved get_logs to beginning of error script so logs are pulled immediately - increase number of lines pulled for each log type # Review requests # Risk assessment --- .../data_collection/abr_robot_error.py | 2 +- .../data_collection/read_robot_logs.py | 21 +++++++++++++------ 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/abr-testing/abr_testing/data_collection/abr_robot_error.py b/abr-testing/abr_testing/data_collection/abr_robot_error.py index 2bf9abbd1a1..b42a195f52a 100644 --- a/abr-testing/abr_testing/data_collection/abr_robot_error.py +++ b/abr-testing/abr_testing/data_collection/abr_robot_error.py @@ -464,6 +464,7 @@ def get_run_error_info_from_robot( email = args.email[0] board_id = args.board_id[0] reporter_id = args.reporter_id[0] + file_paths = read_robot_logs.get_logs(storage_directory, ip) ticket = jira_tool.JiraTicket(url, api_token, email) ticket.issues_on_board(board_id) users_file_path = ticket.get_jira_users(storage_directory) @@ -496,7 +497,6 @@ def get_run_error_info_from_robot( saved_file_path_calibration, calibration = read_robot_logs.get_calibration_offsets( ip, storage_directory ) - file_paths = read_robot_logs.get_logs(storage_directory, ip) print(f"Making ticket for {summary}.") # TODO: make argument or see if I can get rid of with using board_id. diff --git a/abr-testing/abr_testing/data_collection/read_robot_logs.py b/abr-testing/abr_testing/data_collection/read_robot_logs.py index ac3636ed8a7..740adbf0cb6 100644 --- a/abr-testing/abr_testing/data_collection/read_robot_logs.py +++ b/abr-testing/abr_testing/data_collection/read_robot_logs.py @@ -569,23 +569,32 @@ def get_calibration_offsets( def get_logs(storage_directory: str, ip: str) -> List[str]: """Get Robot logs.""" - log_types = ["api.log", "server.log", "serial.log", "touchscreen.log"] + log_types: List[Dict[str, Any]] = [ + {"log type": "api.log", "records": 1000}, + {"log type": "server.log", "records": 10000}, + {"log type": "serial.log", "records": 10000}, + {"log type": "touchscreen.log", "records": 1000}, + ] all_paths = [] for log_type in log_types: try: + log_type_name = log_type["log type"] + print(log_type_name) + log_records = int(log_type["records"]) + print(log_records) response = requests.get( - f"http://{ip}:31950/logs/{log_type}", - headers={"log_identifier": log_type}, - params={"records": 5000}, + f"http://{ip}:31950/logs/{log_type_name}", + headers={"log_identifier": log_type_name}, + params={"records": log_records}, ) response.raise_for_status() log_data = response.text - log_name = ip + "_" + log_type.split(".")[0] + ".log" + log_name = ip + "_" + log_type_name.split(".")[0] + ".log" file_path = os.path.join(storage_directory, log_name) with open(file_path, mode="w", encoding="utf-8") as file: file.write(log_data) except RuntimeError: - print(f"Request exception. Did not save {log_type}") + print(f"Request exception. Did not save {log_type_name}") continue all_paths.append(file_path) # Get weston.log using scp From efb5361c5c86dc0beb7d43b731a1970913b0fd02 Mon Sep 17 00:00:00 2001 From: Jethary Rader <66035149+jerader@users.noreply.github.com> Date: Wed, 17 Jul 2024 10:47:53 -0400 Subject: [PATCH 34/78] feat(protocol-designer): create redesign ff and scaffolding for new nav (#15657) closes AUTH-555 --- protocol-designer/package.json | 1 + .../src/{components => }/App.tsx | 8 +- protocol-designer/src/Navbar.tsx | 62 ++++++++++++ protocol-designer/src/ProtocolEditor.tsx | 99 +++++++++++++++++++ protocol-designer/src/ProtocolRoutes.tsx | 72 ++++++++++++++ .../src/components/ProtocolEditor.tsx | 62 ------------ .../src/feature-flags/reducers.ts | 1 + .../src/feature-flags/selectors.ts | 18 +++- protocol-designer/src/feature-flags/types.ts | 2 + protocol-designer/src/index.tsx | 2 +- .../src/localization/en/feature_flags.json | 4 + .../src/localization/en/index.ts | 8 ++ .../src/localization/en/liquids.json | 3 + .../localization/en/protocol_overview.json | 3 + .../src/localization/en/protocol_steps.json | 3 + .../src/localization/en/shared.json | 3 + .../localization/en/starting_deck_state.json | 3 + protocol-designer/src/localization/index.ts | 4 + .../__tests__/CreateNewProtocol.test.tsx | 3 + .../src/pages/CreateNewProtocol/index.tsx | 5 + .../pages/Landing/__tests__/Landing.test.tsx | 3 + protocol-designer/src/pages/Landing/index.tsx | 27 +++++ .../pages/Liquids/__tests__/Liquids.test.tsx | 3 + protocol-designer/src/pages/Liquids/index.tsx | 7 ++ .../__tests__/ProtocolOverview.test.tsx | 3 + .../src/pages/ProtocolOverview/index.tsx | 8 ++ .../__tests__/ProtocolSteps.test.tsx | 3 + .../src/pages/ProtocolSteps/index.tsx | 8 ++ .../__tests__/StartingDeckState.test.tsx | 3 + .../src/pages/StartingDeckState/index.tsx | 8 ++ .../src/step-forms/selectors/index.ts | 5 +- protocol-designer/src/types.ts | 14 +++ yarn.lock | 51 +++++++++- 33 files changed, 431 insertions(+), 78 deletions(-) rename protocol-designer/src/{components => }/App.tsx (50%) create mode 100644 protocol-designer/src/Navbar.tsx create mode 100644 protocol-designer/src/ProtocolEditor.tsx create mode 100644 protocol-designer/src/ProtocolRoutes.tsx delete mode 100644 protocol-designer/src/components/ProtocolEditor.tsx create mode 100644 protocol-designer/src/localization/en/liquids.json create mode 100644 protocol-designer/src/localization/en/protocol_overview.json create mode 100644 protocol-designer/src/localization/en/protocol_steps.json create mode 100644 protocol-designer/src/localization/en/starting_deck_state.json create mode 100644 protocol-designer/src/pages/CreateNewProtocol/__tests__/CreateNewProtocol.test.tsx create mode 100644 protocol-designer/src/pages/CreateNewProtocol/index.tsx create mode 100644 protocol-designer/src/pages/Landing/__tests__/Landing.test.tsx create mode 100644 protocol-designer/src/pages/Landing/index.tsx create mode 100644 protocol-designer/src/pages/Liquids/__tests__/Liquids.test.tsx create mode 100644 protocol-designer/src/pages/Liquids/index.tsx create mode 100644 protocol-designer/src/pages/ProtocolOverview/__tests__/ProtocolOverview.test.tsx create mode 100644 protocol-designer/src/pages/ProtocolOverview/index.tsx create mode 100644 protocol-designer/src/pages/ProtocolSteps/__tests__/ProtocolSteps.test.tsx create mode 100644 protocol-designer/src/pages/ProtocolSteps/index.tsx create mode 100644 protocol-designer/src/pages/StartingDeckState/__tests__/StartingDeckState.test.tsx create mode 100644 protocol-designer/src/pages/StartingDeckState/index.tsx diff --git a/protocol-designer/package.json b/protocol-designer/package.json index 564ebdb2fe1..21e7b98e18f 100755 --- a/protocol-designer/package.json +++ b/protocol-designer/package.json @@ -54,6 +54,7 @@ "redux": "4.0.5", "redux-actions": "2.2.1", "react-popper": "1.0.0", + "react-router-dom": "6.24.1", "redux-thunk": "2.3.0", "reselect": "4.0.0", "styled-components": "5.3.6", diff --git a/protocol-designer/src/components/App.tsx b/protocol-designer/src/App.tsx similarity index 50% rename from protocol-designer/src/components/App.tsx rename to protocol-designer/src/App.tsx index a64dbf917ac..7699de60ce2 100644 --- a/protocol-designer/src/components/App.tsx +++ b/protocol-designer/src/App.tsx @@ -1,12 +1,6 @@ import * as React from 'react' import { ProtocolEditor } from './ProtocolEditor' -import '../css/reset.module.css' - export function App(): JSX.Element { - return ( -
- -
- ) + return } diff --git a/protocol-designer/src/Navbar.tsx b/protocol-designer/src/Navbar.tsx new file mode 100644 index 00000000000..1c19f7f1605 --- /dev/null +++ b/protocol-designer/src/Navbar.tsx @@ -0,0 +1,62 @@ +import * as React from 'react' +import { NavLink } from 'react-router-dom' +import styled from 'styled-components' + +import { + ALIGN_CENTER, + ALIGN_FLEX_START, + ALIGN_STRETCH, + COLORS, + DIRECTION_COLUMN, + FLEX_NONE, + Flex, + JUSTIFY_SPACE_BETWEEN, + SPACING, + LegacyStyledText, + TYPOGRAPHY, + DIRECTION_ROW, +} from '@opentrons/components' + +import type { RouteProps } from './types' + +export function Navbar({ routes }: { routes: RouteProps[] }): JSX.Element { + const navRoutes = routes.filter( + ({ navLinkTo }: RouteProps) => navLinkTo != null + ) + return ( + + + {navRoutes.map(({ name, navLinkTo }: RouteProps) => ( + + + {name} + + + ))} + + + ) +} + +const NavbarLink = styled(NavLink)` + color: ${COLORS.black90}; + text-decoration: none; + align-self: ${ALIGN_STRETCH}; + &:hover { + color: ${COLORS.black70}; + } +` diff --git a/protocol-designer/src/ProtocolEditor.tsx b/protocol-designer/src/ProtocolEditor.tsx new file mode 100644 index 00000000000..e66bb285e12 --- /dev/null +++ b/protocol-designer/src/ProtocolEditor.tsx @@ -0,0 +1,99 @@ +import * as React from 'react' +import cx from 'classnames' +import { DndProvider } from 'react-dnd' +import { BrowserRouter } from 'react-router-dom' +import { useDispatch, useSelector } from 'react-redux' +import { HTML5Backend } from 'react-dnd-html5-backend' +import { + DIRECTION_COLUMN, + DIRECTION_ROW, + Flex, + PrimaryButton, + SPACING, +} from '@opentrons/components' +import { getEnableRedesign } from './feature-flags/selectors' +import { setFeatureFlags } from './feature-flags/actions' +import { ComputingSpinner } from './components/ComputingSpinner' +import { ConnectedNav } from './containers/ConnectedNav' +import { Sidebar } from './containers/ConnectedSidebar' +import { ConnectedTitleBar } from './containers/ConnectedTitleBar' +import { MainPanel } from './containers/ConnectedMainPanel' +import { PortalRoot as MainPageModalPortalRoot } from './components/portals/MainPageModalPortal' +import { MAIN_CONTENT_FORCED_SCROLL_CLASSNAME } from './ui/steps/utils' +import { PrereleaseModeIndicator } from './components/PrereleaseModeIndicator' +import { PortalRoot as TopPortalRoot } from './components/portals/TopPortal' +import { FileUploadMessageModal } from './components/modals/FileUploadMessageModal/FileUploadMessageModal' +import { LabwareUploadMessageModal } from './components/modals/LabwareUploadMessageModal/LabwareUploadMessageModal' +import { GateModal } from './components/modals/GateModal' +import { CreateFileWizard } from './components/modals/CreateFileWizard' +import { AnnouncementModal } from './components/modals/AnnouncementModal' +import { ProtocolRoutes } from './ProtocolRoutes' + +import styles from './components/ProtocolEditor.module.css' +import './css/reset.module.css' + +const showGateModal = + process.env.NODE_ENV === 'production' || process.env.OT_PD_SHOW_GATE + +function ProtocolEditorComponent(): JSX.Element { + const enableRedesign = useSelector(getEnableRedesign) + const dispatch = useDispatch() + + return ( +
+ + {enableRedesign ? ( + + + { + dispatch(setFeatureFlags({ OT_PD_ENABLE_REDESIGN: false })) + }} + > + turn off redesign + + + + + + + ) : ( +
+ + + {showGateModal ? : null} + +
+ + +
+ + +
+ + + + + + + +
+
+
+
+ )} +
+ ) +} + +export const ProtocolEditor = (): JSX.Element => ( + + + +) diff --git a/protocol-designer/src/ProtocolRoutes.tsx b/protocol-designer/src/ProtocolRoutes.tsx new file mode 100644 index 00000000000..b9d167acd95 --- /dev/null +++ b/protocol-designer/src/ProtocolRoutes.tsx @@ -0,0 +1,72 @@ +import * as React from 'react' +import { Route, Navigate, Routes, useLocation } from 'react-router-dom' +import { Box } from '@opentrons/components' +import { Landing } from './pages/Landing' +import { ProtocolOverview } from './pages/ProtocolOverview' +import { Liquids } from './pages/Liquids' +import { StartingDeckState } from './pages/StartingDeckState' +import { ProtocolSteps } from './pages/ProtocolSteps' +import { CreateNewProtocol } from './pages/CreateNewProtocol' +import { Navbar } from './Navbar' + +import type { RouteProps } from './types' + +const LANDING_ROUTE = '/' +const pdRoutes: RouteProps[] = [ + { + Component: ProtocolOverview, + name: 'Protocol overview', + navLinkTo: '/overview', + path: '/overview', + }, + { + Component: Liquids, + name: 'Liquids', + navLinkTo: '/liquids', + path: '/liquids', + }, + { + Component: StartingDeckState, + name: 'Starting deck state', + navLinkTo: '/startingDeckState', + path: '/startingDeckState', + }, + { + Component: ProtocolSteps, + name: 'Protocol steps', + navLinkTo: '/steps', + path: '/steps', + }, + { + Component: CreateNewProtocol, + name: 'Create new protocol', + navLinkTo: '/createNew', + path: '/createNew', + }, +] + +export function ProtocolRoutes(): JSX.Element { + const location = useLocation() + const currentPath = location.pathname + const landingPage: RouteProps = { + Component: Landing, + name: 'Landing', + navLinkTo: '/', + path: '/', + } + const allRoutes: RouteProps[] = [...pdRoutes, landingPage] + + return ( + <> + {currentPath === LANDING_ROUTE ? null : } + + + {allRoutes.map(({ Component, path }: RouteProps) => { + return } /> + })} + } /> + + + + ) +} diff --git a/protocol-designer/src/components/ProtocolEditor.tsx b/protocol-designer/src/components/ProtocolEditor.tsx deleted file mode 100644 index 140b0ae08ee..00000000000 --- a/protocol-designer/src/components/ProtocolEditor.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import * as React from 'react' -import cx from 'classnames' -import { DndProvider } from 'react-dnd' -import { HTML5Backend } from 'react-dnd-html5-backend' -import { ComputingSpinner } from '../components/ComputingSpinner' -import { ConnectedNav } from '../containers/ConnectedNav' -import { Sidebar } from '../containers/ConnectedSidebar' -import { ConnectedTitleBar } from '../containers/ConnectedTitleBar' -import { MainPanel } from '../containers/ConnectedMainPanel' -import { PortalRoot as MainPageModalPortalRoot } from '../components/portals/MainPageModalPortal' -import { MAIN_CONTENT_FORCED_SCROLL_CLASSNAME } from '../ui/steps/utils' -import { PrereleaseModeIndicator } from './PrereleaseModeIndicator' -import { PortalRoot as TopPortalRoot } from './portals/TopPortal' -import { FileUploadMessageModal } from './modals/FileUploadMessageModal/FileUploadMessageModal' -import { LabwareUploadMessageModal } from './modals/LabwareUploadMessageModal/LabwareUploadMessageModal' -import { GateModal } from './modals/GateModal' -import { AnnouncementModal } from './modals/AnnouncementModal' -import styles from './ProtocolEditor.module.css' -import { CreateFileWizard } from './modals/CreateFileWizard' - -const showGateModal = - process.env.NODE_ENV === 'production' || process.env.OT_PD_SHOW_GATE - -function ProtocolEditorComponent(): JSX.Element { - return ( -
- - - {showGateModal ? : null} - -
- - -
- - -
- - - - - - - -
-
-
-
- ) -} - -export const ProtocolEditor = (): JSX.Element => ( - - - -) diff --git a/protocol-designer/src/feature-flags/reducers.ts b/protocol-designer/src/feature-flags/reducers.ts index 3e60f923a08..f83be0bab29 100644 --- a/protocol-designer/src/feature-flags/reducers.ts +++ b/protocol-designer/src/feature-flags/reducers.ts @@ -25,6 +25,7 @@ const initialFlags: Flags = { process.env.OT_PD_ALLOW_ALL_TIPRACKS === '1' || false, OT_PD_ENABLE_ABSORBANCE_READER: process.env.OT_PD_ENABLE_ABSORBANCE_READER === '1' || false, + OT_PD_ENABLE_REDESIGN: process.env.OT_PD_ENABLE_REDESIGN === '1' || false, } // @ts-expect-error(sa, 2021-6-10): cannot use string literals as action type // TODO IMMEDIATELY: refactor this to the old fashioned way if we cannot have type safety: https://github.com/redux-utilities/redux-actions/issues/282#issuecomment-595163081 diff --git a/protocol-designer/src/feature-flags/selectors.ts b/protocol-designer/src/feature-flags/selectors.ts index fe18b33fb49..5ca661974c0 100644 --- a/protocol-designer/src/feature-flags/selectors.ts +++ b/protocol-designer/src/feature-flags/selectors.ts @@ -2,10 +2,16 @@ import { createSelector } from 'reselect' import { getFlagsFromQueryParams } from './utils' import type { BaseState, Selector } from '../types' import type { Flags } from './types' -export const getFeatureFlagData = (state: BaseState): Flags => ({ - ...state.featureFlags.flags, - ...getFlagsFromQueryParams(), -}) + +const getFeatureFlags = (state: BaseState): Flags => state.featureFlags.flags + +export const getFeatureFlagData: Selector = createSelector( + [getFeatureFlags, getFlagsFromQueryParams], + (flags, queryParamsFlags) => ({ + ...flags, + ...queryParamsFlags, + }) +) export const getEnabledPrereleaseMode: Selector< boolean | null | undefined > = createSelector(getFeatureFlagData, flags => flags.PRERELEASE_MODE) @@ -23,3 +29,7 @@ export const getEnableAbsorbanceReader: Selector = createSelector( getFeatureFlagData, flags => flags.OT_PD_ENABLE_ABSORBANCE_READER ?? false ) +export const getEnableRedesign: Selector = createSelector( + getFeatureFlagData, + flags => flags.OT_PD_ENABLE_REDESIGN ?? false +) diff --git a/protocol-designer/src/feature-flags/types.ts b/protocol-designer/src/feature-flags/types.ts index 5f2ade969d7..75fd0bdfc9e 100644 --- a/protocol-designer/src/feature-flags/types.ts +++ b/protocol-designer/src/feature-flags/types.ts @@ -31,6 +31,7 @@ export type FlagTypes = | 'OT_PD_DISABLE_MODULE_RESTRICTIONS' | 'OT_PD_ALLOW_ALL_TIPRACKS' | 'OT_PD_ENABLE_ABSORBANCE_READER' + | 'OT_PD_ENABLE_REDESIGN' // flags that are not in this list only show in prerelease mode export const userFacingFlags: FlagTypes[] = [ 'OT_PD_DISABLE_MODULE_RESTRICTIONS', @@ -40,5 +41,6 @@ export const allFlags: FlagTypes[] = [ ...userFacingFlags, 'PRERELEASE_MODE', 'OT_PD_ENABLE_ABSORBANCE_READER', + 'OT_PD_ENABLE_REDESIGN', ] export type Flags = Partial> diff --git a/protocol-designer/src/index.tsx b/protocol-designer/src/index.tsx index 6f59322b947..aa88505e153 100644 --- a/protocol-designer/src/index.tsx +++ b/protocol-designer/src/index.tsx @@ -4,10 +4,10 @@ import { Provider } from 'react-redux' import { I18nextProvider } from 'react-i18next' import { configureStore } from './configureStore' -import { App } from './components/App' import { initialize } from './initialize' import { initializeMixpanel } from './analytics/mixpanel' import { i18n } from './localization' +import { App } from './App' // initialize Redux const store = configureStore() diff --git a/protocol-designer/src/localization/en/feature_flags.json b/protocol-designer/src/localization/en/feature_flags.json index 4902fa784d7..d3bbb1638ee 100644 --- a/protocol-designer/src/localization/en/feature_flags.json +++ b/protocol-designer/src/localization/en/feature_flags.json @@ -15,5 +15,9 @@ "OT_PD_ENABLE_ABSORBANCE_READER": { "title": "Enable absorbance plate reader", "description": "Enable absorbance plate reader support." + }, + "OT_PD_ENABLE_REDESIGN": { + "title": "Enable redesign", + "description": "A whole new world." } } diff --git a/protocol-designer/src/localization/en/index.ts b/protocol-designer/src/localization/en/index.ts index 36f6464d56e..8d1c9ca4b79 100644 --- a/protocol-designer/src/localization/en/index.ts +++ b/protocol-designer/src/localization/en/index.ts @@ -12,6 +12,10 @@ import nav from './nav.json' import shared from './shared.json' import tooltip from './tooltip.json' import well_selection from './well_selection.json' +import liquids from './liquids.json' +import protocol_overview from './protocol_overview.json' +import protocol_steps from './protocol_steps.json' +import starting_deck_state from './starting_deck_state.json' export const en = { alert, @@ -28,4 +32,8 @@ export const en = { shared, tooltip, well_selection, + liquids, + protocol_overview, + protocol_steps, + starting_deck_state, } diff --git a/protocol-designer/src/localization/en/liquids.json b/protocol-designer/src/localization/en/liquids.json new file mode 100644 index 00000000000..761f8b0fb2a --- /dev/null +++ b/protocol-designer/src/localization/en/liquids.json @@ -0,0 +1,3 @@ +{ + "liquids": "Liquids" +} diff --git a/protocol-designer/src/localization/en/protocol_overview.json b/protocol-designer/src/localization/en/protocol_overview.json new file mode 100644 index 00000000000..475116fc440 --- /dev/null +++ b/protocol-designer/src/localization/en/protocol_overview.json @@ -0,0 +1,3 @@ +{ + "protocol_overview": "Protocol overview" +} diff --git a/protocol-designer/src/localization/en/protocol_steps.json b/protocol-designer/src/localization/en/protocol_steps.json new file mode 100644 index 00000000000..e3b17ae7dfa --- /dev/null +++ b/protocol-designer/src/localization/en/protocol_steps.json @@ -0,0 +1,3 @@ +{ + "protocol_steps": "Protocol steps" +} diff --git a/protocol-designer/src/localization/en/shared.json b/protocol-designer/src/localization/en/shared.json index 41798ebbc15..6e2f2386cdc 100644 --- a/protocol-designer/src/localization/en/shared.json +++ b/protocol-designer/src/localization/en/shared.json @@ -3,10 +3,13 @@ "amount": "Amount:", "cancel": "Cancel", "confirm_reorder": "Are you sure you want to reorder these steps, it may cause errors?", + "create_new": "Create new", + "create_opentrons_protocol": "Create Opentrons protocol", "done": "Done", "edit": "edit", "exit": "exit", "go_back": "go back", + "import": "Import", "next": "next", "remove": "remove", "step": "Step {{current}} / {{max}}" diff --git a/protocol-designer/src/localization/en/starting_deck_state.json b/protocol-designer/src/localization/en/starting_deck_state.json new file mode 100644 index 00000000000..453c7406bb4 --- /dev/null +++ b/protocol-designer/src/localization/en/starting_deck_state.json @@ -0,0 +1,3 @@ +{ + "starting_deck_state": "Starting deck state" +} diff --git a/protocol-designer/src/localization/index.ts b/protocol-designer/src/localization/index.ts index 744080537c8..7b6188486f5 100644 --- a/protocol-designer/src/localization/index.ts +++ b/protocol-designer/src/localization/index.ts @@ -24,6 +24,10 @@ i18n.use(initReactI18next).init( 'nav', 'tooltip', 'well_selection', + 'liquids', + 'protocol_overview', + 'protocol_steps', + 'starting_deck_state', ], defaultNS: 'shared', interpolation: { diff --git a/protocol-designer/src/pages/CreateNewProtocol/__tests__/CreateNewProtocol.test.tsx b/protocol-designer/src/pages/CreateNewProtocol/__tests__/CreateNewProtocol.test.tsx new file mode 100644 index 00000000000..8ca87d9d825 --- /dev/null +++ b/protocol-designer/src/pages/CreateNewProtocol/__tests__/CreateNewProtocol.test.tsx @@ -0,0 +1,3 @@ +import { it } from 'vitest' + +it.todo('write test for CreateNewProtocol') diff --git a/protocol-designer/src/pages/CreateNewProtocol/index.tsx b/protocol-designer/src/pages/CreateNewProtocol/index.tsx new file mode 100644 index 00000000000..9a1cb694ff3 --- /dev/null +++ b/protocol-designer/src/pages/CreateNewProtocol/index.tsx @@ -0,0 +1,5 @@ +import * as React from 'react' + +export function CreateNewProtocol(): JSX.Element { + return
Create new protocol
+} diff --git a/protocol-designer/src/pages/Landing/__tests__/Landing.test.tsx b/protocol-designer/src/pages/Landing/__tests__/Landing.test.tsx new file mode 100644 index 00000000000..b8cc9d03fa2 --- /dev/null +++ b/protocol-designer/src/pages/Landing/__tests__/Landing.test.tsx @@ -0,0 +1,3 @@ +import { it } from 'vitest' + +it.todo('write test for Landing page') diff --git a/protocol-designer/src/pages/Landing/index.tsx b/protocol-designer/src/pages/Landing/index.tsx new file mode 100644 index 00000000000..5b02885c46f --- /dev/null +++ b/protocol-designer/src/pages/Landing/index.tsx @@ -0,0 +1,27 @@ +import * as React from 'react' +import { NavLink } from 'react-router-dom' +import { + Flex, + SPACING, + DIRECTION_COLUMN, + ALIGN_CENTER, +} from '@opentrons/components' +import { useTranslation } from 'react-i18next' + +export function Landing(): JSX.Element { + const { t } = useTranslation('shared') + + return ( + + {t('create_opentrons_protocol')} + + {t('create_new')} + {t('import')} + + + ) +} diff --git a/protocol-designer/src/pages/Liquids/__tests__/Liquids.test.tsx b/protocol-designer/src/pages/Liquids/__tests__/Liquids.test.tsx new file mode 100644 index 00000000000..687f5b4c417 --- /dev/null +++ b/protocol-designer/src/pages/Liquids/__tests__/Liquids.test.tsx @@ -0,0 +1,3 @@ +import { it } from 'vitest' + +it.todo('write test for Liquids') diff --git a/protocol-designer/src/pages/Liquids/index.tsx b/protocol-designer/src/pages/Liquids/index.tsx new file mode 100644 index 00000000000..dc97bb06111 --- /dev/null +++ b/protocol-designer/src/pages/Liquids/index.tsx @@ -0,0 +1,7 @@ +import * as React from 'react' +import { useTranslation } from 'react-i18next' + +export function Liquids(): JSX.Element { + const { t } = useTranslation('liquids') + return
{t('liquids')}
+} diff --git a/protocol-designer/src/pages/ProtocolOverview/__tests__/ProtocolOverview.test.tsx b/protocol-designer/src/pages/ProtocolOverview/__tests__/ProtocolOverview.test.tsx new file mode 100644 index 00000000000..d2b34d8ff5e --- /dev/null +++ b/protocol-designer/src/pages/ProtocolOverview/__tests__/ProtocolOverview.test.tsx @@ -0,0 +1,3 @@ +import { it } from 'vitest' + +it.todo('write test for ProtocolOverview') diff --git a/protocol-designer/src/pages/ProtocolOverview/index.tsx b/protocol-designer/src/pages/ProtocolOverview/index.tsx new file mode 100644 index 00000000000..a6be423cc74 --- /dev/null +++ b/protocol-designer/src/pages/ProtocolOverview/index.tsx @@ -0,0 +1,8 @@ +import * as React from 'react' +import { useTranslation } from 'react-i18next' + +export function ProtocolOverview(): JSX.Element { + const { t } = useTranslation('protocol_overview') + + return
{t('protocol_overview')}
+} diff --git a/protocol-designer/src/pages/ProtocolSteps/__tests__/ProtocolSteps.test.tsx b/protocol-designer/src/pages/ProtocolSteps/__tests__/ProtocolSteps.test.tsx new file mode 100644 index 00000000000..a8970f1d9d2 --- /dev/null +++ b/protocol-designer/src/pages/ProtocolSteps/__tests__/ProtocolSteps.test.tsx @@ -0,0 +1,3 @@ +import { it } from 'vitest' + +it.todo('write test for ProtocolSteps') diff --git a/protocol-designer/src/pages/ProtocolSteps/index.tsx b/protocol-designer/src/pages/ProtocolSteps/index.tsx new file mode 100644 index 00000000000..706c9402531 --- /dev/null +++ b/protocol-designer/src/pages/ProtocolSteps/index.tsx @@ -0,0 +1,8 @@ +import * as React from 'react' +import { useTranslation } from 'react-i18next' + +export function ProtocolSteps(): JSX.Element { + const { t } = useTranslation('protocol_steps') + + return
{t('protocol_steps')}
+} diff --git a/protocol-designer/src/pages/StartingDeckState/__tests__/StartingDeckState.test.tsx b/protocol-designer/src/pages/StartingDeckState/__tests__/StartingDeckState.test.tsx new file mode 100644 index 00000000000..8ab312f8435 --- /dev/null +++ b/protocol-designer/src/pages/StartingDeckState/__tests__/StartingDeckState.test.tsx @@ -0,0 +1,3 @@ +import { it } from 'vitest' + +it.todo('write test for StartingDeckState') diff --git a/protocol-designer/src/pages/StartingDeckState/index.tsx b/protocol-designer/src/pages/StartingDeckState/index.tsx new file mode 100644 index 00000000000..3027818c73b --- /dev/null +++ b/protocol-designer/src/pages/StartingDeckState/index.tsx @@ -0,0 +1,8 @@ +import * as React from 'react' +import { useTranslation } from 'react-i18next' + +export function StartingDeckState(): JSX.Element { + const { t } = useTranslation('starting_deck_state') + + return
{t('starting_deck_state')}
+} diff --git a/protocol-designer/src/step-forms/selectors/index.ts b/protocol-designer/src/step-forms/selectors/index.ts index 70b4ff44ce2..e67eabdcdb5 100644 --- a/protocol-designer/src/step-forms/selectors/index.ts +++ b/protocol-designer/src/step-forms/selectors/index.ts @@ -613,6 +613,7 @@ export const getInvariantContext: Selector< featureFlagSelectors.getDisableModuleRestrictions, featureFlagSelectors.getAllowAllTipracks, featureFlagSelectors.getEnableAbsorbanceReader, + featureFlagSelectors.getEnableRedesign, ( labwareEntities, moduleEntities, @@ -620,7 +621,8 @@ export const getInvariantContext: Selector< additionalEquipmentEntities, disableModuleRestrictions, allowAllTipracks, - enableAbsorbanceReader + enableAbsorbanceReader, + enableEnableRedesign ) => ({ labwareEntities, moduleEntities, @@ -630,6 +632,7 @@ export const getInvariantContext: Selector< OT_PD_ALLOW_ALL_TIPRACKS: Boolean(allowAllTipracks), OT_PD_DISABLE_MODULE_RESTRICTIONS: Boolean(disableModuleRestrictions), OT_PD_ENABLE_ABSORBANCE_READER: Boolean(enableAbsorbanceReader), + OT_PD_ENABLE_REDESIGN: Boolean(enableEnableRedesign), }, }) ) diff --git a/protocol-designer/src/types.ts b/protocol-designer/src/types.ts index 0feb5692270..274f38948ac 100644 --- a/protocol-designer/src/types.ts +++ b/protocol-designer/src/types.ts @@ -46,3 +46,17 @@ export type WellVolumes = Record export type DeckSlot = string export type NozzleType = NozzleConfigurationStyle | '8-channel' + +export interface RouteProps { + /** the component rendered by a route match + * drop developed components into slots held by placeholder div components + * */ + Component: React.FC + /** a route/page name to render in the nav bar + */ + name: string + /** the path for navigation linking, for example to push to a default tab + */ + path: string + navLinkTo: string +} diff --git a/yarn.lock b/yarn.lock index 48295e5e53e..f30ce404018 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3729,6 +3729,11 @@ "@react-spring/shared" "~9.6.1" "@react-spring/types" "~9.6.1" +"@remix-run/router@1.17.1": + version "1.17.1" + resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.17.1.tgz#bf93997beb81863fde042ebd05013a2618471362" + integrity sha512-mCOMec4BKd6BRGBZeSnGiIgwsbLGp3yhVqAD8H+PxiRNEHgDpZb8J1TnrSDlg97t0ySKMQJTHCWBCmBpSmkF6Q== + "@rollup/plugin-alias@^3.1.2": version "3.1.9" resolved "https://registry.yarnpkg.com/@rollup/plugin-alias/-/plugin-alias-3.1.9.tgz#a5d267548fe48441f34be8323fb64d1d4a1b3fdf" @@ -18587,6 +18592,14 @@ react-router-dom@5.3.4: tiny-invariant "^1.0.2" tiny-warning "^1.0.0" +react-router-dom@6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.24.1.tgz#b1a22f7d6c5a1bfce30732bd370713f991ab4de4" + integrity sha512-U19KtXqooqw967Vw0Qcn5cOvrX5Ejo9ORmOtJMzYWtCT4/WOfFLIZGGsVLxcd9UkBO0mSTZtXqhZBsWlHr7+Sg== + dependencies: + "@remix-run/router" "1.17.1" + react-router "6.24.1" + react-router@5.3.4: version "5.3.4" resolved "https://registry.yarnpkg.com/react-router/-/react-router-5.3.4.tgz#8ca252d70fcc37841e31473c7a151cf777887bb5" @@ -18602,6 +18615,13 @@ react-router@5.3.4: tiny-invariant "^1.0.2" tiny-warning "^1.0.0" +react-router@6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.24.1.tgz#5a3bbba0000afba68d42915456ca4c806f37a7de" + integrity sha512-PTXFXGK2pyXpHzVo3rR9H7ip4lSPZZc0bHG5CARmj65fTT6qG7sTngmb6lcYu1gf3y/8KxORoy9yn59pGpCnpg== + dependencies: + "@remix-run/router" "1.17.1" + react-select@5.4.0: version "5.4.0" resolved "https://registry.yarnpkg.com/react-select/-/react-select-5.4.0.tgz#81f6ac73906126706f104751ee14437bd16798f4" @@ -20436,7 +20456,16 @@ strict-uri-encode@^2.0.0: resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz#b9c7330c7042862f6b142dc274bbcc5866ce3546" integrity sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ== -"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -20566,7 +20595,7 @@ stringify-object@^3.2.1: is-obj "^1.0.1" is-regexp "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -20587,6 +20616,13 @@ strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: dependencies: ansi-regex "^4.1.0" +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + strip-ansi@^7.0.1: version "7.1.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" @@ -22807,7 +22843,7 @@ worker-plugin@^5.0.0: dependencies: loader-utils "^1.1.0" -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -22834,6 +22870,15 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" From 8c1f5ed308c6037c87dbd5c4b60d777a43ea5f92 Mon Sep 17 00:00:00 2001 From: Max Marrone Date: Wed, 17 Jul 2024 11:24:34 -0400 Subject: [PATCH 35/78] feat(protocol-engine): Add tryLiquidProbe to complement liquidProbe (#15667) --- .../protocol_engine/commands/__init__.py | 10 ++ .../commands/command_unions.py | 25 ++- .../protocol_engine/commands/liquid_probe.py | 144 +++++++++++++++-- .../commands/test_liquid_probe.py | 151 +++++++++++------- shared-data/command/schemas/8.json | 70 +++++++- 5 files changed, 324 insertions(+), 76 deletions(-) diff --git a/api/src/opentrons/protocol_engine/commands/__init__.py b/api/src/opentrons/protocol_engine/commands/__init__.py index 09ad591277e..75904ab00a3 100644 --- a/api/src/opentrons/protocol_engine/commands/__init__.py +++ b/api/src/opentrons/protocol_engine/commands/__init__.py @@ -331,6 +331,11 @@ LiquidProbeCreate, LiquidProbeResult, LiquidProbeCommandType, + TryLiquidProbe, + TryLiquidProbeParams, + TryLiquidProbeCreate, + TryLiquidProbeResult, + TryLiquidProbeCommandType, ) __all__ = [ @@ -580,4 +585,9 @@ "LiquidProbeCreate", "LiquidProbeResult", "LiquidProbeCommandType", + "TryLiquidProbe", + "TryLiquidProbeParams", + "TryLiquidProbeCreate", + "TryLiquidProbeResult", + "TryLiquidProbeCommandType", ] diff --git a/api/src/opentrons/protocol_engine/commands/command_unions.py b/api/src/opentrons/protocol_engine/commands/command_unions.py index 68e59d5e3c5..d20b64f363b 100644 --- a/api/src/opentrons/protocol_engine/commands/command_unions.py +++ b/api/src/opentrons/protocol_engine/commands/command_unions.py @@ -1,6 +1,7 @@ """Union types of concrete command definitions.""" -from typing import Annotated, Iterable, Type, Union, get_type_hints +from collections.abc import Collection +from typing import Annotated, Type, Union, get_type_hints from pydantic import Field @@ -313,6 +314,11 @@ LiquidProbeCreate, LiquidProbeResult, LiquidProbeCommandType, + TryLiquidProbe, + TryLiquidProbeParams, + TryLiquidProbeCreate, + TryLiquidProbeResult, + TryLiquidProbeCommandType, ) Command = Annotated[ @@ -353,6 +359,7 @@ VerifyTipPresence, GetTipPresence, LiquidProbe, + TryLiquidProbe, heater_shaker.WaitForTemperature, heater_shaker.SetTargetTemperature, heater_shaker.DeactivateHeater, @@ -421,6 +428,7 @@ VerifyTipPresenceParams, GetTipPresenceParams, LiquidProbeParams, + TryLiquidProbeParams, heater_shaker.WaitForTemperatureParams, heater_shaker.SetTargetTemperatureParams, heater_shaker.DeactivateHeaterParams, @@ -487,6 +495,7 @@ VerifyTipPresenceCommandType, GetTipPresenceCommandType, LiquidProbeCommandType, + TryLiquidProbeCommandType, heater_shaker.WaitForTemperatureCommandType, heater_shaker.SetTargetTemperatureCommandType, heater_shaker.DeactivateHeaterCommandType, @@ -554,6 +563,7 @@ VerifyTipPresenceCreate, GetTipPresenceCreate, LiquidProbeCreate, + TryLiquidProbeCreate, heater_shaker.WaitForTemperatureCreate, heater_shaker.SetTargetTemperatureCreate, heater_shaker.DeactivateHeaterCreate, @@ -622,6 +632,7 @@ VerifyTipPresenceResult, GetTipPresenceResult, LiquidProbeResult, + TryLiquidProbeResult, heater_shaker.WaitForTemperatureResult, heater_shaker.SetTargetTemperatureResult, heater_shaker.DeactivateHeaterResult, @@ -671,12 +682,20 @@ def _map_create_types_by_params_type( - create_types: Iterable[Type[CommandCreate]], + create_types: Collection[Type[CommandCreate]], ) -> dict[Type[CommandParams], Type[CommandCreate]]: def get_params_type(create_type: Type[CommandCreate]) -> Type[CommandParams]: return get_type_hints(create_type)["params"] # type: ignore[no-any-return] - return {get_params_type(create_type): create_type for create_type in create_types} + result = {get_params_type(create_type): create_type for create_type in create_types} + + # This isn't an inherent requirement of opentrons.protocol_engine, + # but this mapping is only useful to higher-level code if this holds true. + assert len(result) == len( + create_types + ), "Param models should map to create models 1:1." + + return result CREATE_TYPES_BY_PARAMS_TYPE = _map_create_types_by_params_type( diff --git a/api/src/opentrons/protocol_engine/commands/liquid_probe.py b/api/src/opentrons/protocol_engine/commands/liquid_probe.py index abac5537e73..1c24a8b2d13 100644 --- a/api/src/opentrons/protocol_engine/commands/liquid_probe.py +++ b/api/src/opentrons/protocol_engine/commands/liquid_probe.py @@ -1,4 +1,5 @@ -"""Liquid-probe command for OT3 hardware. request, result, and implementation models.""" +"""The liquidProbe and tryLiquidProbe commands.""" + from __future__ import annotations from typing import TYPE_CHECKING, Optional, Type, Union from opentrons.protocol_engine.errors.exceptions import MustHomeError, TipNotEmptyError @@ -34,16 +35,30 @@ LiquidProbeCommandType = Literal["liquidProbe"] +TryLiquidProbeCommandType = Literal["tryLiquidProbe"] + +# Both command variants should have identical parameters. +# But we need two separate parameter model classes because +# `command_unions.CREATE_TYPES_BY_PARAMS_TYPE` needs to be a 1:1 mapping. +class _CommonParams(PipetteIdMixin, WellLocationMixin): + pass -class LiquidProbeParams(PipetteIdMixin, WellLocationMixin): - """Parameters required to liquid probe a specific well.""" + +class LiquidProbeParams(_CommonParams): + """Parameters required for a `liquidProbe` command.""" + + pass + + +class TryLiquidProbeParams(_CommonParams): + """Parameters required for a `tryLiquidProbe` command.""" pass class LiquidProbeResult(DestinationPositionResult): - """Result data from the execution of a liquid-probe command.""" + """Result data from the execution of a `liquidProbe` command.""" z_position: float = Field( ..., description="The Z coordinate, in mm, of the found liquid in deck space." @@ -51,13 +66,28 @@ class LiquidProbeResult(DestinationPositionResult): # New fields should use camelCase. z_position is snake_case for historical reasons. -_ExecuteReturn = Union[ +class TryLiquidProbeResult(DestinationPositionResult): + """Result data from the execution of a `tryLiquidProbe` command.""" + + z_position: Optional[float] = Field( + ..., + description=( + "The Z coordinate, in mm, of the found liquid in deck space." + " If no liquid was found, `null` or omitted." + ), + ) + + +_LiquidProbeExecuteReturn = Union[ SuccessData[LiquidProbeResult, None], DefinedErrorData[LiquidNotFoundError, LiquidNotFoundErrorInternalData], ] +_TryLiquidProbeExecuteReturn = SuccessData[TryLiquidProbeResult, None] -class LiquidProbeImplementation(AbstractCommandImpl[LiquidProbeParams, _ExecuteReturn]): +class LiquidProbeImplementation( + AbstractCommandImpl[LiquidProbeParams, _LiquidProbeExecuteReturn] +): """The implementation of a `liquidProbe` command.""" def __init__( @@ -71,16 +101,19 @@ def __init__( self._pipetting = pipetting self._model_utils = model_utils - async def execute(self, params: LiquidProbeParams) -> _ExecuteReturn: + async def execute(self, params: _CommonParams) -> _LiquidProbeExecuteReturn: """Move to and liquid probe the requested well. Return the z-position of the found liquid. + If no liquid is found, return a LiquidNotFoundError as a defined error. Raises: - TipNotAttachedError: if there is no tip attached to the pipette - MustHomeError: if the plunger is not in a valid position - TipNotEmptyError: if the tip starts with liquid in it - LiquidNotFoundError: if liquid is not found during the probe process. + TipNotAttachedError: as an undefined error, if there is not tip attached to + the pipette. + TipNotEmptyError: as an undefined error, if the tip starts with liquid + in it. + MustHomeError: as an undefined error, if the plunger is not in a valid + position. """ pipette_id = params.pipetteId labware_id = params.labwareId @@ -139,8 +172,68 @@ async def execute(self, params: LiquidProbeParams) -> _ExecuteReturn: ) -class LiquidProbe(BaseCommand[LiquidProbeParams, LiquidProbeResult, ErrorOccurrence]): - """LiquidProbe command model.""" +class TryLiquidProbeImplementation( + AbstractCommandImpl[TryLiquidProbeParams, _TryLiquidProbeExecuteReturn] +): + """The implementation of a `tryLiquidProbe` command.""" + + def __init__( + self, + movement: MovementHandler, + pipetting: PipettingHandler, + model_utils: ModelUtils, + **kwargs: object, + ) -> None: + self._movement = movement + self._pipetting = pipetting + self._model_utils = model_utils + + async def execute(self, params: _CommonParams) -> _TryLiquidProbeExecuteReturn: + """Execute a `tryLiquidProbe` command. + + `tryLiquidProbe` is identical to `liquidProbe`, except that if no liquid is + found, `tryLiquidProbe` returns a success result with `z_position=null` instead + of a defined error. + """ + # We defer to the `liquidProbe` implementation. If it returns a defined + # `liquidNotFound` error, we remap that to a success result. + # Otherwise, we return the result or propagate the exception unchanged. + + original_impl = LiquidProbeImplementation( + movement=self._movement, + pipetting=self._pipetting, + model_utils=self._model_utils, + ) + original_result = await original_impl.execute(params) + + match original_result: + case DefinedErrorData( + public=LiquidNotFoundError(), + private=LiquidNotFoundErrorInternalData() as original_private, + ): + return SuccessData( + public=TryLiquidProbeResult( + z_position=None, + position=original_private.position, + ), + private=None, + ) + case SuccessData( + public=LiquidProbeResult() as original_public, private=None + ): + return SuccessData( + public=TryLiquidProbeResult( + position=original_public.position, + z_position=original_public.z_position, + ), + private=None, + ) + + +class LiquidProbe( + BaseCommand[LiquidProbeParams, LiquidProbeResult, LiquidNotFoundError] +): + """The model for a full `liquidProbe` command.""" commandType: LiquidProbeCommandType = "liquidProbe" params: LiquidProbeParams @@ -149,10 +242,33 @@ class LiquidProbe(BaseCommand[LiquidProbeParams, LiquidProbeResult, ErrorOccurre _ImplementationCls: Type[LiquidProbeImplementation] = LiquidProbeImplementation +class TryLiquidProbe( + BaseCommand[TryLiquidProbeParams, TryLiquidProbeResult, ErrorOccurrence] +): + """The model for a full `tryLiquidProbe` command.""" + + commandType: TryLiquidProbeCommandType = "tryLiquidProbe" + params: TryLiquidProbeParams + result: Optional[TryLiquidProbeResult] + + _ImplementationCls: Type[ + TryLiquidProbeImplementation + ] = TryLiquidProbeImplementation + + class LiquidProbeCreate(BaseCommandCreate[LiquidProbeParams]): - """Create LiquidProbe command request model.""" + """The request model for a `liquidProbe` command.""" commandType: LiquidProbeCommandType = "liquidProbe" params: LiquidProbeParams _CommandCls: Type[LiquidProbe] = LiquidProbe + + +class TryLiquidProbeCreate(BaseCommandCreate[TryLiquidProbeParams]): + """The request model for a `tryLiquidProbe` command.""" + + commandType: TryLiquidProbeCommandType = "tryLiquidProbe" + params: TryLiquidProbeParams + + _CommandCls: Type[TryLiquidProbe] = TryLiquidProbe diff --git a/api/tests/opentrons/protocol_engine/commands/test_liquid_probe.py b/api/tests/opentrons/protocol_engine/commands/test_liquid_probe.py index dddf1ca5e06..d5bc6571214 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_liquid_probe.py +++ b/api/tests/opentrons/protocol_engine/commands/test_liquid_probe.py @@ -1,5 +1,6 @@ """Test LiquidProbe commands.""" from datetime import datetime +from typing import Type, Union from opentrons.protocol_engine.errors.exceptions import ( MustHomeError, @@ -23,6 +24,9 @@ LiquidProbeParams, LiquidProbeResult, LiquidProbeImplementation, + TryLiquidProbeParams, + TryLiquidProbeResult, + TryLiquidProbeImplementation, ) from opentrons.protocol_engine.commands.command import DefinedErrorData, SuccessData @@ -36,14 +40,56 @@ from opentrons.protocol_engine.types import LoadedPipette +EitherImplementationType = Union[ + Type[LiquidProbeImplementation], Type[TryLiquidProbeImplementation] +] +EitherImplementation = Union[LiquidProbeImplementation, TryLiquidProbeImplementation] +EitherParamsType = Union[Type[LiquidProbeParams], Type[TryLiquidProbeParams]] +EitherResultType = Union[Type[LiquidProbeResult], Type[TryLiquidProbeResult]] + + +@pytest.fixture( + params=[ + (LiquidProbeImplementation, LiquidProbeParams, LiquidProbeResult), + (TryLiquidProbeImplementation, TryLiquidProbeParams, TryLiquidProbeResult), + ] +) +def types( + request: pytest.FixtureRequest, +) -> tuple[EitherImplementationType, EitherParamsType, EitherResultType]: + """Return a tuple of types associated with a single variant of the command.""" + return request.param # type: ignore[no-any-return] + + +@pytest.fixture +def implementation_type( + types: tuple[EitherImplementationType, object, object] +) -> EitherImplementationType: + """Return an implementation type. Kept in sync with the params and result types.""" + return types[0] + + +@pytest.fixture +def params_type(types: tuple[object, EitherParamsType, object]) -> EitherParamsType: + """Return a params type. Kept in sync with the implementation and result types.""" + return types[1] + + +@pytest.fixture +def result_type(types: tuple[object, object, EitherResultType]) -> EitherResultType: + """Return a result type. Kept in sync with the implementation and params types.""" + return types[2] + + @pytest.fixture def subject( + implementation_type: EitherImplementationType, movement: MovementHandler, pipetting: PipettingHandler, model_utils: ModelUtils, -) -> LiquidProbeImplementation: +) -> Union[LiquidProbeImplementation, TryLiquidProbeImplementation]: """Get the implementation subject.""" - return LiquidProbeImplementation( + return implementation_type( pipetting=pipetting, movement=movement, model_utils=model_utils, @@ -54,12 +100,14 @@ async def test_liquid_probe_implementation_no_prep( decoy: Decoy, movement: MovementHandler, pipetting: PipettingHandler, - subject: LiquidProbeImplementation, + subject: EitherImplementation, + params_type: EitherParamsType, + result_type: EitherResultType, ) -> None: """A Liquid Probe should have an execution implementation without preparing to aspirate.""" location = WellLocation(origin=WellOrigin.BOTTOM, offset=WellOffset(x=0, y=0, z=1)) - data = LiquidProbeParams( + data = params_type( pipetteId="abc", labwareId="123", wellName="A3", @@ -87,8 +135,9 @@ async def test_liquid_probe_implementation_no_prep( result = await subject.execute(data) + assert type(result.public) is result_type # Pydantic v1 only compares the fields. assert result == SuccessData( - public=LiquidProbeResult(z_position=15.0, position=DeckPoint(x=1, y=2, z=3)), + public=result_type(z_position=15.0, position=DeckPoint(x=1, y=2, z=3)), private=None, ) @@ -98,12 +147,14 @@ async def test_liquid_probe_implementation_with_prep( state_view: StateView, movement: MovementHandler, pipetting: PipettingHandler, - subject: LiquidProbeImplementation, + subject: EitherImplementation, + params_type: EitherParamsType, + result_type: EitherResultType, ) -> None: """A Liquid Probe should have an execution implementation with preparing to aspirate.""" location = WellLocation(origin=WellOrigin.TOP, offset=WellOffset(x=0, y=0, z=0)) - data = LiquidProbeParams( + data = params_type( pipetteId="abc", labwareId="123", wellName="A3", @@ -133,26 +184,19 @@ async def test_liquid_probe_implementation_with_prep( result = await subject.execute(data) + assert type(result.public) is result_type # Pydantic v1 only compares the fields. assert result == SuccessData( - public=LiquidProbeResult(z_position=15.0, position=DeckPoint(x=1, y=2, z=3)), + public=result_type(z_position=15.0, position=DeckPoint(x=1, y=2, z=3)), private=None, ) - decoy.verify( - await movement.move_to_well( - pipette_id="abc", - labware_id="123", - well_name="A3", - well_location=WellLocation(origin=WellOrigin.TOP), - ), - ) - async def test_liquid_not_found_error( decoy: Decoy, movement: MovementHandler, pipetting: PipettingHandler, - subject: LiquidProbeImplementation, + subject: EitherImplementation, + params_type: EitherParamsType, model_utils: ModelUtils, ) -> None: """It should return a liquid not found error if the hardware API indicates that.""" @@ -168,7 +212,7 @@ async def test_liquid_not_found_error( error_id = "error-id" error_timestamp = datetime(year=2020, month=1, day=2) - data = LiquidProbeParams( + data = params_type( pipetteId=pipette_id, labwareId=labware_id, wellName=well_name, @@ -201,22 +245,32 @@ async def test_liquid_not_found_error( result = await subject.execute(data) - assert result == DefinedErrorData( - public=LiquidNotFoundError.construct( - id=error_id, createdAt=error_timestamp, wrappedErrors=[matchers.Anything()] - ), - private=LiquidNotFoundErrorInternalData( - position=DeckPoint(x=position.x, y=position.y, z=position.z) - ), - ) + if isinstance(subject, LiquidProbeImplementation): + assert result == DefinedErrorData( + public=LiquidNotFoundError.construct( + id=error_id, + createdAt=error_timestamp, + wrappedErrors=[matchers.Anything()], + ), + private=LiquidNotFoundErrorInternalData( + position=DeckPoint(x=position.x, y=position.y, z=position.z) + ), + ) + else: + assert result == SuccessData( + public=TryLiquidProbeResult( + z_position=None, + position=DeckPoint(x=position.x, y=position.y, z=position.z), + ), + private=None, + ) async def test_liquid_probe_tip_checking( decoy: Decoy, - movement: MovementHandler, pipetting: PipettingHandler, - subject: LiquidProbeImplementation, - model_utils: ModelUtils, + subject: EitherImplementation, + params_type: EitherParamsType, ) -> None: """It should return a TipNotAttached error if the hardware API indicates that.""" pipette_id = "pipette-id" @@ -226,7 +280,7 @@ async def test_liquid_probe_tip_checking( origin=WellOrigin.BOTTOM, offset=WellOffset(x=0, y=0, z=1) ) - data = LiquidProbeParams( + data = params_type( pipetteId=pipette_id, labwareId=labware_id, wellName=well_name, @@ -238,21 +292,15 @@ async def test_liquid_probe_tip_checking( pipette_id=pipette_id, ), ).then_raise(TipNotAttachedError()) - try: + with pytest.raises(TipNotAttachedError): await subject.execute(data) - assert False - except TipNotAttachedError: - assert True - except Exception: - assert False async def test_liquid_probe_volume_checking( decoy: Decoy, - movement: MovementHandler, pipetting: PipettingHandler, - subject: LiquidProbeImplementation, - model_utils: ModelUtils, + subject: EitherImplementation, + params_type: EitherParamsType, ) -> None: """It should return a TipNotEmptyError if the hardware API indicates that.""" pipette_id = "pipette-id" @@ -262,7 +310,7 @@ async def test_liquid_probe_volume_checking( origin=WellOrigin.BOTTOM, offset=WellOffset(x=0, y=0, z=1) ) - data = LiquidProbeParams( + data = params_type( pipetteId=pipette_id, labwareId=labware_id, wellName=well_name, @@ -271,21 +319,15 @@ async def test_liquid_probe_volume_checking( decoy.when( pipetting.get_is_empty(pipette_id=pipette_id), ).then_return(False) - try: + with pytest.raises(TipNotEmptyError): await subject.execute(data) - assert False - except TipNotEmptyError: - assert True - except Exception: - assert False async def test_liquid_probe_location_checking( decoy: Decoy, movement: MovementHandler, - pipetting: PipettingHandler, - subject: LiquidProbeImplementation, - model_utils: ModelUtils, + subject: EitherImplementation, + params_type: EitherParamsType, ) -> None: """It should return a PositionUnkownError if the hardware API indicates that.""" pipette_id = "pipette-id" @@ -295,7 +337,7 @@ async def test_liquid_probe_location_checking( origin=WellOrigin.BOTTOM, offset=WellOffset(x=0, y=0, z=1) ) - data = LiquidProbeParams( + data = params_type( pipetteId=pipette_id, labwareId=labware_id, wellName=well_name, @@ -306,10 +348,5 @@ async def test_liquid_probe_location_checking( mount=MountType.LEFT, ), ).then_return(False) - try: + with pytest.raises(MustHomeError): await subject.execute(data) - assert False - except MustHomeError: - assert True - except Exception: - assert False diff --git a/shared-data/command/schemas/8.json b/shared-data/command/schemas/8.json index 4700ace9229..d985fc7b902 100644 --- a/shared-data/command/schemas/8.json +++ b/shared-data/command/schemas/8.json @@ -41,6 +41,7 @@ "verifyTipPresence": "#/definitions/VerifyTipPresenceCreate", "getTipPresence": "#/definitions/GetTipPresenceCreate", "liquidProbe": "#/definitions/LiquidProbeCreate", + "tryLiquidProbe": "#/definitions/TryLiquidProbeCreate", "heaterShaker/waitForTemperature": "#/definitions/opentrons__protocol_engine__commands__heater_shaker__wait_for_temperature__WaitForTemperatureCreate", "heaterShaker/setTargetTemperature": "#/definitions/opentrons__protocol_engine__commands__heater_shaker__set_target_temperature__SetTargetTemperatureCreate", "heaterShaker/deactivateHeater": "#/definitions/DeactivateHeaterCreate", @@ -179,6 +180,9 @@ { "$ref": "#/definitions/LiquidProbeCreate" }, + { + "$ref": "#/definitions/TryLiquidProbeCreate" + }, { "$ref": "#/definitions/opentrons__protocol_engine__commands__heater_shaker__wait_for_temperature__WaitForTemperatureCreate" }, @@ -2773,7 +2777,7 @@ }, "LiquidProbeParams": { "title": "LiquidProbeParams", - "description": "Parameters required to liquid probe a specific well.", + "description": "Parameters required for a `liquidProbe` command.", "type": "object", "properties": { "labwareId": { @@ -2805,7 +2809,7 @@ }, "LiquidProbeCreate": { "title": "LiquidProbeCreate", - "description": "Create LiquidProbe command request model.", + "description": "The request model for a `liquidProbe` command.", "type": "object", "properties": { "commandType": { @@ -2833,6 +2837,68 @@ }, "required": ["params"] }, + "TryLiquidProbeParams": { + "title": "TryLiquidProbeParams", + "description": "Parameters required for a `tryLiquidProbe` command.", + "type": "object", + "properties": { + "labwareId": { + "title": "Labwareid", + "description": "Identifier of labware to use.", + "type": "string" + }, + "wellName": { + "title": "Wellname", + "description": "Name of well to use in labware.", + "type": "string" + }, + "wellLocation": { + "title": "Welllocation", + "description": "Relative well location at which to perform the operation", + "allOf": [ + { + "$ref": "#/definitions/WellLocation" + } + ] + }, + "pipetteId": { + "title": "Pipetteid", + "description": "Identifier of pipette to use for liquid handling.", + "type": "string" + } + }, + "required": ["labwareId", "wellName", "pipetteId"] + }, + "TryLiquidProbeCreate": { + "title": "TryLiquidProbeCreate", + "description": "The request model for a `tryLiquidProbe` command.", + "type": "object", + "properties": { + "commandType": { + "title": "Commandtype", + "default": "tryLiquidProbe", + "enum": ["tryLiquidProbe"], + "type": "string" + }, + "params": { + "$ref": "#/definitions/TryLiquidProbeParams" + }, + "intent": { + "description": "The reason the command was added. If not specified or `protocol`, the command will be treated as part of the protocol run itself, and added to the end of the existing command queue.\n\nIf `setup`, the command will be treated as part of run setup. A setup command may only be enqueued if the run has not started.\n\nUse setup commands for activities like pre-run calibration checks and module setup, like pre-heating.", + "allOf": [ + { + "$ref": "#/definitions/CommandIntent" + } + ] + }, + "key": { + "title": "Key", + "description": "A key value, unique in this run, that can be used to track the same logical command across multiple runs of the same protocol. If a value is not provided, one will be generated.", + "type": "string" + } + }, + "required": ["params"] + }, "opentrons__protocol_engine__commands__heater_shaker__wait_for_temperature__WaitForTemperatureParams": { "title": "WaitForTemperatureParams", "description": "Input parameters to wait for a Heater-Shaker's target temperature.", From 3ca7c0aec0f0a0b72b5b4e586d1f9ed26cd88267 Mon Sep 17 00:00:00 2001 From: Ryan Howard Date: Wed, 17 Jul 2024 12:39:04 -0400 Subject: [PATCH 36/78] feat(hardware-testing): add testing teams feature requests (#15652) # Overview - Makes the script copy the exact way that Protocol engine calls the ot3api liquid probe - Remove the Radwag vial as a default, since multiprobe with an empty vial can result in a collision and they're fragile - Adds a flag that does a "pre-wet" of the tip to test using the probe with wet tips - adds a flag to override the plunger's solo move time to test that part of the noise reduction - adds my implementation of "blockage" detection to print that out in the results csv # Test Plan # Changelog # Review requests # Risk assessment --- api/src/opentrons/hardware_control/ot3api.py | 2 + .../protocol_api/core/engine/instrument.py | 4 +- .../protocol_engine/commands/liquid_probe.py | 5 +- .../protocol_engine/execution/pipetting.py | 8 ++- .../hardware_control/test_ot3_api.py | 8 +-- .../core/engine/test_instrument_core.py | 9 +-- .../commands/test_liquid_probe.py | 16 ++++- .../hardware_testing/gravimetric/config.py | 37 ++++-------- .../hardware_testing/gravimetric/helpers.py | 2 +- .../hardware_testing/liquid_sense/__main__.py | 23 +++++--- .../hardware_testing/liquid_sense/execute.py | 59 +++++++++++++++---- .../liquid_sense/post_process.py | 10 +++- .../hardware_testing/liquid_sense/report.py | 10 +++- ...l.py => liquid_sense_ot3_p1000_96_well.py} | 2 +- .../liquid_sense_ot3_p50_multi.py | 29 +++++++++ 15 files changed, 156 insertions(+), 68 deletions(-) rename hardware-testing/hardware_testing/protocols/liquid_sense_lpc/{liquid_sense_ot3_p1000_single_vial.py => liquid_sense_ot3_p1000_96_well.py} (95%) create mode 100644 hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p50_multi.py diff --git a/api/src/opentrons/hardware_control/ot3api.py b/api/src/opentrons/hardware_control/ot3api.py index 32f640269fe..6d567b7a667 100644 --- a/api/src/opentrons/hardware_control/ot3api.py +++ b/api/src/opentrons/hardware_control/ot3api.py @@ -2666,6 +2666,8 @@ async def liquid_probe( except PipetteLiquidNotFoundError as lnfe: error = lnfe pos = await self.gantry_position(checked_mount, refresh=True) + await self.move_to(checked_mount, probe_start_pos + top_types.Point(z=2)) + await self.prepare_for_aspirate(checked_mount) await self.move_to(checked_mount, probe_start_pos) if error is not None: # if we never found liquid raise an error diff --git a/api/src/opentrons/protocol_api/core/engine/instrument.py b/api/src/opentrons/protocol_api/core/engine/instrument.py index 71bc5784671..c2cc70f39f3 100644 --- a/api/src/opentrons/protocol_api/core/engine/instrument.py +++ b/api/src/opentrons/protocol_api/core/engine/instrument.py @@ -842,7 +842,7 @@ def liquid_probe_with_recovery(self, well_core: WellCore, loc: Location) -> None labware_id = well_core.labware_id well_name = well_core.get_name() well_location = WellLocation( - origin=WellOrigin.TOP, offset=WellOffset(x=0, y=0, z=0) + origin=WellOrigin.TOP, offset=WellOffset(x=0, y=0, z=2) ) self._engine_client.execute_command( cmd.LiquidProbeParams( @@ -861,7 +861,7 @@ def liquid_probe_without_recovery( labware_id = well_core.labware_id well_name = well_core.get_name() well_location = WellLocation( - origin=WellOrigin.TOP, offset=WellOffset(x=0, y=0, z=0) + origin=WellOrigin.TOP, offset=WellOffset(x=0, y=0, z=2) ) result = self._engine_client.execute_command_without_recovery( cmd.LiquidProbeParams( diff --git a/api/src/opentrons/protocol_engine/commands/liquid_probe.py b/api/src/opentrons/protocol_engine/commands/liquid_probe.py index 1c24a8b2d13..ecf932a3470 100644 --- a/api/src/opentrons/protocol_engine/commands/liquid_probe.py +++ b/api/src/opentrons/protocol_engine/commands/liquid_probe.py @@ -143,7 +143,10 @@ async def execute(self, params: _CommonParams) -> _LiquidProbeExecuteReturn: try: z_pos = await self._pipetting.liquid_probe_in_place( - pipette_id=pipette_id, labware_id=labware_id, well_name=well_name + pipette_id=pipette_id, + labware_id=labware_id, + well_name=well_name, + well_location=params.wellLocation, ) except PipetteLiquidNotFoundError as e: return DefinedErrorData( diff --git a/api/src/opentrons/protocol_engine/execution/pipetting.py b/api/src/opentrons/protocol_engine/execution/pipetting.py index 24e45f6c3ad..c3e606849ff 100644 --- a/api/src/opentrons/protocol_engine/execution/pipetting.py +++ b/api/src/opentrons/protocol_engine/execution/pipetting.py @@ -13,7 +13,7 @@ InvalidPushOutVolumeError, InvalidDispenseVolumeError, ) - +from opentrons.protocol_engine.types import WellLocation # 1e-9 µL (1 femtoliter!) is a good value because: # * It's large relative to rounding errors that occur in practice in protocols. For @@ -68,6 +68,7 @@ async def liquid_probe_in_place( pipette_id: str, labware_id: str, well_name: str, + well_location: WellLocation, ) -> float: """Detect liquid level.""" @@ -176,6 +177,7 @@ async def liquid_probe_in_place( pipette_id: str, labware_id: str, well_name: str, + well_location: WellLocation, ) -> float: """Detect liquid level.""" hw_pipette = self._state_view.pipettes.get_hardware_pipette( @@ -188,7 +190,8 @@ async def liquid_probe_in_place( pipette_id=pipette_id ) z_pos = await self._hardware_api.liquid_probe( - mount=hw_pipette.mount, max_z_dist=well_depth - lld_min_height + mount=hw_pipette.mount, + max_z_dist=well_depth - lld_min_height + well_location.offset.z, ) return float(z_pos) @@ -290,6 +293,7 @@ async def liquid_probe_in_place( pipette_id: str, labware_id: str, well_name: str, + well_location: WellLocation, ) -> float: """Detect liquid level.""" # TODO (pm, 6-18-24): return a value of worth if needed diff --git a/api/tests/opentrons/hardware_control/test_ot3_api.py b/api/tests/opentrons/hardware_control/test_ot3_api.py index 020e3d0dc3c..2b77ebdcd00 100644 --- a/api/tests/opentrons/hardware_control/test_ot3_api.py +++ b/api/tests/opentrons/hardware_control/test_ot3_api.py @@ -835,7 +835,7 @@ async def test_liquid_probe( ) fake_max_z_dist = 10.0 await ot3_hardware.liquid_probe(mount, fake_max_z_dist, fake_settings_aspirate) - mock_move_to_plunger_bottom.assert_called_once() + mock_move_to_plunger_bottom.call_count == 2 mock_liquid_probe.assert_called_once_with( mount, 3.0, @@ -910,7 +910,7 @@ async def test_multi_liquid_probe( await ot3_hardware.liquid_probe( OT3Mount.LEFT, fake_max_z_dist, fake_settings_aspirate ) - assert mock_move_to_plunger_bottom.call_count == 3 + assert mock_move_to_plunger_bottom.call_count == 4 mock_liquid_probe.assert_called_with( OT3Mount.LEFT, plunger_positions.bottom - plunger_positions.top, @@ -986,8 +986,8 @@ async def _fake_pos_update_and_raise( await ot3_hardware.liquid_probe( OT3Mount.LEFT, fake_max_z_dist, fake_settings_aspirate ) - # assert that it went through 3 passes - assert mock_move_to_plunger_bottom.call_count == 3 + # assert that it went through 3 passes and then prepared to aspirate + assert mock_move_to_plunger_bottom.call_count == 4 @pytest.mark.parametrize( diff --git a/api/tests/opentrons/protocol_api/core/engine/test_instrument_core.py b/api/tests/opentrons/protocol_api/core/engine/test_instrument_core.py index 6baba13757c..ac53bb55a59 100644 --- a/api/tests/opentrons/protocol_api/core/engine/test_instrument_core.py +++ b/api/tests/opentrons/protocol_api/core/engine/test_instrument_core.py @@ -1340,12 +1340,7 @@ def test_liquid_probe_without_recovery( ) ).then_raise(PipetteLiquidNotFoundError()) loc = Location(Point(0, 0, 0), None) - try: - subject.liquid_probe_without_recovery(well_core=well_core, loc=loc) - except PipetteLiquidNotFoundError: - assert True - else: - assert False + subject.liquid_probe_without_recovery(well_core=well_core, loc=loc) @pytest.mark.parametrize("version", versions_at_or_above(APIVersion(2, 20))) @@ -1367,7 +1362,7 @@ def test_liquid_probe_with_recovery( cmd.LiquidProbeParams( pipetteId=subject.pipette_id, wellLocation=WellLocation( - origin=WellOrigin.TOP, offset=WellOffset(x=0, y=0, z=0) + origin=WellOrigin.TOP, offset=WellOffset(x=0, y=0, z=2.0) ), wellName=well_core.get_name(), labwareId=well_core.labware_id, diff --git a/api/tests/opentrons/protocol_engine/commands/test_liquid_probe.py b/api/tests/opentrons/protocol_engine/commands/test_liquid_probe.py index d5bc6571214..61f4339360d 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_liquid_probe.py +++ b/api/tests/opentrons/protocol_engine/commands/test_liquid_probe.py @@ -130,6 +130,7 @@ async def test_liquid_probe_implementation_no_prep( pipette_id="abc", labware_id="123", well_name="A3", + well_location=location, ), ).then_return(15.0) @@ -152,7 +153,7 @@ async def test_liquid_probe_implementation_with_prep( result_type: EitherResultType, ) -> None: """A Liquid Probe should have an execution implementation with preparing to aspirate.""" - location = WellLocation(origin=WellOrigin.TOP, offset=WellOffset(x=0, y=0, z=0)) + location = WellLocation(origin=WellOrigin.TOP, offset=WellOffset(x=0, y=0, z=2)) data = params_type( pipetteId="abc", @@ -179,6 +180,7 @@ async def test_liquid_probe_implementation_with_prep( pipette_id="abc", labware_id="123", well_name="A3", + well_location=location, ), ).then_return(15.0) @@ -190,6 +192,17 @@ async def test_liquid_probe_implementation_with_prep( private=None, ) + decoy.verify( + await movement.move_to_well( + pipette_id="abc", + labware_id="123", + well_name="A3", + well_location=WellLocation( + origin=WellOrigin.TOP, offset=WellOffset(x=0, y=0, z=2) + ), + ), + ) + async def test_liquid_not_found_error( decoy: Decoy, @@ -237,6 +250,7 @@ async def test_liquid_not_found_error( pipette_id=pipette_id, labware_id=labware_id, well_name=well_name, + well_location=well_location, ), ).then_raise(PipetteLiquidNotFoundError()) diff --git a/hardware-testing/hardware_testing/gravimetric/config.py b/hardware-testing/hardware_testing/gravimetric/config.py index 67a24d08171..53663fdd614 100644 --- a/hardware-testing/hardware_testing/gravimetric/config.py +++ b/hardware-testing/hardware_testing/gravimetric/config.py @@ -90,17 +90,15 @@ class PhotometricConfig(VolumetricConfig): 50: { 1: { 50: { - "max_z_distance": 20, - "mount_speed": 11, - "plunger_speed": 21, + "mount_speed": 5, + "plunger_speed": 20, "sensor_threshold_pascals": 15, }, }, 8: { 50: { - "max_z_distance": 20, - "mount_speed": 11, - "plunger_speed": 21, + "mount_speed": 5, + "plunger_speed": 20, "sensor_threshold_pascals": 15, }, }, @@ -108,61 +106,52 @@ class PhotometricConfig(VolumetricConfig): 1000: { 1: { 50: { - "max_z_distance": 20, "mount_speed": 5, - "plunger_speed": 10, + "plunger_speed": 20, "sensor_threshold_pascals": 15, }, 200: { - "max_z_distance": 20, "mount_speed": 5, - "plunger_speed": 10, + "plunger_speed": 20, "sensor_threshold_pascals": 15, }, 1000: { - "max_z_distance": 20, "mount_speed": 5, - "plunger_speed": 11, + "plunger_speed": 20, "sensor_threshold_pascals": 15, }, }, 8: { 50: { - "max_z_distance": 20, "mount_speed": 5, - "plunger_speed": 10, + "plunger_speed": 20, "sensor_threshold_pascals": 15, }, 200: { - "max_z_distance": 20, "mount_speed": 5, - "plunger_speed": 10, + "plunger_speed": 20, "sensor_threshold_pascals": 15, }, 1000: { - "max_z_distance": 20, "mount_speed": 5, - "plunger_speed": 11, + "plunger_speed": 20, "sensor_threshold_pascals": 15, }, }, 96: { 50: { - "max_z_distance": 20, "mount_speed": 5, - "plunger_speed": 10, + "plunger_speed": 20, "sensor_threshold_pascals": 15, }, 200: { - "max_z_distance": 20, "mount_speed": 5, - "plunger_speed": 10, + "plunger_speed": 20, "sensor_threshold_pascals": 15, }, 1000: { - "max_z_distance": 20, "mount_speed": 5, - "plunger_speed": 11, + "plunger_speed": 20, "sensor_threshold_pascals": 15, }, }, diff --git a/hardware-testing/hardware_testing/gravimetric/helpers.py b/hardware-testing/hardware_testing/gravimetric/helpers.py index f63928e4893..31541d59f5a 100644 --- a/hardware-testing/hardware_testing/gravimetric/helpers.py +++ b/hardware-testing/hardware_testing/gravimetric/helpers.py @@ -168,7 +168,7 @@ def _jog_to_find_liquid_height( ctx: ProtocolContext, pipette: InstrumentContext, well: Well ) -> float: _well_depth = well.depth - _liquid_height = _well_depth + _liquid_height = _well_depth + 2 _jog_size = -1.0 if ctx.is_simulating(): return _liquid_height - 1 diff --git a/hardware-testing/hardware_testing/liquid_sense/__main__.py b/hardware-testing/hardware_testing/liquid_sense/__main__.py index b35da3a76ba..ca0b632290c 100644 --- a/hardware-testing/hardware_testing/liquid_sense/__main__.py +++ b/hardware-testing/hardware_testing/liquid_sense/__main__.py @@ -6,7 +6,7 @@ import subprocess from time import sleep import os -from typing import List, Any, Optional +from typing import List, Any, Optional, Dict import traceback import sys @@ -27,6 +27,7 @@ get_testing_data_directory, ) from opentrons_hardware.hardware_control.motion_planning import move_utils +from opentrons_hardware.hardware_control import tool_sensors from opentrons.protocol_api import InstrumentContext, ProtocolContext from opentrons.protocol_engine.types import LabwareOffset @@ -37,7 +38,8 @@ from hardware_testing.protocols.liquid_sense_lpc import ( liquid_sense_ot3_p50_single_vial, - liquid_sense_ot3_p1000_single_vial, + liquid_sense_ot3_p1000_96_well, + liquid_sense_ot3_p50_multi, ) try: @@ -67,13 +69,13 @@ MAX_PROBE_SECONDS = 3.5 -LIQUID_SENSE_CFG = { +LIQUID_SENSE_CFG: Dict[int, Dict[int, Any]] = { 50: { 1: liquid_sense_ot3_p50_single_vial, - 8: None, + 8: liquid_sense_ot3_p50_multi, }, 1000: { - 1: liquid_sense_ot3_p1000_single_vial, + 1: liquid_sense_ot3_p1000_96_well, 8: None, 96: None, }, @@ -118,6 +120,7 @@ class RunArgs: trials_before_jog: int no_multi_pass: int test_well: str + wet: bool @classmethod def _get_protocol_context(cls, args: argparse.Namespace) -> ProtocolContext: @@ -227,6 +230,7 @@ def build_run_args(cls, args: argparse.Namespace) -> "RunArgs": protocol_cfg.LABWARE_ON_SCALE, # type: ignore[union-attr] args.z_speed, ) + tool_sensors.PLUNGER_SOLO_MOVE_TIME = args.p_solo_time return RunArgs( tip_volumes=tip_volumes, run_id=run_id, @@ -250,6 +254,7 @@ def build_run_args(cls, args: argparse.Namespace) -> "RunArgs": trials_before_jog=args.trials_before_jog, no_multi_pass=args.no_multi_pass, test_well=args.test_well, + wet=args.wet, ) @@ -265,12 +270,16 @@ def build_run_args(cls, args: argparse.Namespace) -> "RunArgs": parser.add_argument("--return-tip", action="store_true") parser.add_argument("--trials", type=int, default=7) parser.add_argument("--trials-before-jog", type=int, default=7) - parser.add_argument("--z-speed", type=float, default=1) + parser.add_argument("--z-speed", type=float, default=5) parser.add_argument("--aspirate", action="store_true") - parser.add_argument("--plunger-speed", type=float, default=-1.0) + parser.add_argument("--plunger-speed", type=float, default=20) parser.add_argument("--no-multi-pass", action="store_true") + parser.add_argument("--wet", action="store_true") parser.add_argument("--starting-tip", type=str, default="A1") parser.add_argument("--test-well", type=str, default="A1") + parser.add_argument( + "--p-solo-time", type=float, default=tool_sensors.PLUNGER_SOLO_MOVE_TIME + ) parser.add_argument("--google-sheet-name", type=str, default="LLD-Shared-Data") parser.add_argument( "--gd-parent-folder", type=str, default="1b2V85fDPA0tNqjEhyHOGCWRZYgn8KsGf" diff --git a/hardware-testing/hardware_testing/liquid_sense/execute.py b/hardware-testing/hardware_testing/liquid_sense/execute.py index 679a7306967..05f5165b020 100644 --- a/hardware-testing/hardware_testing/liquid_sense/execute.py +++ b/hardware-testing/hardware_testing/liquid_sense/execute.py @@ -1,4 +1,6 @@ """Logic for running a single liquid probe test.""" +import csv +from enum import Enum from typing import Dict, Any, List, Tuple, Optional from .report import store_tip_results, store_trial, store_baseline_trial from opentrons.config.types import LiquidProbeSettings, OutputOptions @@ -16,6 +18,7 @@ Axis, top_types, ) +from opentrons.hardware_control.dev_types import PipetteDict from hardware_testing.gravimetric.measurement.scale import Scale from hardware_testing.gravimetric.measurement.record import ( @@ -39,6 +42,14 @@ pass +class LLDResult(Enum): + """Result Strings.""" + + success = "success" + not_found = "not found" + blockage = "blockage" + + def _load_tipracks( ctx: ProtocolContext, pipette_channels: int, protocol_cfg: Any, tip: int ) -> List[Labware]: @@ -257,11 +268,13 @@ def _get_target_height() -> None: run_args.pipette.pick_up_tip(tips[0]) del tips[: run_args.pipette_channels] - run_args.pipette.move_to(test_well.top()) + run_args.pipette.move_to(test_well.top(z=2)) + if run_args.wet: + run_args.pipette.move_to(test_well.bottom(1)) + run_args.pipette.move_to(test_well.top(z=2)) start_pos = hw_api.current_position_ot3(OT3Mount.LEFT) - height = _run_trial(run_args, tip, test_well, trial, start_pos) + height, result = _run_trial(run_args, tip, test_well, trial, start_pos) end_pos = hw_api.current_position_ot3(OT3Mount.LEFT) - run_args.pipette.blow_out() tip_length_offset = 0.0 if run_args.dial_indicator is not None: run_args.pipette._retract() @@ -297,6 +310,7 @@ def _get_target_height() -> None: plunger_start - end_pos[Axis.P_L], tip_length_offset, liquid_height_from_deck, + result.value, google_sheet, run_args.run_id, sheet_id, @@ -335,14 +349,17 @@ def find_max_z_distances( """ hw_mount = OT3Mount.LEFT if run_args.pipette.mount == "left" else OT3Mount.RIGHT hw_api = get_sync_hw_api(run_args.ctx) - lld_settings = hw_api._pipette_handler.get_pipette(hw_mount).lld_settings - - z_speed = run_args.z_speed - max_z_distance = ( - well.top().point.z - - well.bottom().point.z - - lld_settings[f"t{int(tip)}"]["minHeight"] + attached_instrument: PipetteDict = hw_api._pipette_handler.get_attached_instrument( + hw_mount ) + lld_settings = attached_instrument["lld_settings"] + z_speed = run_args.z_speed + if lld_settings is not None: + min_height = lld_settings[f"t{int(tip)}"]["minHeight"] + else: + ui.print_warning("No minimum height for pipette") + min_height = 0.5 + max_z_distance = well.top().point.z - well.bottom().point.z - min_height + 2 plunger_travel = get_plunger_travel(run_args) if p_speed == 0: p_travel_time = 10.0 @@ -357,13 +374,23 @@ def find_max_z_distances( return z_travels +def _test_for_blockage(datafile: str, threshold: float) -> bool: + with open(datafile, "r") as file: + reader = csv.reader(file) + reader_list = list(reader) + for i in range(1, len(reader_list)): + if i > 1 and abs(float(reader_list[i][1])) > threshold: + return abs(float(reader_list[i][1]) - float(reader_list[i - 1][1])) > 40 + return False + + def _run_trial( run_args: RunArgs, tip: int, well: Well, trial: int, start_pos: Dict[Axis, float], -) -> float: +) -> Tuple[float, LLDResult]: hw_api = get_sync_hw_api(run_args.ctx) lqid_cfg: Dict[str, int] = LIQUID_PROBE_SETTINGS[run_args.pipette_volume][ run_args.pipette_channels @@ -410,9 +437,17 @@ def _run_trial( # TODO add in stuff for secondary probe try: height = hw_api.liquid_probe(hw_mount, z_distance, lps, probe_target) + result: LLDResult = LLDResult.success + if not run_args.ctx.is_simulating(): + for probe in data_files: + if _test_for_blockage(data_files[probe], lps.sensor_threshold_pascals): + result = LLDResult.blockage + break except PipetteLiquidNotFoundError as lnf: ui.print_info(f"Liquid not found current position {lnf.detail}") + result = LLDResult.not_found + run_args.recorder.clear_sample_tag() ui.print_info(f"Trial {trial} complete") - return height + return height, result diff --git a/hardware-testing/hardware_testing/liquid_sense/post_process.py b/hardware-testing/hardware_testing/liquid_sense/post_process.py index 0c4e80df713..cebfc014f17 100644 --- a/hardware-testing/hardware_testing/liquid_sense/post_process.py +++ b/hardware-testing/hardware_testing/liquid_sense/post_process.py @@ -34,6 +34,8 @@ 13: "AO", } +BASELINE_TRIAL_LINE_NUMBER = 43 + def _get_pressure_results(result_file: str) -> Tuple[float, float, float, List[float]]: z_velocity: float = 0.0 @@ -137,11 +139,13 @@ def process_csv_directory( # noqa: C901 for row in summary_reader: final_report_writer.writerow(row) s += 1 - if s == 44: + if s == BASELINE_TRIAL_LINE_NUMBER: meniscus_travel = float(row[6]) - if s >= 45 and s < 45 + (trials * len(tips)): + if s >= (BASELINE_TRIAL_LINE_NUMBER + 1) and s < ( + BASELINE_TRIAL_LINE_NUMBER + 1 + (trials * len(tips)) + ): # while processing this grab the tip offsets from the summary - tip_offsets[tips[int((s - 45) / trials)]].append(float(row[8])) + tip_offsets[tips[int((s - 44) / trials)]].append(float(row[8])) # summary_reader.line_num is the last line in the summary that has text pressures_start_line = summary_reader.line_num + 3 # calculate where the start and end of each block of data we want to graph diff --git a/hardware-testing/hardware_testing/liquid_sense/report.py b/hardware-testing/hardware_testing/liquid_sense/report.py index f7b0b5b7b16..c68f679cf00 100644 --- a/hardware-testing/hardware_testing/liquid_sense/report.py +++ b/hardware-testing/hardware_testing/liquid_sense/report.py @@ -83,13 +83,13 @@ def build_config_section() -> CSVSection: def build_trials_section(trials: int, tips: List[int]) -> CSVSection: """Build section.""" lines: List[Union[CSVLine, CSVLineRepeating]] = [ - CSVLine("trial_number", [str, str, str, str, str, str, str, str, str]) + CSVLine("trial_number", [str, str, str, str, str, str, str, str, str, str]) ] lines.extend( [ CSVLine( f"trial-baseline-{tip}ul", - [float, float, float, float, float, float, float, float], + [float, float, float, float, float, float, float, float, str], ) for tip in tips ] @@ -98,7 +98,7 @@ def build_trials_section(trials: int, tips: List[int]) -> CSVSection: [ CSVLine( f"trial-{t + 1}-{tip}ul", - [float, float, float, float, float, float, float, float, float], + [float, float, float, float, float, float, float, float, float, str], ) for tip in tips for t in range(trials) @@ -195,6 +195,7 @@ def store_baseline_trial( 0, 0, measured_error, + "Baseline", ], ) @@ -211,6 +212,7 @@ def store_trial( plunger_travel: float, tip_length_offset: float, target_height: float, + result: str, google_sheet: Optional[google_sheets_tool.google_sheet], sheet_name: str, sheet_id: Optional[str], @@ -229,6 +231,7 @@ def store_trial( tip_length_offset, height + tip_length_offset, target_height, + result, ], ) if google_sheet is not None and sheet_id is not None: @@ -305,6 +308,7 @@ def build_ls_report( "tip_length_offset", "adjusted_height", "target_height", + "result", ], ) return report diff --git a/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p1000_single_vial.py b/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p1000_96_well.py similarity index 95% rename from hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p1000_single_vial.py rename to hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p1000_96_well.py index d760f8da0ed..306abe2d48d 100644 --- a/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p1000_single_vial.py +++ b/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p1000_96_well.py @@ -11,7 +11,7 @@ 200: [3], 1000: [3], } -LABWARE_ON_SCALE = "radwag_pipette_calibration_vial" +LABWARE_ON_SCALE = "corning_96_wellplate_360ul_flat" def run(ctx: ProtocolContext) -> None: diff --git a/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p50_multi.py b/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p50_multi.py new file mode 100644 index 00000000000..69d571f0259 --- /dev/null +++ b/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p50_multi.py @@ -0,0 +1,29 @@ +"""Liquid Sense OT3 P1000.""" +from opentrons.protocol_api import ProtocolContext + +metadata = {"protocolName": "liquid-sense-ot3-p50-multi"} +requirements = {"robotType": "Flex", "apiLevel": "2.15"} + +SLOT_SCALE = 1 +SLOT_DIAL = 9 +SLOTS_TIPRACK = {50: [3]} +LABWARE_ON_SCALE = "nest_12_reservoir_15ml" + + +def run(ctx: ProtocolContext) -> None: + """Run.""" + tipracks = [ + ctx.load_labware(f"opentrons_flex_96_tiprack_{size}uL", slot) + for size, slots in SLOTS_TIPRACK.items() + for slot in slots + ] + vial = ctx.load_labware(LABWARE_ON_SCALE, SLOT_SCALE) + dial = ctx.load_labware("dial_indicator", SLOT_DIAL) + pipette = ctx.load_instrument("flex_8channel_50", "left") + for rack in tipracks: + pipette.pick_up_tip(rack["A1"]) + pipette.aspirate(10, vial["A1"].top()) + pipette.dispense(10, vial["A1"].top()) + pipette.aspirate(1, dial["A1"].top()) + pipette.dispense(1, dial["A1"].top()) + pipette.drop_tip(home_after=False) From 1e3389a994f96c1b420a57d1a6bb63b28fb32918 Mon Sep 17 00:00:00 2001 From: Jamey Huffnagle Date: Wed, 17 Jul 2024 13:15:24 -0400 Subject: [PATCH 37/78] fix(app): support layered WellSelection (#15688) Closes EXEC-534 WellSelection utilizes a helper function, getCollidingWells, which selects multiple wells in case a user happens to click multiple bounding rects. This helper detects clicks using the DOM, a class tag, and the document x,y coordinate space. It works as intended, but it doesn't account for one edge case: a WellSelection component renders above a second WellSelection component with at least partial overlap in the x,y coordinate space. In this instance, getCollidingWells detects multiple wells were selected, which leads to surprising behavior (video). The simplest solution is to effectively select "visible" wells only, utilizing document.elementFromPoint() to accomplish this task. --- .../shared/TipSelection.tsx | 1 - app/src/organisms/WellSelection/utils.ts | 36 ++++++++++++++++--- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/TipSelection.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/TipSelection.tsx index 30640465f5e..2b9084cb0f4 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/TipSelection.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/TipSelection.tsx @@ -9,7 +9,6 @@ export type TipSelectionProps = RecoveryContentProps & { allowTipSelection: boolean } -// TODO(jh, 06-13-24): EXEC-535. export function TipSelection(props: TipSelectionProps): JSX.Element { const { failedLabwareUtils, failedPipetteInfo, allowTipSelection } = props diff --git a/app/src/organisms/WellSelection/utils.ts b/app/src/organisms/WellSelection/utils.ts index 1fb5e66f678..bd6bc660a1a 100644 --- a/app/src/organisms/WellSelection/utils.ts +++ b/app/src/organisms/WellSelection/utils.ts @@ -26,7 +26,19 @@ export function clientRectToBoundingRect(rect: ClientRect): BoundingRect { } } +// TODO(jh, 07-17-24): Consider checking specific well labels instead of elementAtPoint as a more robust alternative. export const getCollidingWells = (rectPositions: GenericRect): WellGroup => { + const isElementVisible = (element: HTMLElement): boolean => { + const rect = element.getBoundingClientRect() + // If multiple well elements occupy the same x,y coordinate space, document.elementFromPoint() selects + // ONLY the "topmost" well element, accounting for z-index, stacking order, visibility, and opacity. + const elementAtPoint = document.elementFromPoint( + rect.left + rect.width / 2, + rect.top + rect.height / 2 + ) + return element.contains(elementAtPoint) + } + // Returns set of selected wells under a collision rect const { x0, y0, x1, y1 } = rectPositions const selectionBoundingRect = { @@ -41,12 +53,26 @@ export const getCollidingWells = (rectPositions: GenericRect): WellGroup => { `[${INTERACTIVE_WELL_DATA_ATTRIBUTE}]` ), ] - const collidedElems = selectableElems.filter((selectableElem, i) => - rectCollision( - selectionBoundingRect, - clientRectToBoundingRect(selectableElem.getBoundingClientRect()) + + const collidedElems = selectableElems.filter(selectableElem => { + const rect = selectableElem.getBoundingClientRect() + + // Check if the element is in the viewport and not obscured. + const isInViewport = + rect.top >= 0 && + rect.left >= 0 && + rect.bottom <= + (window.innerHeight || document.documentElement.clientHeight) && + rect.right <= (window.innerWidth || document.documentElement.clientWidth) + + const isVisible = isInViewport && isElementVisible(selectableElem) + + return ( + isVisible && + rectCollision(selectionBoundingRect, clientRectToBoundingRect(rect)) ) - ) + }) + const collidedWellData = collidedElems.reduce( (acc: WellGroup, elem): WellGroup => { if ( From 06d040a38edf0f2fc95bc76da7ee3cb16f3b1a40 Mon Sep 17 00:00:00 2001 From: aaron-kulkarni <107003644+aaron-kulkarni@users.noreply.github.com> Date: Wed, 17 Jul 2024 14:04:45 -0400 Subject: [PATCH 38/78] refactor(api): delete the delete attribute of protocol context (#15692) # Overview According to the ticket: ProtocolContext.\_\_del\_\_ is a useless method from the RPC API times. It can safely be removed EXEC-10 # Test Plan # Changelog # Review requests # Risk assessment Low --- api/src/opentrons/protocol_api/protocol_context.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/api/src/opentrons/protocol_api/protocol_context.py b/api/src/opentrons/protocol_api/protocol_context.py index ad96e0c3156..57a04d664a6 100644 --- a/api/src/opentrons/protocol_api/protocol_context.py +++ b/api/src/opentrons/protocol_api/protocol_context.py @@ -246,10 +246,6 @@ def cleanup(self) -> None: self._unsubscribe_commands() self._unsubscribe_commands = None - def __del__(self) -> None: - if getattr(self, "_unsubscribe_commands", None): - self._unsubscribe_commands() # type: ignore - @property @requires_version(2, 0) def max_speeds(self) -> AxisMaxSpeeds: From 2cdc3ddfdece4598a284e7583ec7cd33996d87c6 Mon Sep 17 00:00:00 2001 From: koji Date: Wed, 17 Jul 2024 14:09:59 -0400 Subject: [PATCH 39/78] fix(api-client): fix upload csv function issue (#15693) * fix(api-client): fix upload csv function issue --- api-client/src/dataFiles/uploadCsvFile.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/api-client/src/dataFiles/uploadCsvFile.ts b/api-client/src/dataFiles/uploadCsvFile.ts index 051d44cc26b..2c0e6ee39a5 100644 --- a/api-client/src/dataFiles/uploadCsvFile.ts +++ b/api-client/src/dataFiles/uploadCsvFile.ts @@ -8,11 +8,18 @@ export function uploadCsvFile( config: HostConfig, data: FileData ): ResponsePromise { - return request( + let formData + + if (typeof data !== 'string') { + formData = new FormData() + formData.append('file', data) + } else { + formData = data + } + return request( POST, '/dataFiles', - null, - config, - data + formData, + config ) } From 218bf915a8635b2a41023682df6615984e5fe27c Mon Sep 17 00:00:00 2001 From: Max Marrone Date: Wed, 17 Jul 2024 14:24:18 -0400 Subject: [PATCH 40/78] feat(robot-server): Allow downloading logs from update-server (#15685) --- api/src/opentrons/system/log_control.py | 6 ++++- robot-server/robot_server/health/models.py | 26 ++++++++++++++++--- robot-server/robot_server/health/router.py | 8 +++++- .../service/legacy/models/logs.py | 1 + .../service/legacy/routers/logs.py | 1 + .../tests/health/test_health_router.py | 14 ++++++++-- robot-server/tests/integration/fixtures.py | 8 +++++- 7 files changed, 55 insertions(+), 9 deletions(-) diff --git a/api/src/opentrons/system/log_control.py b/api/src/opentrons/system/log_control.py index bd44af3c7c2..0b992b31658 100644 --- a/api/src/opentrons/system/log_control.py +++ b/api/src/opentrons/system/log_control.py @@ -15,7 +15,11 @@ MAX_RECORDS = 100000 DEFAULT_RECORDS = 50000 -UNIT_SELECTORS = ["opentrons-robot-server", "opentrons-robot-app"] +UNIT_SELECTORS = [ + "opentrons-robot-server", + "opentrons-update-server", + "opentrons-robot-app", +] SERIAL_SPECIAL = "ALL_SERIAL" SERIAL_SELECTORS = [ "opentrons-api-serial", diff --git a/robot-server/robot_server/health/models.py b/robot-server/robot_server/health/models.py index 16090ade4d5..9f886ca8f2f 100644 --- a/robot-server/robot_server/health/models.py +++ b/robot-server/robot_server/health/models.py @@ -10,26 +10,44 @@ class HealthLinks(BaseModel): apiLog: str = Field( ..., - description="The path to the API logs endpoint", + description=( + "The path to the API logs endpoint." + " Deprecated: Use the `logs` field of the `GET /health` response" + " or refer to the OpenAPI specification of the `/logs` endpoint, instead." + ), examples=["/logs/api.log"], + deprecated=True, ) serialLog: str = Field( ..., - description="The path to the motor control serial communication logs endpoint", + description=( + "The path to the motor control serial communication logs endpoint." + " Deprecated: Use the `logs` field of the `GET /health` response" + " or refer to the OpenAPI specification of the `/logs` endpoint, instead." + ), examples=["/logs/serial.log"], + deprecated=True, ) serverLog: str = Field( ..., - description="The path to the HTTP server logs endpoint", + description=( + "The path to the HTTP server logs endpoint." + " Deprecated: Use the `logs` field of the `GET /health` response" + " or refer to the OpenAPI specification of the `/logs` endpoint, instead." + ), examples=["/logs/server.log"], + deprecated=True, ) oddLog: typing.Optional[str] = Field( None, description=( "The path to the on-device display app logs endpoint" - " (only present on the Opentrons Flex)" + " (only present on the Opentrons Flex)." + " Deprecated: Use the `logs` field of the `GET /health` response" + " or refer to the OpenAPI specification of the `/logs` endpoint, instead." ), examples=["/logs/touchscreen.log"], + deprecated=True, ) apiSpec: str = Field( ..., diff --git a/robot-server/robot_server/health/router.py b/robot-server/robot_server/health/router.py index 610979f0b3a..9d9572bfc9b 100644 --- a/robot-server/robot_server/health/router.py +++ b/robot-server/robot_server/health/router.py @@ -22,11 +22,17 @@ _log = logging.getLogger(__name__) -OT2_LOG_PATHS = ["/logs/serial.log", "/logs/api.log", "/logs/server.log"] +OT2_LOG_PATHS = [ + "/logs/serial.log", + "/logs/api.log", + "/logs/server.log", + "/logs/update_server.log", +] FLEX_LOG_PATHS = [ "/logs/serial.log", "/logs/api.log", "/logs/server.log", + "/logs/update_server.log", "/logs/touchscreen.log", ] VERSION_PATH = "/etc/VERSION.json" diff --git a/robot-server/robot_server/service/legacy/models/logs.py b/robot-server/robot_server/service/legacy/models/logs.py index b4dd56edbe2..5a501fdd2ff 100644 --- a/robot-server/robot_server/service/legacy/models/logs.py +++ b/robot-server/robot_server/service/legacy/models/logs.py @@ -8,6 +8,7 @@ class LogIdentifier(str, Enum): serial = "serial.log" server = "server.log" api_server = "combined_api_server.log" + update_server = "update_server.log" touchscreen = "touchscreen.log" diff --git a/robot-server/robot_server/service/legacy/routers/logs.py b/robot-server/robot_server/service/legacy/routers/logs.py index fe270611eb7..589413181fb 100644 --- a/robot-server/robot_server/service/legacy/routers/logs.py +++ b/robot-server/robot_server/service/legacy/routers/logs.py @@ -12,6 +12,7 @@ LogIdentifier.serial: log_control.SERIAL_SPECIAL, LogIdentifier.server: "uvicorn", LogIdentifier.api_server: "opentrons-robot-server", + LogIdentifier.update_server: "opentrons-update-server", LogIdentifier.touchscreen: "opentrons-robot-app", } diff --git a/robot-server/tests/health/test_health_router.py b/robot-server/tests/health/test_health_router.py index 0fa37526a3e..554ac2b7528 100644 --- a/robot-server/tests/health/test_health_router.py +++ b/robot-server/tests/health/test_health_router.py @@ -25,7 +25,12 @@ def test_get_health( "api_version": "mytestapiversion", "fw_version": "FW111", "board_revision": "BR2.1", - "logs": ["/logs/serial.log", "/logs/api.log", "/logs/server.log"], + "logs": [ + "/logs/serial.log", + "/logs/api.log", + "/logs/server.log", + "/logs/update_server.log", + ], "system_version": "mytestsystemversion", "minimum_protocol_api_version": list(MIN_SUPPORTED_VERSION), "maximum_protocol_api_version": list(MAX_SUPPORTED_VERSION), @@ -63,7 +68,12 @@ def test_get_health_with_none_version( "api_version": "mytestapiversion", "fw_version": "FW111", "board_revision": "BR2.1", - "logs": ["/logs/serial.log", "/logs/api.log", "/logs/server.log"], + "logs": [ + "/logs/serial.log", + "/logs/api.log", + "/logs/server.log", + "/logs/update_server.log", + ], "system_version": "mytestsystemversion", "minimum_protocol_api_version": list(MIN_SUPPORTED_VERSION), "maximum_protocol_api_version": list(MAX_SUPPORTED_VERSION), diff --git a/robot-server/tests/integration/fixtures.py b/robot-server/tests/integration/fixtures.py index 69d9ccdb00a..08175cc6b99 100644 --- a/robot-server/tests/integration/fixtures.py +++ b/robot-server/tests/integration/fixtures.py @@ -18,7 +18,12 @@ def check_health_response(response: Response) -> None: "api_version": __version__, "fw_version": "Virtual Smoothie", "board_revision": "2.1", - "logs": ["/logs/serial.log", "/logs/api.log", "/logs/server.log"], + "logs": [ + "/logs/serial.log", + "/logs/api.log", + "/logs/server.log", + "/logs/update_server.log", + ], "system_version": config.OT_SYSTEM_VERSION, "robot_model": "OT-2 Standard", "minimum_protocol_api_version": list(MIN_SUPPORTED_VERSION), @@ -47,6 +52,7 @@ def check_ot3_health_response(response: Response) -> None: "/logs/serial.log", "/logs/api.log", "/logs/server.log", + "/logs/update_server.log", "/logs/touchscreen.log", ], "system_version": config.OT_SYSTEM_VERSION, From fccfd49fbdadae577452f5d6968e646b0adddcca Mon Sep 17 00:00:00 2001 From: aaron-kulkarni <107003644+aaron-kulkarni@users.noreply.github.com> Date: Wed, 17 Jul 2024 14:36:38 -0400 Subject: [PATCH 41/78] refactor(shared-data): remove outdated id fields from JSON v6 fixtures (#15690) replace all instances of Command `id` with `key` in the v6 protocol fixtures close EXEC-221 # Overview # Test Plan # Changelog Just changed all of v6 fixtures to have a "key" field in Command rather than an "id" field Updated `shared-data/protocol/fixtures/types/schemaV6/command/index.ts` to properly recognize the key field. # Review requests This ticket is like 2 years old so I don't even know if this matters anymore but here it is. # Risk assessment Low --- .../fixtures/6/heaterShakerCommands.json | 36 ++--- .../6/heaterShakerCommandsWithResultsKey.json | 14 +- .../fixtures/6/multipleTempModules.json | 116 ++++++++-------- .../protocol/fixtures/6/multipleTipracks.json | 120 ++++++++-------- .../fixtures/6/multipleTipracksWithTC.json | 130 +++++++++--------- .../protocol/fixtures/6/oneTiprack.json | 28 ++-- shared-data/protocol/fixtures/6/simpleV6.json | 48 +++---- .../fixtures/6/tempAndMagModuleCommands.json | 34 ++--- .../protocol/fixtures/6/transferSettings.json | 116 ++++++++-------- 9 files changed, 321 insertions(+), 321 deletions(-) diff --git a/shared-data/protocol/fixtures/6/heaterShakerCommands.json b/shared-data/protocol/fixtures/6/heaterShakerCommands.json index 8307cf8aedd..3c4733c0268 100644 --- a/shared-data/protocol/fixtures/6/heaterShakerCommands.json +++ b/shared-data/protocol/fixtures/6/heaterShakerCommands.json @@ -1241,7 +1241,7 @@ "commands": [ { "commandType": "loadPipette", - "id": "0abc123", + "key": "0abc123", "params": { "pipetteId": "pipetteId", "mount": "left" @@ -1249,7 +1249,7 @@ }, { "commandType": "loadModule", - "id": "1abc123", + "key": "1abc123", "params": { "moduleId": "magneticModuleId", "location": { "slotName": "3" } @@ -1257,7 +1257,7 @@ }, { "commandType": "loadModule", - "id": "2abc123", + "key": "2abc123", "params": { "moduleId": "heaterShakerId", "location": { "slotName": "1" } @@ -1265,7 +1265,7 @@ }, { "commandType": "loadLabware", - "id": "3abc123", + "key": "3abc123", "params": { "labwareId": "sourcePlateId", "location": { @@ -1275,7 +1275,7 @@ }, { "commandType": "loadLabware", - "id": "4abc123", + "key": "4abc123", "params": { "labwareId": "destPlateId", "location": { @@ -1285,7 +1285,7 @@ }, { "commandType": "loadLabware", - "id": "5abc123", + "key": "5abc123", "params": { "labwareId": "tipRackId", "location": { "slotName": "8" } @@ -1293,7 +1293,7 @@ }, { "commandType": "loadLabware", - "id": "6abc123", + "key": "6abc123", "params": { "labwareId": "fixedTrash", "location": { @@ -1303,7 +1303,7 @@ }, { "commandType": "loadLiquid", - "id": "7abc123", + "key": "7abc123", "params": { "liquidId": "waterId", "labwareId": "sourcePlateId", @@ -1316,7 +1316,7 @@ { "commandType": "pickUpTip", - "id": "8abc123", + "key": "8abc123", "params": { "pipetteId": "pipetteId", "labwareId": "tipRackId", @@ -1325,7 +1325,7 @@ }, { "commandType": "aspirate", - "id": "9abc123", + "key": "9abc123", "params": { "pipetteId": "pipetteId", "labwareId": "sourcePlateId", @@ -1340,14 +1340,14 @@ }, { "commandType": "delay", - "id": "10abc123", + "key": "10abc123", "params": { "seconds": 42 } }, { "commandType": "dispense", - "id": "11abc123", + "key": "11abc123", "params": { "pipetteId": "pipetteId", "labwareId": "destPlateId", @@ -1362,7 +1362,7 @@ }, { "commandType": "heaterShaker/setAndWaitForShakeSpeed", - "id": "13abc123", + "key": "13abc123", "params": { "moduleId": "heaterShakerId", "rpm": 2000 @@ -1370,7 +1370,7 @@ }, { "commandType": "heaterShaker/setTargetTemperature", - "id": "14abc123", + "key": "14abc123", "params": { "moduleId": "heaterShakerId", "celsius": 42 @@ -1378,28 +1378,28 @@ }, { "commandType": "heaterShaker/waitForTemperature", - "id": "15abc123", + "key": "15abc123", "params": { "moduleId": "heaterShakerId" } }, { "commandType": "heaterShaker/openLabwareLatch", - "id": "16abc123", + "key": "16abc123", "params": { "moduleId": "heaterShakerId" } }, { "commandType": "heaterShaker/closeLabwareLatch", - "id": "17abc123", + "key": "17abc123", "params": { "moduleId": "heaterShakerId" } }, { "commandType": "heaterShaker/deactivateHeater", - "id": "16abc123", + "key": "16abc123", "params": { "moduleId": "heaterShakerId" } diff --git a/shared-data/protocol/fixtures/6/heaterShakerCommandsWithResultsKey.json b/shared-data/protocol/fixtures/6/heaterShakerCommandsWithResultsKey.json index ef83c29f0a6..6a028cf04e9 100644 --- a/shared-data/protocol/fixtures/6/heaterShakerCommandsWithResultsKey.json +++ b/shared-data/protocol/fixtures/6/heaterShakerCommandsWithResultsKey.json @@ -2494,7 +2494,7 @@ }, "commands": [ { - "id": "0", + "key": "0", "commandType": "loadPipette", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -2502,7 +2502,7 @@ } }, { - "id": "1", + "key": "1", "commandType": "loadPipette", "params": { "pipetteId": "4da579b0-a9bf-11eb-bce6-9f1d5b9c1a1b", @@ -2510,7 +2510,7 @@ } }, { - "id": "2", + "key": "2", "commandType": "loadModule", "params": { "moduleId": "3e012450-3412-11eb-ad93-ed232a2337cf:heaterShakerModuleType", @@ -2523,7 +2523,7 @@ } }, { - "id": "5", + "key": "5", "commandType": "loadLabware", "params": { "labwareId": "fixedTrash", @@ -2590,7 +2590,7 @@ } }, { - "id": "6", + "key": "6", "commandType": "loadLabware", "params": { "labwareId": "3e047fb0-3412-11eb-ad93-ed232a2337cf:opentrons/opentrons_96_tiprack_1000ul/1", @@ -3622,7 +3622,7 @@ } }, { - "id": "7", + "key": "7", "commandType": "loadLabware", "params": { "labwareId": "5ae317e0-3412-11eb-ad93-ed232a2337cf:opentrons/nest_1_reservoir_195ml/1", @@ -3691,7 +3691,7 @@ } }, { - "id": "8", + "key": "8", "commandType": "loadLabware", "params": { "labwareId": "aac5d680-3412-11eb-ad93-ed232a2337cf:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1", diff --git a/shared-data/protocol/fixtures/6/multipleTempModules.json b/shared-data/protocol/fixtures/6/multipleTempModules.json index ecca66a0271..bba498a875d 100644 --- a/shared-data/protocol/fixtures/6/multipleTempModules.json +++ b/shared-data/protocol/fixtures/6/multipleTempModules.json @@ -4561,7 +4561,7 @@ }, "commands": [ { - "id": "0", + "key": "0", "commandType": "loadPipette", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -4569,7 +4569,7 @@ } }, { - "id": "1", + "key": "1", "commandType": "loadPipette", "params": { "pipetteId": "4da579b0-a9bf-11eb-bce6-9f1d5b9c1a1b", @@ -4577,7 +4577,7 @@ } }, { - "id": "2", + "key": "2", "commandType": "loadModule", "params": { "moduleId": "3e012450-3412-11eb-ad93-ed232a2337cf:magneticModuleType", @@ -4590,7 +4590,7 @@ } }, { - "id": "3", + "key": "3", "commandType": "loadModule", "params": { "moduleId": "3e0283e0-3412-11eb-ad93-ed232a2337cf:temperatureModuleType1", @@ -4603,7 +4603,7 @@ } }, { - "id": "4", + "key": "4", "commandType": "loadModule", "params": { "moduleId": "3e039550-3412-11eb-ad93-ed232a2337cf:temperatureModuleType2", @@ -4616,7 +4616,7 @@ } }, { - "id": "5", + "key": "5", "commandType": "loadLabware", "params": { "labwareId": "fixedTrash", @@ -4683,7 +4683,7 @@ } }, { - "id": "6", + "key": "6", "commandType": "loadLabware", "params": { "labwareId": "3e047fb0-3412-11eb-ad93-ed232a2337cf:opentrons/opentrons_96_tiprack_1000ul/1", @@ -5715,7 +5715,7 @@ } }, { - "id": "7", + "key": "7", "commandType": "loadLabware", "params": { "labwareId": "5ae317e0-3412-11eb-ad93-ed232a2337cf:opentrons/nest_1_reservoir_195ml/1", @@ -5784,7 +5784,7 @@ } }, { - "id": "8", + "key": "8", "commandType": "loadLabware", "params": { "labwareId": "aac5d680-3412-11eb-ad93-ed232a2337cf:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1", @@ -6815,7 +6815,7 @@ } }, { - "id": "9", + "key": "9", "commandType": "loadLabware", "params": { "labwareId": "60e8b050-3412-11eb-ad93-ed232a2337cf:opentrons/corning_24_wellplate_3.4ml_flat/1", @@ -7121,7 +7121,7 @@ } }, { - "id": "10", + "key": "10", "commandType": "loadLabware", "params": { "labwareId": "ada13110-3412-11eb-ad93-ed232a2337cf:opentrons/opentrons_96_aluminumblock_generic_pcr_strip_200ul/1", @@ -8160,7 +8160,7 @@ } }, { - "id": "11", + "key": "11", "commandType": "loadLabware", "params": { "labwareId": "b0103540-3412-11eb-ad93-ed232a2337cf:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1", @@ -9191,7 +9191,7 @@ } }, { - "id": "12", + "key": "12", "commandType": "loadLabware", "params": { "labwareId": "faa13a50-a9bf-11eb-bce6-9f1d5b9c1a1b:opentrons/opentrons_96_tiprack_20ul/1", @@ -10223,7 +10223,7 @@ } }, { - "id": "13", + "key": "13", "commandType": "loadLabware", "params": { "labwareId": "53d3b350-a9c0-11eb-bce6-9f1d5b9c1a1b", @@ -10529,7 +10529,7 @@ } }, { - "id": "14", + "key": "14", "commandType": "pickUpTip", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10538,7 +10538,7 @@ } }, { - "id": "15", + "key": "15", "commandType": "aspirate", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10553,7 +10553,7 @@ } }, { - "id": "16", + "key": "16", "commandType": "dispense", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10568,7 +10568,7 @@ } }, { - "id": "17", + "key": "17", "commandType": "dropTip", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10577,7 +10577,7 @@ } }, { - "id": "18", + "key": "18", "commandType": "pickUpTip", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10586,7 +10586,7 @@ } }, { - "id": "19", + "key": "19", "commandType": "aspirate", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10601,7 +10601,7 @@ } }, { - "id": "20", + "key": "20", "commandType": "dispense", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10616,7 +10616,7 @@ } }, { - "id": "21", + "key": "21", "commandType": "dropTip", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10625,7 +10625,7 @@ } }, { - "id": "22", + "key": "22", "commandType": "pickUpTip", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10634,7 +10634,7 @@ } }, { - "id": "23", + "key": "23", "commandType": "aspirate", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10649,7 +10649,7 @@ } }, { - "id": "24", + "key": "24", "commandType": "dispense", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10664,7 +10664,7 @@ } }, { - "id": "25", + "key": "25", "commandType": "dropTip", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10673,7 +10673,7 @@ } }, { - "id": "26", + "key": "26", "commandType": "pickUpTip", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10682,7 +10682,7 @@ } }, { - "id": "27", + "key": "27", "commandType": "aspirate", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10697,7 +10697,7 @@ } }, { - "id": "28", + "key": "28", "commandType": "dispense", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10712,7 +10712,7 @@ } }, { - "id": "29", + "key": "29", "commandType": "dropTip", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10721,7 +10721,7 @@ } }, { - "id": "30", + "key": "30", "commandType": "pickUpTip", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10730,7 +10730,7 @@ } }, { - "id": "31", + "key": "31", "commandType": "aspirate", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10745,7 +10745,7 @@ } }, { - "id": "32", + "key": "32", "commandType": "dispense", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10760,7 +10760,7 @@ } }, { - "id": "33", + "key": "33", "commandType": "dropTip", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10769,7 +10769,7 @@ } }, { - "id": "34", + "key": "34", "commandType": "pickUpTip", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10778,7 +10778,7 @@ } }, { - "id": "35", + "key": "35", "commandType": "aspirate", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10793,7 +10793,7 @@ } }, { - "id": "36", + "key": "36", "commandType": "dispense", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10808,7 +10808,7 @@ } }, { - "id": "37", + "key": "37", "commandType": "dropTip", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10817,7 +10817,7 @@ } }, { - "id": "38", + "key": "38", "commandType": "pickUpTip", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10826,7 +10826,7 @@ } }, { - "id": "39", + "key": "39", "commandType": "aspirate", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10841,7 +10841,7 @@ } }, { - "id": "40", + "key": "40", "commandType": "dispense", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10856,7 +10856,7 @@ } }, { - "id": "41", + "key": "41", "commandType": "dropTip", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10865,7 +10865,7 @@ } }, { - "id": "42", + "key": "42", "commandType": "pickUpTip", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10874,7 +10874,7 @@ } }, { - "id": "43", + "key": "43", "commandType": "aspirate", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10889,7 +10889,7 @@ } }, { - "id": "44", + "key": "44", "commandType": "dispense", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10904,7 +10904,7 @@ } }, { - "id": "45", + "key": "45", "commandType": "dropTip", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10913,7 +10913,7 @@ } }, { - "id": "46", + "key": "46", "commandType": "pickUpTip", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10922,7 +10922,7 @@ } }, { - "id": "47", + "key": "47", "commandType": "aspirate", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10937,7 +10937,7 @@ } }, { - "id": "48", + "key": "48", "commandType": "dispense", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10952,7 +10952,7 @@ } }, { - "id": "49", + "key": "49", "commandType": "dropTip", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10961,7 +10961,7 @@ } }, { - "id": "50", + "key": "50", "commandType": "pickUpTip", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10970,7 +10970,7 @@ } }, { - "id": "51", + "key": "51", "commandType": "aspirate", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10985,7 +10985,7 @@ } }, { - "id": "52", + "key": "52", "commandType": "dispense", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -11000,7 +11000,7 @@ } }, { - "id": "53", + "key": "53", "commandType": "dropTip", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -11009,7 +11009,7 @@ } }, { - "id": "54", + "key": "54", "commandType": "pickUpTip", "params": { "pipetteId": "4da579b0-a9bf-11eb-bce6-9f1d5b9c1a1b", @@ -11018,7 +11018,7 @@ } }, { - "id": "55", + "key": "55", "commandType": "aspirate", "params": { "pipetteId": "4da579b0-a9bf-11eb-bce6-9f1d5b9c1a1b", @@ -11033,7 +11033,7 @@ } }, { - "id": "56", + "key": "56", "commandType": "dispense", "params": { "pipetteId": "4da579b0-a9bf-11eb-bce6-9f1d5b9c1a1b", @@ -11048,7 +11048,7 @@ } }, { - "id": "57", + "key": "57", "commandType": "dropTip", "params": { "pipetteId": "4da579b0-a9bf-11eb-bce6-9f1d5b9c1a1b", diff --git a/shared-data/protocol/fixtures/6/multipleTipracks.json b/shared-data/protocol/fixtures/6/multipleTipracks.json index cc38a6c9f89..f63048ade3a 100644 --- a/shared-data/protocol/fixtures/6/multipleTipracks.json +++ b/shared-data/protocol/fixtures/6/multipleTipracks.json @@ -1967,7 +1967,7 @@ "commands": [ { "commandType": "loadPipette", - "id": "0abc1", + "key": "0abc1", "params": { "pipetteId": "50d23e00-0042-11ec-8258-f7ffdf5ad45a", "mount": "left" @@ -1978,7 +1978,7 @@ }, { "commandType": "loadPipette", - "id": "0abc2", + "key": "0abc2", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", "mount": "right" @@ -1989,7 +1989,7 @@ }, { "commandType": "loadLabware", - "id": "0abc3", + "key": "0abc3", "params": { "labwareId": "fixedTrash", "location": { "slotName": "12" } @@ -2043,7 +2043,7 @@ }, { "commandType": "loadLabware", - "id": "0abc4", + "key": "0abc4", "params": { "labwareId": "50d3ebb0-0042-11ec-8258-f7ffdf5ad45a:opentrons/opentrons_96_tiprack_300ul/1", "location": { "slotName": "1" } @@ -3069,7 +3069,7 @@ }, { "commandType": "loadLabware", - "id": "0abc5", + "key": "0abc5", "params": { "labwareId": "9fbc1db0-0042-11ec-8258-f7ffdf5ad45a:opentrons/nest_12_reservoir_15ml/1", "location": { "slotName": "10" } @@ -3266,7 +3266,7 @@ }, { "commandType": "loadLabware", - "id": "0abc6", + "key": "0abc6", "params": { "labwareId": "e24818a0-0042-11ec-8258-f7ffdf5ad45a", "location": { "slotName": "2" } @@ -4291,7 +4291,7 @@ } }, { - "id": "0", + "key": "0", "commandType": "pickUpTip", "params": { "pipetteId": "50d23e00-0042-11ec-8258-f7ffdf5ad45a", @@ -4300,7 +4300,7 @@ } }, { - "id": "1", + "key": "1", "commandType": "aspirate", "params": { "pipetteId": "50d23e00-0042-11ec-8258-f7ffdf5ad45a", @@ -4315,7 +4315,7 @@ } }, { - "id": "2", + "key": "2", "commandType": "dispense", "params": { "pipetteId": "50d23e00-0042-11ec-8258-f7ffdf5ad45a", @@ -4330,7 +4330,7 @@ } }, { - "id": "3", + "key": "3", "commandType": "dropTip", "params": { "pipetteId": "50d23e00-0042-11ec-8258-f7ffdf5ad45a", @@ -4339,7 +4339,7 @@ } }, { - "id": "4", + "key": "4", "commandType": "pickUpTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4348,7 +4348,7 @@ } }, { - "id": "5", + "key": "5", "commandType": "aspirate", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4363,7 +4363,7 @@ } }, { - "id": "6", + "key": "6", "commandType": "dispense", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4378,7 +4378,7 @@ } }, { - "id": "7", + "key": "7", "commandType": "dropTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4387,7 +4387,7 @@ } }, { - "id": "8", + "key": "8", "commandType": "pickUpTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4396,7 +4396,7 @@ } }, { - "id": "9", + "key": "9", "commandType": "aspirate", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4411,7 +4411,7 @@ } }, { - "id": "10", + "key": "10", "commandType": "dispense", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4426,7 +4426,7 @@ } }, { - "id": "11", + "key": "11", "commandType": "dropTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4435,7 +4435,7 @@ } }, { - "id": "12", + "key": "12", "commandType": "pickUpTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4444,7 +4444,7 @@ } }, { - "id": "13", + "key": "13", "commandType": "aspirate", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4459,7 +4459,7 @@ } }, { - "id": "14", + "key": "14", "commandType": "dispense", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4474,7 +4474,7 @@ } }, { - "id": "15", + "key": "15", "commandType": "dropTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4483,7 +4483,7 @@ } }, { - "id": "16", + "key": "16", "commandType": "pickUpTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4492,7 +4492,7 @@ } }, { - "id": "17", + "key": "17", "commandType": "aspirate", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4507,7 +4507,7 @@ } }, { - "id": "18", + "key": "18", "commandType": "dispense", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4522,7 +4522,7 @@ } }, { - "id": "19", + "key": "19", "commandType": "dropTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4531,7 +4531,7 @@ } }, { - "id": "20", + "key": "20", "commandType": "pickUpTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4540,7 +4540,7 @@ } }, { - "id": "21", + "key": "21", "commandType": "aspirate", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4555,7 +4555,7 @@ } }, { - "id": "22", + "key": "22", "commandType": "dispense", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4570,7 +4570,7 @@ } }, { - "id": "23", + "key": "23", "commandType": "dropTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4579,7 +4579,7 @@ } }, { - "id": "24", + "key": "24", "commandType": "pickUpTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4588,7 +4588,7 @@ } }, { - "id": "25", + "key": "25", "commandType": "aspirate", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4603,7 +4603,7 @@ } }, { - "id": "26", + "key": "26", "commandType": "dispense", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4618,7 +4618,7 @@ } }, { - "id": "27", + "key": "27", "commandType": "dropTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4627,7 +4627,7 @@ } }, { - "id": "28", + "key": "28", "commandType": "pickUpTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4636,7 +4636,7 @@ } }, { - "id": "29", + "key": "29", "commandType": "aspirate", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4651,7 +4651,7 @@ } }, { - "id": "30", + "key": "30", "commandType": "dispense", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4666,7 +4666,7 @@ } }, { - "id": "31", + "key": "31", "commandType": "dropTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4675,7 +4675,7 @@ } }, { - "id": "32", + "key": "32", "commandType": "pickUpTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4684,7 +4684,7 @@ } }, { - "id": "33", + "key": "33", "commandType": "aspirate", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4699,7 +4699,7 @@ } }, { - "id": "34", + "key": "34", "commandType": "dispense", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4714,7 +4714,7 @@ } }, { - "id": "35", + "key": "35", "commandType": "dropTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4723,7 +4723,7 @@ } }, { - "id": "36", + "key": "36", "commandType": "pickUpTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4732,7 +4732,7 @@ } }, { - "id": "37", + "key": "37", "commandType": "aspirate", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4747,7 +4747,7 @@ } }, { - "id": "38", + "key": "38", "commandType": "dispense", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4762,7 +4762,7 @@ } }, { - "id": "39", + "key": "39", "commandType": "dropTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4771,7 +4771,7 @@ } }, { - "id": "40", + "key": "40", "commandType": "pickUpTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4780,7 +4780,7 @@ } }, { - "id": "41", + "key": "41", "commandType": "aspirate", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4795,7 +4795,7 @@ } }, { - "id": "42", + "key": "42", "commandType": "dispense", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4810,7 +4810,7 @@ } }, { - "id": "43", + "key": "43", "commandType": "dropTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4819,7 +4819,7 @@ } }, { - "id": "44", + "key": "44", "commandType": "pickUpTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4828,7 +4828,7 @@ } }, { - "id": "45", + "key": "45", "commandType": "aspirate", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4843,7 +4843,7 @@ } }, { - "id": "46", + "key": "46", "commandType": "dispense", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4858,7 +4858,7 @@ } }, { - "id": "47", + "key": "47", "commandType": "dropTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4867,7 +4867,7 @@ } }, { - "id": "48", + "key": "48", "commandType": "pickUpTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4876,7 +4876,7 @@ } }, { - "id": "49", + "key": "49", "commandType": "aspirate", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4891,7 +4891,7 @@ } }, { - "id": "50", + "key": "50", "commandType": "moveToWell", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4908,14 +4908,14 @@ } }, { - "id": "51", + "key": "51", "commandType": "delay", "params": { "seconds": 1 } }, { - "id": "52", + "key": "52", "commandType": "dispense", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -4930,7 +4930,7 @@ } }, { - "id": "53", + "key": "53", "commandType": "dropTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", diff --git a/shared-data/protocol/fixtures/6/multipleTipracksWithTC.json b/shared-data/protocol/fixtures/6/multipleTipracksWithTC.json index 702610d744b..7c54b41b358 100644 --- a/shared-data/protocol/fixtures/6/multipleTipracksWithTC.json +++ b/shared-data/protocol/fixtures/6/multipleTipracksWithTC.json @@ -3012,7 +3012,7 @@ }, "commands": [ { - "id": "0abc1", + "key": "0abc1", "commandType": "loadPipette", "params": { "pipetteId": "50d23e00-0042-11ec-8258-f7ffdf5ad45a", @@ -3023,7 +3023,7 @@ } }, { - "id": "0abc2", + "key": "0abc2", "commandType": "loadPipette", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -3035,7 +3035,7 @@ }, { "commandType": "loadModule", - "id": "00abc2", + "key": "00abc2", "params": { "moduleId": "18f0c1b0-0122-11ec-88a3-f1745cf9b36c:thermocyclerModuleType", "location": { @@ -3047,7 +3047,7 @@ } }, { - "id": "0abc3", + "key": "0abc3", "commandType": "loadLabware", "params": { "labwareId": "fixedTrash", @@ -3103,7 +3103,7 @@ } }, { - "id": "0abc4", + "key": "0abc4", "commandType": "loadLabware", "params": { "labwareId": "50d3ebb0-0042-11ec-8258-f7ffdf5ad45a:opentrons/opentrons_96_tiprack_300ul/1", @@ -4131,7 +4131,7 @@ } }, { - "id": "0abc5", + "key": "0abc5", "commandType": "loadLabware", "params": { "labwareId": "9fbc1db0-0042-11ec-8258-f7ffdf5ad45a:opentrons/nest_12_reservoir_15ml/1", @@ -4330,7 +4330,7 @@ } }, { - "id": "0abc6", + "key": "0abc6", "commandType": "loadLabware", "params": { "labwareId": "e24818a0-0042-11ec-8258-f7ffdf5ad45a", @@ -5358,7 +5358,7 @@ } }, { - "id": "0abc7", + "key": "0abc7", "commandType": "loadLabware", "params": { "labwareId": "1dc0c050-0122-11ec-88a3-f1745cf9b36c:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1", @@ -6383,7 +6383,7 @@ } }, { - "id": "7", + "key": "7", "commandType": "pickUpTip", "params": { "pipetteId": "50d23e00-0042-11ec-8258-f7ffdf5ad45a", @@ -6392,7 +6392,7 @@ } }, { - "id": "8", + "key": "8", "commandType": "aspirate", "params": { "pipetteId": "50d23e00-0042-11ec-8258-f7ffdf5ad45a", @@ -6407,7 +6407,7 @@ } }, { - "id": "9", + "key": "9", "commandType": "dispense", "params": { "pipetteId": "50d23e00-0042-11ec-8258-f7ffdf5ad45a", @@ -6422,7 +6422,7 @@ } }, { - "id": "10", + "key": "10", "commandType": "dropTip", "params": { "pipetteId": "50d23e00-0042-11ec-8258-f7ffdf5ad45a", @@ -6431,7 +6431,7 @@ } }, { - "id": "11", + "key": "11", "commandType": "pickUpTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -6440,7 +6440,7 @@ } }, { - "id": "12", + "key": "12", "commandType": "aspirate", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -6455,7 +6455,7 @@ } }, { - "id": "13", + "key": "13", "commandType": "dispense", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -6470,7 +6470,7 @@ } }, { - "id": "14", + "key": "14", "commandType": "dropTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -6479,7 +6479,7 @@ } }, { - "id": "15", + "key": "15", "commandType": "pickUpTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -6488,7 +6488,7 @@ } }, { - "id": "16", + "key": "16", "commandType": "aspirate", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -6503,7 +6503,7 @@ } }, { - "id": "17", + "key": "17", "commandType": "dispense", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -6518,7 +6518,7 @@ } }, { - "id": "18", + "key": "18", "commandType": "dropTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -6527,7 +6527,7 @@ } }, { - "id": "19", + "key": "19", "commandType": "pickUpTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -6536,7 +6536,7 @@ } }, { - "id": "20", + "key": "20", "commandType": "aspirate", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -6551,7 +6551,7 @@ } }, { - "id": "21", + "key": "21", "commandType": "dispense", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -6566,7 +6566,7 @@ } }, { - "id": "22", + "key": "22", "commandType": "dropTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -6575,7 +6575,7 @@ } }, { - "id": "23", + "key": "23", "commandType": "pickUpTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -6584,7 +6584,7 @@ } }, { - "id": "24", + "key": "24", "commandType": "aspirate", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -6599,7 +6599,7 @@ } }, { - "id": "25", + "key": "25", "commandType": "dispense", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -6614,7 +6614,7 @@ } }, { - "id": "26", + "key": "26", "commandType": "dropTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -6623,7 +6623,7 @@ } }, { - "id": "27", + "key": "27", "commandType": "pickUpTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -6632,7 +6632,7 @@ } }, { - "id": "28", + "key": "28", "commandType": "aspirate", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -6647,7 +6647,7 @@ } }, { - "id": "29", + "key": "29", "commandType": "dispense", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -6662,7 +6662,7 @@ } }, { - "id": "30", + "key": "30", "commandType": "dropTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -6671,7 +6671,7 @@ } }, { - "id": "31", + "key": "31", "commandType": "pickUpTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -6680,7 +6680,7 @@ } }, { - "id": "32", + "key": "32", "commandType": "aspirate", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -6695,7 +6695,7 @@ } }, { - "id": "33", + "key": "33", "commandType": "dispense", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -6710,7 +6710,7 @@ } }, { - "id": "34", + "key": "34", "commandType": "dropTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -6719,7 +6719,7 @@ } }, { - "id": "35", + "key": "35", "commandType": "pickUpTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -6728,7 +6728,7 @@ } }, { - "id": "36", + "key": "36", "commandType": "aspirate", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -6743,7 +6743,7 @@ } }, { - "id": "37", + "key": "37", "commandType": "dispense", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -6758,7 +6758,7 @@ } }, { - "id": "38", + "key": "38", "commandType": "dropTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -6767,7 +6767,7 @@ } }, { - "id": "39", + "key": "39", "commandType": "pickUpTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -6776,7 +6776,7 @@ } }, { - "id": "40", + "key": "40", "commandType": "aspirate", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -6791,7 +6791,7 @@ } }, { - "id": "41", + "key": "41", "commandType": "dispense", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -6806,7 +6806,7 @@ } }, { - "id": "42", + "key": "42", "commandType": "dropTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -6815,7 +6815,7 @@ } }, { - "id": "43", + "key": "43", "commandType": "pickUpTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -6824,7 +6824,7 @@ } }, { - "id": "44", + "key": "44", "commandType": "aspirate", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -6839,7 +6839,7 @@ } }, { - "id": "45", + "key": "45", "commandType": "dispense", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -6854,7 +6854,7 @@ } }, { - "id": "46", + "key": "46", "commandType": "dropTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -6863,7 +6863,7 @@ } }, { - "id": "47", + "key": "47", "commandType": "pickUpTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -6872,7 +6872,7 @@ } }, { - "id": "48", + "key": "48", "commandType": "aspirate", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -6887,7 +6887,7 @@ } }, { - "id": "49", + "key": "49", "commandType": "dispense", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -6902,7 +6902,7 @@ } }, { - "id": "50", + "key": "50", "commandType": "dropTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -6911,7 +6911,7 @@ } }, { - "id": "51", + "key": "51", "commandType": "pickUpTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -6920,7 +6920,7 @@ } }, { - "id": "52", + "key": "52", "commandType": "aspirate", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -6935,7 +6935,7 @@ } }, { - "id": "53", + "key": "53", "commandType": "dispense", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -6950,7 +6950,7 @@ } }, { - "id": "54", + "key": "54", "commandType": "dropTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -6959,7 +6959,7 @@ } }, { - "id": "55", + "key": "55", "commandType": "pickUpTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -6968,7 +6968,7 @@ } }, { - "id": "56", + "key": "56", "commandType": "aspirate", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -6983,7 +6983,7 @@ } }, { - "id": "57", + "key": "57", "commandType": "moveToWell", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -7000,14 +7000,14 @@ } }, { - "id": "58", + "key": "58", "commandType": "delay", "params": { "seconds": 1 } }, { - "id": "59", + "key": "59", "commandType": "dispense", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -7022,7 +7022,7 @@ } }, { - "id": "60", + "key": "60", "commandType": "dropTip", "params": { "pipetteId": "c235a5a0-0042-11ec-8258-f7ffdf5ad45a", @@ -7031,14 +7031,14 @@ } }, { - "id": "61", + "key": "61", "commandType": "thermocycler/closeLid", "params": { "moduleId": "18f0c1b0-0122-11ec-88a3-f1745cf9b36c:thermocyclerModuleType" } }, { - "id": "62", + "key": "62", "commandType": "thermocycler/setTargetLidTemperature", "params": { "moduleId": "18f0c1b0-0122-11ec-88a3-f1745cf9b36c:thermocyclerModuleType", @@ -7046,7 +7046,7 @@ } }, { - "id": "63", + "key": "63", "commandType": "thermocycler/waitForLidTemperature", "params": { "moduleId": "18f0c1b0-0122-11ec-88a3-f1745cf9b36c:thermocyclerModuleType", diff --git a/shared-data/protocol/fixtures/6/oneTiprack.json b/shared-data/protocol/fixtures/6/oneTiprack.json index d61e5c9e108..7dd58e7a064 100644 --- a/shared-data/protocol/fixtures/6/oneTiprack.json +++ b/shared-data/protocol/fixtures/6/oneTiprack.json @@ -1224,7 +1224,7 @@ }, "commands": [ { - "id": "0abc1", + "key": "0abc1", "commandType": "loadPipette", "params": { "pipetteId": "pipetteId", @@ -1232,7 +1232,7 @@ } }, { - "id": "0abc2", + "key": "0abc2", "commandType": "loadLabware", "params": { "labwareId": "fixedTrash", @@ -1242,7 +1242,7 @@ } }, { - "id": "0abc3", + "key": "0abc3", "commandType": "loadLabware", "params": { "labwareId": "tiprackId", @@ -1252,7 +1252,7 @@ } }, { - "id": "0abc4", + "key": "0abc4", "commandType": "loadLabware", "params": { "labwareId": "sourcePlateId", @@ -1262,7 +1262,7 @@ } }, { - "id": "0abc4", + "key": "0abc4", "commandType": "loadLabware", "params": { "labwareId": "destPlateId", @@ -1272,7 +1272,7 @@ } }, { - "id": "0", + "key": "0", "commandType": "pickUpTip", "params": { "pipetteId": "pipetteId", @@ -1281,7 +1281,7 @@ } }, { - "id": "1", + "key": "1", "commandType": "aspirate", "params": { "pipetteId": "pipetteId", @@ -1296,14 +1296,14 @@ } }, { - "id": "2", + "key": "2", "commandType": "delay", "params": { "seconds": 42 } }, { - "id": "3", + "key": "3", "commandType": "dispense", "params": { "pipetteId": "pipetteId", @@ -1318,7 +1318,7 @@ } }, { - "id": "4", + "key": "4", "commandType": "touchTip", "params": { "pipetteId": "pipetteId", @@ -1331,7 +1331,7 @@ } }, { - "id": "5", + "key": "5", "commandType": "blowout", "params": { "pipetteId": "pipetteId", @@ -1345,7 +1345,7 @@ } }, { - "id": "7", + "key": "7", "commandType": "moveToWell", "params": { "pipetteId": "pipetteId", @@ -1357,7 +1357,7 @@ } }, { - "id": "8", + "key": "8", "commandType": "moveToWell", "params": { "pipetteId": "pipetteId", @@ -1376,7 +1376,7 @@ } }, { - "id": "9", + "key": "9", "commandType": "dropTip", "params": { "pipetteId": "pipetteId", diff --git a/shared-data/protocol/fixtures/6/simpleV6.json b/shared-data/protocol/fixtures/6/simpleV6.json index 6349396dcf6..65745a1decb 100644 --- a/shared-data/protocol/fixtures/6/simpleV6.json +++ b/shared-data/protocol/fixtures/6/simpleV6.json @@ -1239,7 +1239,7 @@ "commands": [ { "commandType": "loadPipette", - "id": "0abc123", + "key": "0abc123", "params": { "pipetteId": "pipetteId", "mount": "left" @@ -1247,7 +1247,7 @@ }, { "commandType": "loadModule", - "id": "1abc123", + "key": "1abc123", "params": { "moduleId": "magneticModuleId", "location": { "slotName": "3" } @@ -1255,7 +1255,7 @@ }, { "commandType": "loadModule", - "id": "2abc123", + "key": "2abc123", "params": { "moduleId": "temperatureModuleId", "location": { "slotName": "1" } @@ -1263,7 +1263,7 @@ }, { "commandType": "loadLabware", - "id": "3abc123", + "key": "3abc123", "params": { "labwareId": "sourcePlateId", "location": { @@ -1273,7 +1273,7 @@ }, { "commandType": "loadLabware", - "id": "4abc123", + "key": "4abc123", "params": { "labwareId": "destPlateId", "location": { @@ -1283,7 +1283,7 @@ }, { "commandType": "loadLabware", - "id": "5abc123", + "key": "5abc123", "params": { "labwareId": "tipRackId", "location": { "slotName": "8" } @@ -1291,7 +1291,7 @@ }, { "commandType": "loadLiquid", - "id": "7abc123", + "key": "7abc123", "params": { "liquidId": "waterId", "labwareId": "sourcePlateId", @@ -1303,12 +1303,12 @@ }, { "commandType": "home", - "id": "00abc123", + "key": "00abc123", "params": {} }, { "commandType": "pickUpTip", - "id": "8abc123", + "key": "8abc123", "params": { "pipetteId": "pipetteId", "labwareId": "tipRackId", @@ -1317,7 +1317,7 @@ }, { "commandType": "aspirate", - "id": "9abc123", + "key": "9abc123", "params": { "pipetteId": "pipetteId", "labwareId": "sourcePlateId", @@ -1332,14 +1332,14 @@ }, { "commandType": "delay", - "id": "10abc123", + "key": "10abc123", "params": { "seconds": 42 } }, { "commandType": "dispense", - "id": "11abc123", + "key": "11abc123", "params": { "pipetteId": "pipetteId", "labwareId": "destPlateId", @@ -1354,7 +1354,7 @@ }, { "commandType": "touchTip", - "id": "12abc123", + "key": "12abc123", "params": { "pipetteId": "pipetteId", "labwareId": "destPlateId", @@ -1369,7 +1369,7 @@ }, { "commandType": "blowout", - "id": "13abc123", + "key": "13abc123", "params": { "pipetteId": "pipetteId", "labwareId": "destPlateId", @@ -1383,7 +1383,7 @@ }, { "commandType": "moveToWell", - "id": "15abc123", + "key": "15abc123", "params": { "pipetteId": "pipetteId", "labwareId": "destPlateId", @@ -1392,7 +1392,7 @@ }, { "commandType": "moveToWell", - "id": "16abc123", + "key": "16abc123", "params": { "pipetteId": "pipetteId", "labwareId": "destPlateId", @@ -1408,7 +1408,7 @@ }, { "commandType": "dropTip", - "id": "17abc123", + "key": "17abc123", "params": { "pipetteId": "pipetteId", "labwareId": "fixedTrash", @@ -1417,7 +1417,7 @@ }, { "commandType": "waitForResume", - "id": "18abc123", + "key": "18abc123", "params": { "message": "pause command" } @@ -1425,7 +1425,7 @@ { "commandType": "moveToCoordinates", - "id": "16abc123", + "key": "16abc123", "params": { "pipetteId": "pipetteId", "coordinates": { "x": 0, "y": 0, "z": 0 }, @@ -1436,7 +1436,7 @@ }, { "commandType": "moveRelative", - "id": "18abc123", + "key": "18abc123", "params": { "pipetteId": "pipetteId", "axis": "x", @@ -1445,7 +1445,7 @@ }, { "commandType": "moveRelative", - "id": "19abc123", + "key": "19abc123", "params": { "pipetteId": "pipetteId", "axis": "y", @@ -1454,14 +1454,14 @@ }, { "commandType": "savePosition", - "id": "21abc123", + "key": "21abc123", "params": { "pipetteId": "pipetteId" } }, { "commandType": "moveRelative", - "id": "20abc123", + "key": "20abc123", "params": { "pipetteId": "pipetteId", "axis": "z", @@ -1470,7 +1470,7 @@ }, { "commandType": "savePosition", - "id": "21abc123", + "key": "21abc123", "params": { "pipetteId": "pipetteId", "positionId": "positionId" diff --git a/shared-data/protocol/fixtures/6/tempAndMagModuleCommands.json b/shared-data/protocol/fixtures/6/tempAndMagModuleCommands.json index ad3031beaa4..167d9aa08da 100644 --- a/shared-data/protocol/fixtures/6/tempAndMagModuleCommands.json +++ b/shared-data/protocol/fixtures/6/tempAndMagModuleCommands.json @@ -1238,7 +1238,7 @@ "commands": [ { "commandType": "loadPipette", - "id": "0abc123", + "key": "0abc123", "params": { "pipetteId": "pipetteId", "mount": "left" @@ -1246,7 +1246,7 @@ }, { "commandType": "loadModule", - "id": "1abc123", + "key": "1abc123", "params": { "moduleId": "magneticModuleId", "location": { "slotName": "3" } @@ -1254,7 +1254,7 @@ }, { "commandType": "loadModule", - "id": "2abc123", + "key": "2abc123", "params": { "moduleId": "temperatureModuleId", "location": { "slotName": "1" } @@ -1262,7 +1262,7 @@ }, { "commandType": "loadLabware", - "id": "3abc123", + "key": "3abc123", "params": { "labwareId": "sourcePlateId", "location": { @@ -1272,7 +1272,7 @@ }, { "commandType": "loadLabware", - "id": "4abc123", + "key": "4abc123", "params": { "labwareId": "destPlateId", "location": { @@ -1282,7 +1282,7 @@ }, { "commandType": "loadLabware", - "id": "5abc123", + "key": "5abc123", "params": { "labwareId": "tipRackId", "location": { "slotName": "8" } @@ -1290,7 +1290,7 @@ }, { "commandType": "loadLabware", - "id": "6abc123", + "key": "6abc123", "params": { "labwareId": "fixedTrash", "location": { @@ -1300,7 +1300,7 @@ }, { "commandType": "loadLiquid", - "id": "7abc123", + "key": "7abc123", "params": { "liquidId": "waterId", "labwareId": "sourcePlateId", @@ -1313,7 +1313,7 @@ { "commandType": "pickUpTip", - "id": "8abc123", + "key": "8abc123", "params": { "pipetteId": "pipetteId", "labwareId": "tipRackId", @@ -1322,7 +1322,7 @@ }, { "commandType": "aspirate", - "id": "9abc123", + "key": "9abc123", "params": { "pipetteId": "pipetteId", "labwareId": "sourcePlateId", @@ -1337,14 +1337,14 @@ }, { "commandType": "delay", - "id": "10abc123", + "key": "10abc123", "params": { "seconds": 42 } }, { "commandType": "dispense", - "id": "11abc123", + "key": "11abc123", "params": { "pipetteId": "pipetteId", "labwareId": "destPlateId", @@ -1359,7 +1359,7 @@ }, { "commandType": "temperatureModule/setTargetTemperature", - "id": "12abc123", + "key": "12abc123", "params": { "moduleId": "temperatureModuleId", "celsius": 80 @@ -1367,7 +1367,7 @@ }, { "commandType": "temperatureModule/waitForTemperature", - "id": "13abc123", + "key": "13abc123", "params": { "moduleId": "temperatureModuleId", "celsius": 80 @@ -1375,14 +1375,14 @@ }, { "commandType": "temperatureModule/deactivate", - "id": "14abc123", + "key": "14abc123", "params": { "moduleId": "temperatureModuleId" } }, { "commandType": "magneticModule/engage", - "id": "15abc123", + "key": "15abc123", "params": { "moduleId": "magneticModuleId", "height": 10 @@ -1390,7 +1390,7 @@ }, { "commandType": "magneticModule/disengage", - "id": "16abc123", + "key": "16abc123", "params": { "moduleId": "magneticModuleId" } diff --git a/shared-data/protocol/fixtures/6/transferSettings.json b/shared-data/protocol/fixtures/6/transferSettings.json index 2f316d324d8..28c12fa9700 100644 --- a/shared-data/protocol/fixtures/6/transferSettings.json +++ b/shared-data/protocol/fixtures/6/transferSettings.json @@ -4561,7 +4561,7 @@ }, "commands": [ { - "id": "0", + "key": "0", "commandType": "loadPipette", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -4569,7 +4569,7 @@ } }, { - "id": "1", + "key": "1", "commandType": "loadPipette", "params": { "pipetteId": "4da579b0-a9bf-11eb-bce6-9f1d5b9c1a1b", @@ -4577,7 +4577,7 @@ } }, { - "id": "2", + "key": "2", "commandType": "loadModule", "params": { "moduleId": "3e012450-3412-11eb-ad93-ed232a2337cf:magneticModuleType", @@ -4590,7 +4590,7 @@ } }, { - "id": "3", + "key": "3", "commandType": "loadModule", "params": { "moduleId": "3e0283e0-3412-11eb-ad93-ed232a2337cf:temperatureModuleType", @@ -4603,7 +4603,7 @@ } }, { - "id": "4", + "key": "4", "commandType": "loadModule", "params": { "moduleId": "3e039550-3412-11eb-ad93-ed232a2337cf:thermocyclerModuleType", @@ -4616,7 +4616,7 @@ } }, { - "id": "5", + "key": "5", "commandType": "loadLabware", "params": { "labwareId": "fixedTrash", @@ -4683,7 +4683,7 @@ } }, { - "id": "6", + "key": "6", "commandType": "loadLabware", "params": { "labwareId": "3e047fb0-3412-11eb-ad93-ed232a2337cf:opentrons/opentrons_96_tiprack_1000ul/1", @@ -5715,7 +5715,7 @@ } }, { - "id": "7", + "key": "7", "commandType": "loadLabware", "params": { "labwareId": "5ae317e0-3412-11eb-ad93-ed232a2337cf:opentrons/nest_1_reservoir_195ml/1", @@ -5784,7 +5784,7 @@ } }, { - "id": "8", + "key": "8", "commandType": "loadLabware", "params": { "labwareId": "aac5d680-3412-11eb-ad93-ed232a2337cf:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1", @@ -6815,7 +6815,7 @@ } }, { - "id": "9", + "key": "9", "commandType": "loadLabware", "params": { "labwareId": "60e8b050-3412-11eb-ad93-ed232a2337cf:opentrons/corning_24_wellplate_3.4ml_flat/1", @@ -7121,7 +7121,7 @@ } }, { - "id": "10", + "key": "10", "commandType": "loadLabware", "params": { "labwareId": "ada13110-3412-11eb-ad93-ed232a2337cf:opentrons/opentrons_96_aluminumblock_generic_pcr_strip_200ul/1", @@ -8160,7 +8160,7 @@ } }, { - "id": "11", + "key": "11", "commandType": "loadLabware", "params": { "labwareId": "b0103540-3412-11eb-ad93-ed232a2337cf:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1", @@ -9191,7 +9191,7 @@ } }, { - "id": "12", + "key": "12", "commandType": "loadLabware", "params": { "labwareId": "faa13a50-a9bf-11eb-bce6-9f1d5b9c1a1b:opentrons/opentrons_96_tiprack_20ul/1", @@ -10223,7 +10223,7 @@ } }, { - "id": "13", + "key": "13", "commandType": "loadLabware", "params": { "labwareId": "53d3b350-a9c0-11eb-bce6-9f1d5b9c1a1b", @@ -10529,7 +10529,7 @@ } }, { - "id": "14", + "key": "14", "commandType": "pickUpTip", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10538,7 +10538,7 @@ } }, { - "id": "15", + "key": "15", "commandType": "aspirate", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10553,7 +10553,7 @@ } }, { - "id": "16", + "key": "16", "commandType": "dispense", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10568,7 +10568,7 @@ } }, { - "id": "17", + "key": "17", "commandType": "dropTip", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10577,7 +10577,7 @@ } }, { - "id": "18", + "key": "18", "commandType": "pickUpTip", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10586,7 +10586,7 @@ } }, { - "id": "19", + "key": "19", "commandType": "aspirate", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10601,7 +10601,7 @@ } }, { - "id": "20", + "key": "20", "commandType": "dispense", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10616,7 +10616,7 @@ } }, { - "id": "21", + "key": "21", "commandType": "dropTip", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10625,7 +10625,7 @@ } }, { - "id": "22", + "key": "22", "commandType": "pickUpTip", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10634,7 +10634,7 @@ } }, { - "id": "23", + "key": "23", "commandType": "aspirate", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10649,7 +10649,7 @@ } }, { - "id": "24", + "key": "24", "commandType": "dispense", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10664,7 +10664,7 @@ } }, { - "id": "25", + "key": "25", "commandType": "dropTip", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10673,7 +10673,7 @@ } }, { - "id": "26", + "key": "26", "commandType": "pickUpTip", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10682,7 +10682,7 @@ } }, { - "id": "27", + "key": "27", "commandType": "aspirate", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10697,7 +10697,7 @@ } }, { - "id": "28", + "key": "28", "commandType": "dispense", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10712,7 +10712,7 @@ } }, { - "id": "29", + "key": "29", "commandType": "dropTip", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10721,7 +10721,7 @@ } }, { - "id": "30", + "key": "30", "commandType": "pickUpTip", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10730,7 +10730,7 @@ } }, { - "id": "31", + "key": "31", "commandType": "aspirate", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10745,7 +10745,7 @@ } }, { - "id": "32", + "key": "32", "commandType": "dispense", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10760,7 +10760,7 @@ } }, { - "id": "33", + "key": "33", "commandType": "dropTip", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10769,7 +10769,7 @@ } }, { - "id": "34", + "key": "34", "commandType": "pickUpTip", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10778,7 +10778,7 @@ } }, { - "id": "35", + "key": "35", "commandType": "aspirate", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10793,7 +10793,7 @@ } }, { - "id": "36", + "key": "36", "commandType": "dispense", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10808,7 +10808,7 @@ } }, { - "id": "37", + "key": "37", "commandType": "dropTip", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10817,7 +10817,7 @@ } }, { - "id": "38", + "key": "38", "commandType": "pickUpTip", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10826,7 +10826,7 @@ } }, { - "id": "39", + "key": "39", "commandType": "aspirate", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10841,7 +10841,7 @@ } }, { - "id": "40", + "key": "40", "commandType": "dispense", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10856,7 +10856,7 @@ } }, { - "id": "41", + "key": "41", "commandType": "dropTip", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10865,7 +10865,7 @@ } }, { - "id": "42", + "key": "42", "commandType": "pickUpTip", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10874,7 +10874,7 @@ } }, { - "id": "43", + "key": "43", "commandType": "aspirate", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10889,7 +10889,7 @@ } }, { - "id": "44", + "key": "44", "commandType": "dispense", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10904,7 +10904,7 @@ } }, { - "id": "45", + "key": "45", "commandType": "dropTip", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10913,7 +10913,7 @@ } }, { - "id": "46", + "key": "46", "commandType": "pickUpTip", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10922,7 +10922,7 @@ } }, { - "id": "47", + "key": "47", "commandType": "aspirate", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10937,7 +10937,7 @@ } }, { - "id": "48", + "key": "48", "commandType": "dispense", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10952,7 +10952,7 @@ } }, { - "id": "49", + "key": "49", "commandType": "dropTip", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10961,7 +10961,7 @@ } }, { - "id": "50", + "key": "50", "commandType": "pickUpTip", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10970,7 +10970,7 @@ } }, { - "id": "51", + "key": "51", "commandType": "aspirate", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -10985,7 +10985,7 @@ } }, { - "id": "52", + "key": "52", "commandType": "dispense", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -11000,7 +11000,7 @@ } }, { - "id": "53", + "key": "53", "commandType": "dropTip", "params": { "pipetteId": "3dff4f90-3412-11eb-ad93-ed232a2337cf", @@ -11009,7 +11009,7 @@ } }, { - "id": "54", + "key": "54", "commandType": "pickUpTip", "params": { "pipetteId": "4da579b0-a9bf-11eb-bce6-9f1d5b9c1a1b", @@ -11018,7 +11018,7 @@ } }, { - "id": "55", + "key": "55", "commandType": "aspirate", "params": { "pipetteId": "4da579b0-a9bf-11eb-bce6-9f1d5b9c1a1b", @@ -11033,7 +11033,7 @@ } }, { - "id": "56", + "key": "56", "commandType": "dispense", "params": { "pipetteId": "4da579b0-a9bf-11eb-bce6-9f1d5b9c1a1b", @@ -11048,7 +11048,7 @@ } }, { - "id": "57", + "key": "57", "commandType": "dropTip", "params": { "pipetteId": "4da579b0-a9bf-11eb-bce6-9f1d5b9c1a1b", From e2625a2320b4b4addccbad8d7d35006c73600993 Mon Sep 17 00:00:00 2001 From: koji Date: Wed, 17 Jul 2024 14:39:34 -0400 Subject: [PATCH 42/78] fix(app): fix modal display area (#15672) * fix(app): fix modal display area --- app/src/App/DesktopApp.tsx | 15 ++++++++++----- .../molecules/LegacyModal/LegacyModalShell.tsx | 10 +++++----- .../AddFixtureModal.tsx | 2 +- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/app/src/App/DesktopApp.tsx b/app/src/App/DesktopApp.tsx index 5576d995a1f..910f9a9895d 100644 --- a/app/src/App/DesktopApp.tsx +++ b/app/src/App/DesktopApp.tsx @@ -130,14 +130,19 @@ export const DesktopApp = (): JSX.Element => { - - + + + + ) diff --git a/app/src/molecules/LegacyModal/LegacyModalShell.tsx b/app/src/molecules/LegacyModal/LegacyModalShell.tsx index 431b607c815..7f9378433dc 100644 --- a/app/src/molecules/LegacyModal/LegacyModalShell.tsx +++ b/app/src/molecules/LegacyModal/LegacyModalShell.tsx @@ -1,17 +1,17 @@ import * as React from 'react' import styled from 'styled-components' import { - COLORS, - POSITION_ABSOLUTE, ALIGN_CENTER, + BORDERS, + COLORS, JUSTIFY_CENTER, - POSITION_RELATIVE, OVERFLOW_AUTO, + POSITION_ABSOLUTE, + POSITION_RELATIVE, POSITION_STICKY, - BORDERS, RESPONSIVENESS, - styleProps, SPACING, + styleProps, } from '@opentrons/components' import type { StyleProps } from '@opentrons/components' export interface LegacyModalShellProps extends StyleProps { diff --git a/app/src/organisms/DeviceDetailsDeckConfiguration/AddFixtureModal.tsx b/app/src/organisms/DeviceDetailsDeckConfiguration/AddFixtureModal.tsx index 38627396e08..756ca93acb7 100644 --- a/app/src/organisms/DeviceDetailsDeckConfiguration/AddFixtureModal.tsx +++ b/app/src/organisms/DeviceDetailsDeckConfiguration/AddFixtureModal.tsx @@ -11,8 +11,8 @@ import { DIRECTION_ROW, Flex, JUSTIFY_SPACE_BETWEEN, - SPACING, LegacyStyledText, + SPACING, TYPOGRAPHY, } from '@opentrons/components' import { From 63ea53f39a38a77e7b92efa9852a2ed0502a3a89 Mon Sep 17 00:00:00 2001 From: Rhyann Clarke <146747548+rclarke0@users.noreply.github.com> Date: Wed, 17 Jul 2024 14:50:16 -0400 Subject: [PATCH 43/78] fix(hardware-testing): fix ABR asair sensor time adjustment (#15658) # Overview Change asair time adjustment from 5 hrs to 4 hrs to account for daylight savings. # Test Plan # Changelog # Review requests # Risk assessment --- .../data_collection/abr_robot_error.py | 24 +- hardware-testing/Pipfile | 1 + hardware-testing/Pipfile.lock | 351 +++++++++--------- .../scripts/abr_asair_sensor.py | 11 +- 4 files changed, 189 insertions(+), 198 deletions(-) diff --git a/abr-testing/abr_testing/data_collection/abr_robot_error.py b/abr-testing/abr_testing/data_collection/abr_robot_error.py index b42a195f52a..a45e64cd86d 100644 --- a/abr-testing/abr_testing/data_collection/abr_robot_error.py +++ b/abr-testing/abr_testing/data_collection/abr_robot_error.py @@ -1,5 +1,5 @@ """Create ticket for robot with error.""" -from typing import List, Tuple, Any, Dict +from typing import List, Tuple, Any, Dict, Optional from abr_testing.data_collection import read_robot_logs, abr_google_drive, get_run_logs import requests import argparse @@ -18,7 +18,7 @@ def compare_current_trh_to_average( robot: str, start_time: Any, - end_time: Any, + end_time: Optional[Any], protocol_name: str, storage_directory: str, ) -> str: @@ -38,27 +38,29 @@ def compare_current_trh_to_average( df_all_trh = pd.DataFrame(all_trh_data) # Convert timestamps to datetime objects df_all_trh["Timestamp"] = pd.to_datetime( - df_all_trh["Timestamp"], format="mixed" + df_all_trh["Timestamp"], format="mixed", utc=True ).dt.tz_localize(None) # Ensure start_time is timezone-naive start_time = start_time.replace(tzinfo=None) - end_time = end_time.replace(tzinfo=None) relevant_temp_rhs = df_all_trh[ - (df_all_trh["Robot"] == robot) - & (df_all_trh["Timestamp"] >= start_time) - & (df_all_trh["Timestamp"] <= end_time) + (df_all_trh["Robot"] == robot) & (df_all_trh["Timestamp"] >= start_time) ] try: avg_temp = round(mean(relevant_temp_rhs["Temp (oC)"]), 2) avg_rh = round(mean(relevant_temp_rhs["Relative Humidity (%)"]), 2) except StatisticsError: - avg_temp = None - avg_rh = None + # If there is one value assign it as the average. + if len(relevant_temp_rhs["Temp (oC)"]) == 1: + avg_temp = relevant_temp_rhs["Temp (oC)"][0] + avg_rh = relevant_temp_rhs["Relative Humidity (%)"][0] + else: + avg_temp = None + avg_rh = None # Get AVG t/rh of runs w/ same robot & protocol newer than 3 wks old with no errors weeks_ago_3 = start_time - timedelta(weeks=3) df_all_run_data = pd.DataFrame(all_run_data) df_all_run_data["Start_Time"] = pd.to_datetime( - df_all_run_data["Start_Time"], format="mixed" + df_all_run_data["Start_Time"], format="mixed", utc=True ).dt.tz_localize(None) df_all_run_data["Errors"] = pd.to_numeric(df_all_run_data["Errors"]) df_all_run_data["Average Temp (oC)"] = pd.to_numeric( @@ -283,7 +285,9 @@ def get_robot_state( components = match_error_to_component("RABR", reported_string, components) print(components) end_time = datetime.now() + print(end_time) start_time = end_time - timedelta(hours=2) + print(start_time) # Get current temp/rh compared to historical data temp_rh_string = compare_current_trh_to_average( parent, start_time, end_time, "", storage_directory diff --git a/hardware-testing/Pipfile b/hardware-testing/Pipfile index 4f9d82e5964..1cbf12ae8b6 100644 --- a/hardware-testing/Pipfile +++ b/hardware-testing/Pipfile @@ -10,6 +10,7 @@ opentrons-hardware = {editable = true, path = "./../hardware", extras=['FLEX']} hardware-testing = { editable = true, path = "." } abr-testing = { editable = true, path = "./../abr-testing" } pyserial = "==3.5" +types-pytz = "*" [dev-packages] atomicwrites = "==1.4.1" diff --git a/hardware-testing/Pipfile.lock b/hardware-testing/Pipfile.lock index 7b9381ef39e..026dae40f4b 100644 --- a/hardware-testing/Pipfile.lock +++ b/hardware-testing/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "1e609e94df92fa225c1352401ecd3c21e2c7ec319754ae3f209155553712704f" + "sha256": "aa401ceace7aebaa4a5a7727066f35d36bb28179ae1850fbdffda93edf48e7b0" }, "pipfile-spec": 6, "requires": { @@ -25,6 +25,7 @@ "sha256:25816a9eef030c774beaee22189a24e29bc43f81cebe574ef723851eaf89ddee", "sha256:9651e1373873c75786101330e302e114f85b6e8b5ad70b491497c8b3609a8449" ], + "markers": "python_version >= '3.8'", "version": "==0.3.1" }, "anyio": { @@ -51,13 +52,21 @@ "markers": "python_version >= '3.7'", "version": "==8.1.7" }, + "colorama": { + "hashes": [ + "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", + "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6" + ], + "markers": "platform_system == 'Windows'", + "version": "==0.4.6" + }, "exceptiongroup": { "hashes": [ - "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad", - "sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16" + "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", + "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc" ], "markers": "python_version < '3.11'", - "version": "==1.2.1" + "version": "==1.2.2" }, "hardware-testing": { "editable": true, @@ -79,68 +88,6 @@ "markers": "python_version >= '3.7'", "version": "==4.17.3" }, - "msgpack": { - "hashes": [ - "sha256:00e073efcba9ea99db5acef3959efa45b52bc67b61b00823d2a1a6944bf45982", - "sha256:0726c282d188e204281ebd8de31724b7d749adebc086873a59efb8cf7ae27df3", - "sha256:0ceea77719d45c839fd73abcb190b8390412a890df2f83fb8cf49b2a4b5c2f40", - "sha256:114be227f5213ef8b215c22dde19532f5da9652e56e8ce969bf0a26d7c419fee", - "sha256:13577ec9e247f8741c84d06b9ece5f654920d8365a4b636ce0e44f15e07ec693", - "sha256:1876b0b653a808fcd50123b953af170c535027bf1d053b59790eebb0aeb38950", - "sha256:1ab0bbcd4d1f7b6991ee7c753655b481c50084294218de69365f8f1970d4c151", - "sha256:1cce488457370ffd1f953846f82323cb6b2ad2190987cd4d70b2713e17268d24", - "sha256:26ee97a8261e6e35885c2ecd2fd4a6d38252246f94a2aec23665a4e66d066305", - "sha256:3528807cbbb7f315bb81959d5961855e7ba52aa60a3097151cb21956fbc7502b", - "sha256:374a8e88ddab84b9ada695d255679fb99c53513c0a51778796fcf0944d6c789c", - "sha256:376081f471a2ef24828b83a641a02c575d6103a3ad7fd7dade5486cad10ea659", - "sha256:3923a1778f7e5ef31865893fdca12a8d7dc03a44b33e2a5f3295416314c09f5d", - "sha256:4916727e31c28be8beaf11cf117d6f6f188dcc36daae4e851fee88646f5b6b18", - "sha256:493c5c5e44b06d6c9268ce21b302c9ca055c1fd3484c25ba41d34476c76ee746", - "sha256:505fe3d03856ac7d215dbe005414bc28505d26f0c128906037e66d98c4e95868", - "sha256:5845fdf5e5d5b78a49b826fcdc0eb2e2aa7191980e3d2cfd2a30303a74f212e2", - "sha256:5c330eace3dd100bdb54b5653b966de7f51c26ec4a7d4e87132d9b4f738220ba", - "sha256:5dbf059fb4b7c240c873c1245ee112505be27497e90f7c6591261c7d3c3a8228", - "sha256:5e390971d082dba073c05dbd56322427d3280b7cc8b53484c9377adfbae67dc2", - "sha256:5fbb160554e319f7b22ecf530a80a3ff496d38e8e07ae763b9e82fadfe96f273", - "sha256:64d0fcd436c5683fdd7c907eeae5e2cbb5eb872fafbc03a43609d7941840995c", - "sha256:69284049d07fce531c17404fcba2bb1df472bc2dcdac642ae71a2d079d950653", - "sha256:6a0e76621f6e1f908ae52860bdcb58e1ca85231a9b0545e64509c931dd34275a", - "sha256:73ee792784d48aa338bba28063e19a27e8d989344f34aad14ea6e1b9bd83f596", - "sha256:74398a4cf19de42e1498368c36eed45d9528f5fd0155241e82c4082b7e16cffd", - "sha256:7938111ed1358f536daf311be244f34df7bf3cdedb3ed883787aca97778b28d8", - "sha256:82d92c773fbc6942a7a8b520d22c11cfc8fd83bba86116bfcf962c2f5c2ecdaa", - "sha256:83b5c044f3eff2a6534768ccfd50425939e7a8b5cf9a7261c385de1e20dcfc85", - "sha256:8db8e423192303ed77cff4dce3a4b88dbfaf43979d280181558af5e2c3c71afc", - "sha256:9517004e21664f2b5a5fd6333b0731b9cf0817403a941b393d89a2f1dc2bd836", - "sha256:95c02b0e27e706e48d0e5426d1710ca78e0f0628d6e89d5b5a5b91a5f12274f3", - "sha256:99881222f4a8c2f641f25703963a5cefb076adffd959e0558dc9f803a52d6a58", - "sha256:9ee32dcb8e531adae1f1ca568822e9b3a738369b3b686d1477cbc643c4a9c128", - "sha256:a22e47578b30a3e199ab067a4d43d790249b3c0587d9a771921f86250c8435db", - "sha256:b5505774ea2a73a86ea176e8a9a4a7c8bf5d521050f0f6f8426afe798689243f", - "sha256:bd739c9251d01e0279ce729e37b39d49a08c0420d3fee7f2a4968c0576678f77", - "sha256:d16a786905034e7e34098634b184a7d81f91d4c3d246edc6bd7aefb2fd8ea6ad", - "sha256:d3420522057ebab1728b21ad473aa950026d07cb09da41103f8e597dfbfaeb13", - "sha256:d56fd9f1f1cdc8227d7b7918f55091349741904d9520c65f0139a9755952c9e8", - "sha256:d661dc4785affa9d0edfdd1e59ec056a58b3dbb9f196fa43587f3ddac654ac7b", - "sha256:dfe1f0f0ed5785c187144c46a292b8c34c1295c01da12e10ccddfc16def4448a", - "sha256:e1dd7839443592d00e96db831eddb4111a2a81a46b028f0facd60a09ebbdd543", - "sha256:e2872993e209f7ed04d963e4b4fbae72d034844ec66bc4ca403329db2074377b", - "sha256:e2f879ab92ce502a1e65fce390eab619774dda6a6ff719718069ac94084098ce", - "sha256:e3aa7e51d738e0ec0afbed661261513b38b3014754c9459508399baf14ae0c9d", - "sha256:e532dbd6ddfe13946de050d7474e3f5fb6ec774fbb1a188aaf469b08cf04189a", - "sha256:e6b7842518a63a9f17107eb176320960ec095a8ee3b4420b5f688e24bf50c53c", - "sha256:e75753aeda0ddc4c28dce4c32ba2f6ec30b1b02f6c0b14e547841ba5b24f753f", - "sha256:eadb9f826c138e6cf3c49d6f8de88225a3c0ab181a9b4ba792e006e5292d150e", - "sha256:ed59dd52075f8fc91da6053b12e8c89e37aa043f8986efd89e61fae69dc1b011", - "sha256:ef254a06bcea461e65ff0373d8a0dd1ed3aa004af48839f002a0c994a6f72d04", - "sha256:f3709997b228685fe53e8c433e2df9f0cdb5f4542bd5114ed17ac3c0129b0480", - "sha256:f51bab98d52739c50c56658cc303f190785f9a2cd97b823357e7aeae54c8f68a", - "sha256:f9904e24646570539a8950400602d66d2b2c492b9010ea7e965025cb71d0c86d", - "sha256:f9af38a89b6a5c04b7d18c492c8ccf2aee7048aff1ce8437c4683bb5a1df893d" - ], - "markers": "platform_system != 'Windows'", - "version": "==1.0.8" - }, "numpy": { "hashes": [ "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b", @@ -205,53 +152,60 @@ }, "packaging": { "hashes": [ - "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5", - "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9" + "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002", + "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124" ], - "markers": "python_version >= '3.7'", - "version": "==24.0" + "markers": "python_version >= '3.8'", + "version": "==24.1" }, "pydantic": { "hashes": [ - "sha256:005655cabc29081de8243126e036f2065bd7ea5b9dff95fde6d2c642d39755de", - "sha256:0d142fa1b8f2f0ae11ddd5e3e317dcac060b951d605fda26ca9b234b92214986", - "sha256:22ed12ee588b1df028a2aa5d66f07bf8f8b4c8579c2e96d5a9c1f96b77f3bb55", - "sha256:2746189100c646682eff0bce95efa7d2e203420d8e1c613dc0c6b4c1d9c1fde4", - "sha256:28e552a060ba2740d0d2aabe35162652c1459a0b9069fe0db7f4ee0e18e74d58", - "sha256:3287e1614393119c67bd4404f46e33ae3be3ed4cd10360b48d0a4459f420c6a3", - "sha256:3350f527bb04138f8aff932dc828f154847fbdc7a1a44c240fbfff1b57f49a12", - "sha256:3453685ccd7140715e05f2193d64030101eaad26076fad4e246c1cc97e1bb30d", - "sha256:394f08750bd8eaad714718812e7fab615f873b3cdd0b9d84e76e51ef3b50b6b7", - "sha256:4e316e54b5775d1eb59187f9290aeb38acf620e10f7fd2f776d97bb788199e53", - "sha256:50f1666a9940d3d68683c9d96e39640f709d7a72ff8702987dab1761036206bb", - "sha256:51d405b42f1b86703555797270e4970a9f9bd7953f3990142e69d1037f9d9e51", - "sha256:584f2d4c98ffec420e02305cf675857bae03c9d617fcfdc34946b1160213a948", - "sha256:5e09c19df304b8123938dc3c53d3d3be6ec74b9d7d0d80f4f4b5432ae16c2022", - "sha256:676ed48f2c5bbad835f1a8ed8a6d44c1cd5a21121116d2ac40bd1cd3619746ed", - "sha256:67f1a1fb467d3f49e1708a3f632b11c69fccb4e748a325d5a491ddc7b5d22383", - "sha256:6a51a1dd4aa7b3f1317f65493a182d3cff708385327c1c82c81e4a9d6d65b2e4", - "sha256:6bd7030c9abc80134087d8b6e7aa957e43d35714daa116aced57269a445b8f7b", - "sha256:75279d3cac98186b6ebc2597b06bcbc7244744f6b0b44a23e4ef01e5683cc0d2", - "sha256:7ac9237cd62947db00a0d16acf2f3e00d1ae9d3bd602b9c415f93e7a9fc10528", - "sha256:7ea210336b891f5ea334f8fc9f8f862b87acd5d4a0cbc9e3e208e7aa1775dabf", - "sha256:82790d4753ee5d00739d6cb5cf56bceb186d9d6ce134aca3ba7befb1eedbc2c8", - "sha256:92229f73400b80c13afcd050687f4d7e88de9234d74b27e6728aa689abcf58cc", - "sha256:9bea1f03b8d4e8e86702c918ccfd5d947ac268f0f0cc6ed71782e4b09353b26f", - "sha256:a980a77c52723b0dc56640ced396b73a024d4b74f02bcb2d21dbbac1debbe9d0", - "sha256:af9850d98fc21e5bc24ea9e35dd80a29faf6462c608728a110c0a30b595e58b7", - "sha256:bbc6989fad0c030bd70a0b6f626f98a862224bc2b1e36bfc531ea2facc0a340c", - "sha256:be51dd2c8596b25fe43c0a4a59c2bee4f18d88efb8031188f9e7ddc6b469cf44", - "sha256:c365ad9c394f9eeffcb30a82f4246c0006417f03a7c0f8315d6211f25f7cb654", - "sha256:c3d5731a120752248844676bf92f25a12f6e45425e63ce22e0849297a093b5b0", - "sha256:ca832e124eda231a60a041da4f013e3ff24949d94a01154b137fc2f2a43c3ffb", - "sha256:d207d5b87f6cbefbdb1198154292faee8017d7495a54ae58db06762004500d00", - "sha256:d31ee5b14a82c9afe2bd26aaa405293d4237d0591527d9129ce36e58f19f95c1", - "sha256:d3b5c4cbd0c9cb61bbbb19ce335e1f8ab87a811f6d589ed52b0254cf585d709c", - "sha256:d573082c6ef99336f2cb5b667b781d2f776d4af311574fb53d908517ba523c22", - "sha256:e49db944fad339b2ccb80128ffd3f8af076f9f287197a480bf1e4ca053a866f0" + "sha256:098ad8de840c92ea586bf8efd9e2e90c6339d33ab5c1cfbb85be66e4ecf8213f", + "sha256:0e2495309b1266e81d259a570dd199916ff34f7f51f1b549a0d37a6d9b17b4dc", + "sha256:0fa51175313cc30097660b10eec8ca55ed08bfa07acbfe02f7a42f6c242e9a4b", + "sha256:11289fa895bcbc8f18704efa1d8020bb9a86314da435348f59745473eb042e6b", + "sha256:2a72d2a5ff86a3075ed81ca031eac86923d44bc5d42e719d585a8eb547bf0c9b", + "sha256:371dcf1831f87c9e217e2b6a0c66842879a14873114ebb9d0861ab22e3b5bb1e", + "sha256:409b2b36d7d7d19cd8310b97a4ce6b1755ef8bd45b9a2ec5ec2b124db0a0d8f3", + "sha256:4866a1579c0c3ca2c40575398a24d805d4db6cb353ee74df75ddeee3c657f9a7", + "sha256:48db882e48575ce4b39659558b2f9f37c25b8d348e37a2b4e32971dd5a7d6227", + "sha256:525bbef620dac93c430d5d6bdbc91bdb5521698d434adf4434a7ef6ffd5c4b7f", + "sha256:543da3c6914795b37785703ffc74ba4d660418620cc273490d42c53949eeeca6", + "sha256:62d96b8799ae3d782df7ec9615cb59fc32c32e1ed6afa1b231b0595f6516e8ab", + "sha256:6654028d1144df451e1da69a670083c27117d493f16cf83da81e1e50edce72ad", + "sha256:7017971ffa7fd7808146880aa41b266e06c1e6e12261768a28b8b41ba55c8076", + "sha256:7623b59876f49e61c2e283551cc3647616d2fbdc0b4d36d3d638aae8547ea681", + "sha256:7e17c0ee7192e54a10943f245dc79e36d9fe282418ea05b886e1c666063a7b54", + "sha256:820ae12a390c9cbb26bb44913c87fa2ff431a029a785642c1ff11fed0a095fcb", + "sha256:94833612d6fd18b57c359a127cbfd932d9150c1b72fea7c86ab58c2a77edd7c7", + "sha256:95ef534e3c22e5abbdbdd6f66b6ea9dac3ca3e34c5c632894f8625d13d084cbe", + "sha256:9c803a5113cfab7bbb912f75faa4fc1e4acff43e452c82560349fff64f852e1b", + "sha256:9e53fb834aae96e7b0dadd6e92c66e7dd9cdf08965340ed04c16813102a47fab", + "sha256:ab2f976336808fd5d539fdc26eb51f9aafc1f4b638e212ef6b6f05e753c8011d", + "sha256:ad1e33dc6b9787a6f0f3fd132859aa75626528b49cc1f9e429cdacb2608ad5f0", + "sha256:ae5184e99a060a5c80010a2d53c99aee76a3b0ad683d493e5f0620b5d86eeb75", + "sha256:aeb4e741782e236ee7dc1fb11ad94dc56aabaf02d21df0e79e0c21fe07c95741", + "sha256:b4ad32aed3bf5eea5ca5decc3d1bbc3d0ec5d4fbcd72a03cdad849458decbc63", + "sha256:b8ad363330557beac73159acfbeed220d5f1bfcd6b930302a987a375e02f74fd", + "sha256:bfbb18b616abc4df70591b8c1ff1b3eabd234ddcddb86b7cac82657ab9017e33", + "sha256:c1e51d1af306641b7d1574d6d3307eaa10a4991542ca324f0feb134fee259815", + "sha256:c31d281c7485223caf6474fc2b7cf21456289dbaa31401844069b77160cab9c7", + "sha256:c7e8988bb16988890c985bd2093df9dd731bfb9d5e0860db054c23034fab8f7a", + "sha256:c87cedb4680d1614f1d59d13fea353faf3afd41ba5c906a266f3f2e8c245d655", + "sha256:cafb9c938f61d1b182dfc7d44a7021326547b7b9cf695db5b68ec7b590214773", + "sha256:d2f89a719411cb234105735a520b7c077158a81e0fe1cb05a79c01fc5eb59d3c", + "sha256:d4b40c9e13a0b61583e5599e7950490c700297b4a375b55b2b592774332798b7", + "sha256:d4ecb515fa7cb0e46e163ecd9d52f9147ba57bc3633dca0e586cdb7a232db9e3", + "sha256:d8c209af63ccd7b22fba94b9024e8b7fd07feffee0001efae50dd99316b27768", + "sha256:db3b48d9283d80a314f7a682f7acae8422386de659fffaba454b77a083c3937d", + "sha256:e41b5b973e5c64f674b3b4720286ded184dcc26a691dd55f34391c62c6934688", + "sha256:e840e6b2026920fc3f250ea8ebfdedf6ea7a25b77bf04c6576178e681942ae0f", + "sha256:ebb249096d873593e014535ab07145498957091aa6ae92759a32d40cb9998e2e", + "sha256:f434160fb14b353caf634149baaf847206406471ba70e64657c1e8330277a991", + "sha256:fa43f362b46741df8f201bf3e7dff3569fa92069bcc7b4a740dea3602e27ab7a" ], "markers": "python_version >= '3.7'", - "version": "==1.10.15" + "version": "==1.10.17" }, "pyrsistent": { "hashes": [ @@ -304,15 +258,36 @@ "sha256:6ad50f4613289f3c4d276b6d2ac8901d776dcb929994cce93f55a69e858c595f", "sha256:7eea9b81b0ff908000a825db024313f622895bd578e8a17433e0474cd7d2da83" ], + "markers": "python_version >= '3.7'", "version": "==4.2.2" }, + "pywin32": { + "hashes": [ + "sha256:06d3420a5155ba65f0b72f2699b5bacf3109f36acbe8923765c22938a69dfc8d", + "sha256:1c73ea9a0d2283d889001998059f5eaaba3b6238f767c9cf2833b13e6a685f65", + "sha256:37257794c1ad39ee9be652da0462dc2e394c8159dfd913a8a4e8eb6fd346da0e", + "sha256:383229d515657f4e3ed1343da8be101000562bf514591ff383ae940cad65458b", + "sha256:39b61c15272833b5c329a2989999dcae836b1eed650252ab1b7bfbe1d59f30f4", + "sha256:5821ec52f6d321aa59e2db7e0a35b997de60c201943557d108af9d4ae1ec7040", + "sha256:70dba0c913d19f942a2db25217d9a1b726c278f483a919f1abfed79c9cf64d3a", + "sha256:72c5f621542d7bdd4fdb716227be0dd3f8565c11b280be6315b06ace35487d36", + "sha256:84f4471dbca1887ea3803d8848a1616429ac94a4a8d05f4bc9c5dcfd42ca99c8", + "sha256:a7639f51c184c0272e93f244eb24dafca9b1855707d94c192d4a0b4c01e1100e", + "sha256:e25fd5b485b55ac9c057f67d94bc203f3f6595078d1fb3b458c9c28b7153a802", + "sha256:e4c092e2589b5cf0d365849e73e02c391c1349958c5ac3e9d5ccb9a28e017b3a", + "sha256:e65028133d15b64d2ed8f06dd9fbc268352478d4f9289e69c190ecd6818b6407", + "sha256:e8ac1ae3601bee6ca9f7cb4b5363bf1c0badb935ef243c4733ff9a393b1690c0" + ], + "markers": "platform_system == 'Windows' and platform_python_implementation == 'CPython'", + "version": "==306" + }, "setuptools": { "hashes": [ - "sha256:54faa7f2e8d2d11bcd2c07bed282eef1046b5c080d1c32add737d7b5817b1ad4", - "sha256:f211a66637b8fa059bb28183da127d4e86396c991a942b028c6650d4319c3fd0" + "sha256:f171bab1dfbc86b132997f26a119f6056a57950d058587841a0082e8830f9dc5", + "sha256:fe384da74336c398e0d956d1cae0669bc02eed936cdb1d49b57de1990dc11ffc" ], "markers": "python_version >= '3.8'", - "version": "==70.0.0" + "version": "==70.3.0" }, "sniffio": { "hashes": [ @@ -322,13 +297,22 @@ "markers": "python_version >= '3.7'", "version": "==1.3.1" }, + "types-pytz": { + "hashes": [ + "sha256:6810c8a1f68f21fdf0f4f374a432487c77645a0ac0b31de4bf4690cf21ad3981", + "sha256:8335d443310e2db7b74e007414e74c4f53b67452c0cb0d228ca359ccfba59659" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==2024.1.0.20240417" + }, "typing-extensions": { "hashes": [ - "sha256:8cbcdc8606ebcb0d95453ad7dc5065e6237b6aa230a31e81d0f440c30fed5fd8", - "sha256:b349c66bea9016ac22978d800cfff206d5f9816951f12a7d0ec5578b0a819594" + "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", + "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8" ], "markers": "python_version >= '3.8'", - "version": "==4.12.0" + "version": "==4.12.2" }, "wrapt": { "hashes": [ @@ -455,11 +439,11 @@ }, "certifi": { "hashes": [ - "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f", - "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1" + "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b", + "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90" ], "markers": "python_version >= '3.6'", - "version": "==2024.2.2" + "version": "==2024.7.4" }, "charset-normalizer": { "hashes": [ @@ -479,70 +463,69 @@ }, "colorama": { "hashes": [ - "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b", - "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2" + "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", + "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6" ], - "index": "pypi", - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==0.4.4" + "markers": "platform_system == 'Windows'", + "version": "==0.4.6" }, "coverage": { "hashes": [ - "sha256:015eddc5ccd5364dcb902eaecf9515636806fa1e0d5bef5769d06d0f31b54523", - "sha256:04aefca5190d1dc7a53a4c1a5a7f8568811306d7a8ee231c42fb69215571944f", - "sha256:05ac5f60faa0c704c0f7e6a5cbfd6f02101ed05e0aee4d2822637a9e672c998d", - "sha256:0bbddc54bbacfc09b3edaec644d4ac90c08ee8ed4844b0f86227dcda2d428fcb", - "sha256:1d2a830ade66d3563bb61d1e3c77c8def97b30ed91e166c67d0632c018f380f0", - "sha256:239a4e75e09c2b12ea478d28815acf83334d32e722e7433471fbf641c606344c", - "sha256:244f509f126dc71369393ce5fea17c0592c40ee44e607b6d855e9c4ac57aac98", - "sha256:25a5caf742c6195e08002d3b6c2dd6947e50efc5fc2c2205f61ecb47592d2d83", - "sha256:296a7d9bbc598e8744c00f7a6cecf1da9b30ae9ad51c566291ff1314e6cbbed8", - "sha256:2e079c9ec772fedbade9d7ebc36202a1d9ef7291bc9b3a024ca395c4d52853d7", - "sha256:33ca90a0eb29225f195e30684ba4a6db05dbef03c2ccd50b9077714c48153cac", - "sha256:33fc65740267222fc02975c061eb7167185fef4cc8f2770267ee8bf7d6a42f84", - "sha256:341dd8f61c26337c37988345ca5c8ccabeff33093a26953a1ac72e7d0103c4fb", - "sha256:34d6d21d8795a97b14d503dcaf74226ae51eb1f2bd41015d3ef332a24d0a17b3", - "sha256:3538d8fb1ee9bdd2e2692b3b18c22bb1c19ffbefd06880f5ac496e42d7bb3884", - "sha256:38a3b98dae8a7c9057bd91fbf3415c05e700a5114c5f1b5b0ea5f8f429ba6614", - "sha256:3d5a67f0da401e105753d474369ab034c7bae51a4c31c77d94030d59e41df5bd", - "sha256:50084d3516aa263791198913a17354bd1dc627d3c1639209640b9cac3fef5807", - "sha256:55f689f846661e3f26efa535071775d0483388a1ccfab899df72924805e9e7cd", - "sha256:5bc5a8c87714b0c67cfeb4c7caa82b2d71e8864d1a46aa990b5588fa953673b8", - "sha256:62bda40da1e68898186f274f832ef3e759ce929da9a9fd9fcf265956de269dbc", - "sha256:705f3d7c2b098c40f5b81790a5fedb274113373d4d1a69e65f8b68b0cc26f6db", - "sha256:75e3f4e86804023e991096b29e147e635f5e2568f77883a1e6eed74512659ab0", - "sha256:7b2a19e13dfb5c8e145c7a6ea959485ee8e2204699903c88c7d25283584bfc08", - "sha256:7cec2af81f9e7569280822be68bd57e51b86d42e59ea30d10ebdbb22d2cb7232", - "sha256:8383a6c8cefba1b7cecc0149415046b6fc38836295bc4c84e820872eb5478b3d", - "sha256:8c836309931839cca658a78a888dab9676b5c988d0dd34ca247f5f3e679f4e7a", - "sha256:8e317953bb4c074c06c798a11dbdd2cf9979dbcaa8ccc0fa4701d80042d4ebf1", - "sha256:923b7b1c717bd0f0f92d862d1ff51d9b2b55dbbd133e05680204465f454bb286", - "sha256:990fb20b32990b2ce2c5f974c3e738c9358b2735bc05075d50a6f36721b8f303", - "sha256:9aad68c3f2566dfae84bf46295a79e79d904e1c21ccfc66de88cd446f8686341", - "sha256:a5812840d1d00eafae6585aba38021f90a705a25b8216ec7f66aebe5b619fb84", - "sha256:a6519d917abb15e12380406d721e37613e2a67d166f9fb7e5a8ce0375744cd45", - "sha256:ab0b028165eea880af12f66086694768f2c3139b2c31ad5e032c8edbafca6ffc", - "sha256:aea7da970f1feccf48be7335f8b2ca64baf9b589d79e05b9397a06696ce1a1ec", - "sha256:b1196e13c45e327d6cd0b6e471530a1882f1017eb83c6229fc613cd1a11b53cd", - "sha256:b368e1aee1b9b75757942d44d7598dcd22a9dbb126affcbba82d15917f0cc155", - "sha256:bde997cac85fcac227b27d4fb2c7608a2c5f6558469b0eb704c5726ae49e1c52", - "sha256:c4c2872b3c91f9baa836147ca33650dc5c172e9273c808c3c3199c75490e709d", - "sha256:c59d2ad092dc0551d9f79d9d44d005c945ba95832a6798f98f9216ede3d5f485", - "sha256:d1da0a2e3b37b745a2b2a678a4c796462cf753aebf94edcc87dcc6b8641eae31", - "sha256:d8b7339180d00de83e930358223c617cc343dd08e1aa5ec7b06c3a121aec4e1d", - "sha256:dd4b3355b01273a56b20c219e74e7549e14370b31a4ffe42706a8cda91f19f6d", - "sha256:e08c470c2eb01977d221fd87495b44867a56d4d594f43739a8028f8646a51e0d", - "sha256:f5102a92855d518b0996eb197772f5ac2a527c0ec617124ad5242a3af5e25f85", - "sha256:f542287b1489c7a860d43a7d8883e27ca62ab84ca53c965d11dac1d3a1fab7ce", - "sha256:f78300789a708ac1f17e134593f577407d52d0417305435b134805c4fb135adb", - "sha256:f81bc26d609bf0fbc622c7122ba6307993c83c795d2d6f6f6fd8c000a770d974", - "sha256:f836c174c3a7f639bded48ec913f348c4761cbf49de4a20a956d3431a7c9cb24", - "sha256:fa21a04112c59ad54f69d80e376f7f9d0f5f9123ab87ecd18fbb9ec3a2beed56", - "sha256:fcf7d1d6f5da887ca04302db8e0e0cf56ce9a5e05f202720e49b3e8157ddb9a9", - "sha256:fd27d8b49e574e50caa65196d908f80e4dff64d7e592d0c59788b45aad7e8b35" + "sha256:0086cd4fc71b7d485ac93ca4239c8f75732c2ae3ba83f6be1c9be59d9e2c6382", + "sha256:01c322ef2bbe15057bc4bf132b525b7e3f7206f071799eb8aa6ad1940bcf5fb1", + "sha256:03cafe82c1b32b770a29fd6de923625ccac3185a54a5e66606da26d105f37dac", + "sha256:044a0985a4f25b335882b0966625270a8d9db3d3409ddc49a4eb00b0ef5e8cee", + "sha256:07ed352205574aad067482e53dd606926afebcb5590653121063fbf4e2175166", + "sha256:0d1b923fc4a40c5832be4f35a5dab0e5ff89cddf83bb4174499e02ea089daf57", + "sha256:0e7b27d04131c46e6894f23a4ae186a6a2207209a05df5b6ad4caee6d54a222c", + "sha256:1fad32ee9b27350687035cb5fdf9145bc9cf0a094a9577d43e909948ebcfa27b", + "sha256:289cc803fa1dc901f84701ac10c9ee873619320f2f9aff38794db4a4a0268d51", + "sha256:3c59105f8d58ce500f348c5b56163a4113a440dad6daa2294b5052a10db866da", + "sha256:46c3d091059ad0b9c59d1034de74a7f36dcfa7f6d3bde782c49deb42438f2450", + "sha256:482855914928c8175735a2a59c8dc5806cf7d8f032e4820d52e845d1f731dca2", + "sha256:49c76cdfa13015c4560702574bad67f0e15ca5a2872c6a125f6327ead2b731dd", + "sha256:4b03741e70fb811d1a9a1d75355cf391f274ed85847f4b78e35459899f57af4d", + "sha256:4bea27c4269234e06f621f3fac3925f56ff34bc14521484b8f66a580aacc2e7d", + "sha256:4d5fae0a22dc86259dee66f2cc6c1d3e490c4a1214d7daa2a93d07491c5c04b6", + "sha256:543ef9179bc55edfd895154a51792b01c017c87af0ebaae092720152e19e42ca", + "sha256:54dece71673b3187c86226c3ca793c5f891f9fc3d8aa183f2e3653da18566169", + "sha256:6379688fb4cfa921ae349c76eb1a9ab26b65f32b03d46bb0eed841fd4cb6afb1", + "sha256:65fa405b837060db569a61ec368b74688f429b32fa47a8929a7a2f9b47183713", + "sha256:6616d1c9bf1e3faea78711ee42a8b972367d82ceae233ec0ac61cc7fec09fa6b", + "sha256:6fe885135c8a479d3e37a7aae61cbd3a0fb2deccb4dda3c25f92a49189f766d6", + "sha256:7221f9ac9dad9492cecab6f676b3eaf9185141539d5c9689d13fd6b0d7de840c", + "sha256:76d5f82213aa78098b9b964ea89de4617e70e0d43e97900c2778a50856dac605", + "sha256:7792f0ab20df8071d669d929c75c97fecfa6bcab82c10ee4adb91c7a54055463", + "sha256:831b476d79408ab6ccfadaaf199906c833f02fdb32c9ab907b1d4aa0713cfa3b", + "sha256:9146579352d7b5f6412735d0f203bbd8d00113a680b66565e205bc605ef81bc6", + "sha256:9cc44bf0315268e253bf563f3560e6c004efe38f76db03a1558274a6e04bf5d5", + "sha256:a73d18625f6a8a1cbb11eadc1d03929f9510f4131879288e3f7922097a429f63", + "sha256:a8659fd33ee9e6ca03950cfdcdf271d645cf681609153f218826dd9805ab585c", + "sha256:a94925102c89247530ae1dab7dc02c690942566f22e189cbd53579b0693c0783", + "sha256:ad4567d6c334c46046d1c4c20024de2a1c3abc626817ae21ae3da600f5779b44", + "sha256:b2e16f4cd2bc4d88ba30ca2d3bbf2f21f00f382cf4e1ce3b1ddc96c634bc48ca", + "sha256:bbdf9a72403110a3bdae77948b8011f644571311c2fb35ee15f0f10a8fc082e8", + "sha256:beb08e8508e53a568811016e59f3234d29c2583f6b6e28572f0954a6b4f7e03d", + "sha256:c4cbe651f3904e28f3a55d6f371203049034b4ddbce65a54527a3f189ca3b390", + "sha256:c7b525ab52ce18c57ae232ba6f7010297a87ced82a2383b1afd238849c1ff933", + "sha256:ca5d79cfdae420a1d52bf177de4bc2289c321d6c961ae321503b2ca59c17ae67", + "sha256:cdab02a0a941af190df8782aafc591ef3ad08824f97850b015c8c6a8b3877b0b", + "sha256:d17c6a415d68cfe1091d3296ba5749d3d8696e42c37fca5d4860c5bf7b729f03", + "sha256:d39bd10f0ae453554798b125d2f39884290c480f56e8a02ba7a6ed552005243b", + "sha256:d4b3cd1ca7cd73d229487fa5caca9e4bc1f0bca96526b922d61053ea751fe791", + "sha256:d50a252b23b9b4dfeefc1f663c568a221092cbaded20a05a11665d0dbec9b8fb", + "sha256:da8549d17489cd52f85a9829d0e1d91059359b3c54a26f28bec2c5d369524807", + "sha256:dcd070b5b585b50e6617e8972f3fbbee786afca71b1936ac06257f7e178f00f6", + "sha256:ddaaa91bfc4477d2871442bbf30a125e8fe6b05da8a0015507bfbf4718228ab2", + "sha256:df423f351b162a702c053d5dddc0fc0ef9a9e27ea3f449781ace5f906b664428", + "sha256:dff044f661f59dace805eedb4a7404c573b6ff0cdba4a524141bc63d7be5c7fd", + "sha256:e7e128f85c0b419907d1f38e616c4f1e9f1d1b37a7949f44df9a73d5da5cd53c", + "sha256:ed8d1d1821ba5fc88d4a4f45387b65de52382fa3ef1f0115a4f7a20cdfab0e94", + "sha256:f2501d60d7497fd55e391f423f965bbe9e650e9ffc3c627d5f0ac516026000b8", + "sha256:f7db0b6ae1f96ae41afe626095149ecd1b212b424626175a6633c2999eaad45b" ], "markers": "python_version >= '3.8'", - "version": "==7.5.3" + "version": "==7.6.0" }, "flake8": { "hashes": [ @@ -643,11 +626,11 @@ }, "packaging": { "hashes": [ - "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5", - "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9" + "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002", + "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124" ], - "markers": "python_version >= '3.7'", - "version": "==24.0" + "markers": "python_version >= '3.8'", + "version": "==24.1" }, "pathspec": { "hashes": [ @@ -766,19 +749,19 @@ }, "typing-extensions": { "hashes": [ - "sha256:8cbcdc8606ebcb0d95453ad7dc5065e6237b6aa230a31e81d0f440c30fed5fd8", - "sha256:b349c66bea9016ac22978d800cfff206d5f9816951f12a7d0ec5578b0a819594" + "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", + "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8" ], "markers": "python_version >= '3.8'", - "version": "==4.12.0" + "version": "==4.12.2" }, "urllib3": { "hashes": [ - "sha256:34b97092d7e0a3a8cf7cd10e386f401b3737364026c45e622aa02903dffe0f07", - "sha256:f8ecc1bba5667413457c529ab955bf8c67b45db799d159066261719e328580a0" + "sha256:37a0344459b199fce0e80b0d3569837ec6b6937435c5244e7fd73fa6006830f3", + "sha256:3e3d753a8618b86d7de333b4223005f68720bcd6a7d2bcb9fbd2229ec7c1e429" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", - "version": "==1.26.18" + "version": "==1.26.19" } } } diff --git a/hardware-testing/hardware_testing/scripts/abr_asair_sensor.py b/hardware-testing/hardware_testing/scripts/abr_asair_sensor.py index 33195dacd5a..fc46fcd8df0 100644 --- a/hardware-testing/hardware_testing/scripts/abr_asair_sensor.py +++ b/hardware-testing/hardware_testing/scripts/abr_asair_sensor.py @@ -8,6 +8,7 @@ from typing import List import os import argparse +import pytz class _ABRAsairSensor: @@ -49,12 +50,14 @@ def __init__(self, robot: str, duration: int, frequency: int) -> None: "There are no google sheets credentials. Make sure credentials in jupyter notebook." ) results_list = [] # type: List - start_time = datetime.datetime.now() + timezone = pytz.timezone("America/New_York") + start_time = datetime.datetime.now(timezone) + # start_time = datetime.datetime.now(tz=tzinfo.utcoffset(timezone)) while True: env_data = sensor.get_reading() - timestamp = datetime.datetime.now() + timestamp = datetime.datetime.now(timezone) # Time adjustment for ABR robot timezone - new_timestamp = timestamp - datetime.timedelta(hours=5) + new_timestamp = timestamp date = new_timestamp.date() time = new_timestamp.time() temp = env_data.temperature @@ -72,7 +75,7 @@ def __init__(self, robot: str, duration: int, frequency: int) -> None: results_list.append(row) # Check if duration elapsed - elapsed_time = datetime.datetime.now() - start_time + elapsed_time = datetime.datetime.now(timezone) - start_time if elapsed_time.total_seconds() >= duration * 60: break # write to google sheet From c25c96d04f313c90f466c32a19209bccd4f3f42b Mon Sep 17 00:00:00 2001 From: Rhyann Clarke <146747548+rclarke0@users.noreply.github.com> Date: Wed, 17 Jul 2024 14:51:47 -0400 Subject: [PATCH 44/78] fix(hardware-testing): fix ABR asair sensor time adjustment (#15658) # Overview Change asair time adjustment from 5 hrs to 4 hrs to account for daylight savings. # Test Plan # Changelog # Review requests # Risk assessment From 08283f3afcce50d55126a4df812bb8f71a9d35b4 Mon Sep 17 00:00:00 2001 From: koji Date: Wed, 17 Jul 2024 15:43:27 -0400 Subject: [PATCH 45/78] feat(app): add a function to check unplug (#15684) * feat(app): add a function to check unplug --- api-client/src/protocols/getCsvFiles.ts | 8 ++------ .../organisms/ProtocolSetupParameters/ChooseCsvFile.tsx | 6 ++++++ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/api-client/src/protocols/getCsvFiles.ts b/api-client/src/protocols/getCsvFiles.ts index 8cb962f795a..ebfd7f19a74 100644 --- a/api-client/src/protocols/getCsvFiles.ts +++ b/api-client/src/protocols/getCsvFiles.ts @@ -1,5 +1,3 @@ -import { v4 as uuidv4 } from 'uuid' - // import { GET, request } from '../request' // import type { ResponsePromise } from '../request' @@ -25,18 +23,16 @@ export function getCsvFiles( config: HostConfig, protocolId: string ): Promise<{ data: UploadedCsvFilesResponse }> { - const fileIdOne = uuidv4() - const fileIdTwo = uuidv4() const stub = { data: { files: [ { - id: fileIdOne, + id: '1', createdAt: '2024-06-07T19:19:56.268029+00:00', name: 'rtp_mock_file1.csv', }, { - id: fileIdTwo, + id: '2', createdAt: '2024-06-17T19:19:56.268029+00:00', name: 'rtp_mock_file2.csv', }, diff --git a/app/src/organisms/ProtocolSetupParameters/ChooseCsvFile.tsx b/app/src/organisms/ProtocolSetupParameters/ChooseCsvFile.tsx index e45b2f1309f..f12516f38c4 100644 --- a/app/src/organisms/ProtocolSetupParameters/ChooseCsvFile.tsx +++ b/app/src/organisms/ProtocolSetupParameters/ChooseCsvFile.tsx @@ -62,6 +62,12 @@ export function ChooseCsvFile({ handleGoBack() } + React.useEffect(() => { + if (csvFilesOnUSB.length === 0) { + setCsvFileSelected({}) + } + }, [csvFilesOnUSB]) + return ( <> Date: Wed, 17 Jul 2024 16:48:07 -0400 Subject: [PATCH 46/78] feat(app, api-client, react-api-client): update POST interface for file parameters (#15682) Previously, file-type parameters were included in the `runTimeParameterValues` object used in posting to /protocols, /runs, and /analyses. Now, a robot server refactor creates an expectation by these endpoints for a separate `runTimeParameterFiles` object to be posted for protocols including a file-type parameter. `runTimeParameterValues` will be reserved for value-type parmeters. This PR introduces a new utility to retrieve these parameters and return them in their own object, and implements them in the required places across api-client and react-api-client. --- api-client/src/protocols/createProtocol.ts | 13 +++++-- .../src/protocols/createProtocolAnalysis.ts | 12 +++++-- api-client/src/runs/createRun.ts | 6 ++-- api-client/src/runs/types.ts | 7 ++-- .../ChooseProtocolSlideout/index.tsx | 27 +++++++++++---- .../index.tsx | 9 ++++- .../ProtocolRunRunTimeParameters.tsx | 3 +- app/src/organisms/Devices/utils.ts | 34 ++++++++++++++----- .../ProtocolSetupParameters/index.tsx | 10 +++++- .../hooks/__tests__/useCloneRun.test.tsx | 5 ++- .../ProtocolUpload/hooks/useCloneRun.ts | 21 ++++++++++-- .../useCreateProtocolAnalysisMutation.ts | 14 ++++++-- .../protocols/useCreateProtocolMutation.ts | 10 ++++-- 13 files changed, 133 insertions(+), 38 deletions(-) diff --git a/api-client/src/protocols/createProtocol.ts b/api-client/src/protocols/createProtocol.ts index 98712031246..965f7e9e962 100644 --- a/api-client/src/protocols/createProtocol.ts +++ b/api-client/src/protocols/createProtocol.ts @@ -2,14 +2,18 @@ import { POST, request } from '../request' import type { ResponsePromise } from '../request' import type { HostConfig } from '../types' import type { Protocol } from './types' -import type { RunTimeParameterCreateData } from '../runs' +import type { + RunTimeParameterValuesCreateData, + RunTimeParameterFilesCreateData, +} from '../runs' export function createProtocol( config: HostConfig, files: File[], protocolKey?: string, protocolKind?: string, - runTimeParameterValues?: RunTimeParameterCreateData + runTimeParameterValues?: RunTimeParameterValuesCreateData, + runTimeParameterFiles?: RunTimeParameterFilesCreateData ): ResponsePromise { const formData = new FormData() files.forEach(file => { @@ -22,6 +26,11 @@ export function createProtocol( 'runTimeParameterValues', JSON.stringify(runTimeParameterValues) ) + if (runTimeParameterFiles != null) + formData.append( + 'runTimeParameterFiles', + JSON.stringify(runTimeParameterFiles) + ) return request(POST, '/protocols', formData, config) } diff --git a/api-client/src/protocols/createProtocolAnalysis.ts b/api-client/src/protocols/createProtocolAnalysis.ts index 81ab83c11af..faf10907c85 100644 --- a/api-client/src/protocols/createProtocolAnalysis.ts +++ b/api-client/src/protocols/createProtocolAnalysis.ts @@ -3,21 +3,27 @@ import { POST, request } from '../request' import type { ProtocolAnalysisSummary } from '@opentrons/shared-data' import type { ResponsePromise } from '../request' import type { HostConfig } from '../types' -import type { RunTimeParameterCreateData } from '../runs' +import type { + RunTimeParameterFilesCreateData, + RunTimeParameterValuesCreateData, +} from '../runs' interface CreateProtocolAnalysisData { - runTimeParameterValues: RunTimeParameterCreateData + runTimeParameterValues: RunTimeParameterValuesCreateData + runTimeParameterFiles: RunTimeParameterFilesCreateData forceReAnalyze: boolean } export function createProtocolAnalysis( config: HostConfig, protocolKey: string, - runTimeParameterValues?: RunTimeParameterCreateData, + runTimeParameterValues?: RunTimeParameterValuesCreateData, + runTimeParameterFiles?: RunTimeParameterFilesCreateData, forceReAnalyze?: boolean ): ResponsePromise { const data = { runTimeParameterValues: runTimeParameterValues ?? {}, + runTimeParameterFiles: runTimeParameterFiles ?? {}, forceReAnalyze: forceReAnalyze ?? false, } const response = request< diff --git a/api-client/src/runs/createRun.ts b/api-client/src/runs/createRun.ts index 7f0fb1ad72d..6e8cd4b7525 100644 --- a/api-client/src/runs/createRun.ts +++ b/api-client/src/runs/createRun.ts @@ -5,13 +5,15 @@ import type { HostConfig } from '../types' import type { Run, LabwareOffsetCreateData, - RunTimeParameterCreateData, + RunTimeParameterValuesCreateData, + RunTimeParameterFilesCreateData, } from './types' export interface CreateRunData { protocolId?: string labwareOffsets?: LabwareOffsetCreateData[] - runTimeParameterValues?: RunTimeParameterCreateData + runTimeParameterValues?: RunTimeParameterValuesCreateData + runTimeParameterFiles?: RunTimeParameterFilesCreateData } export function createRun( diff --git a/api-client/src/runs/types.ts b/api-client/src/runs/types.ts index b2416d7a31a..45e40f2f8b9 100644 --- a/api-client/src/runs/types.ts +++ b/api-client/src/runs/types.ts @@ -132,11 +132,12 @@ export interface LabwareOffsetCreateData { vector: VectorOffset } -type RunTimeParameterValueType = string | number | boolean | { id: string } -export type RunTimeParameterCreateData = Record< +type RunTimeParameterValuesType = string | number | boolean | { id: string } +export type RunTimeParameterValuesCreateData = Record< string, - RunTimeParameterValueType + RunTimeParameterValuesType > +export type RunTimeParameterFilesCreateData = Record export interface CommandData { data: RunTimeCommand diff --git a/app/src/organisms/ChooseProtocolSlideout/index.tsx b/app/src/organisms/ChooseProtocolSlideout/index.tsx index 41e10b092ed..5374bd2e7ad 100644 --- a/app/src/organisms/ChooseProtocolSlideout/index.tsx +++ b/app/src/organisms/ChooseProtocolSlideout/index.tsx @@ -52,7 +52,10 @@ import { useCreateRunFromProtocol } from '../ChooseRobotToRunProtocolSlideout/us import { ApplyHistoricOffsets } from '../ApplyHistoricOffsets' import { useOffsetCandidatesForAnalysis } from '../ApplyHistoricOffsets/hooks/useOffsetCandidatesForAnalysis' import { FileCard } from '../ChooseRobotSlideout/FileCard' -import { getRunTimeParameterValuesForRun } from '../Devices/utils' +import { + getRunTimeParameterFilesForRun, + getRunTimeParameterValuesForRun, +} from '../Devices/utils' import { getAnalysisStatus } from '../ProtocolsLanding/utils' import type { DropdownOption } from '@opentrons/components' @@ -230,14 +233,26 @@ export function ChooseProtocolSlideoutComponent( return { ...acc, [variableName]: uploadedFileResponse.data.id } }, {}) const runTimeParameterValues = getRunTimeParameterValuesForRun( + runTimeParametersOverrides + ) + const runTimeParameterFiles = getRunTimeParameterFilesForRun( runTimeParametersOverrides, mappedResolvedCsvVariableToFileId ) - createRunFromProtocolSource({ - files: srcFileObjects, - protocolKey: selectedProtocol.protocolKey, - runTimeParameterValues, - }) + if (enableCsvFile) { + createRunFromProtocolSource({ + files: srcFileObjects, + protocolKey: selectedProtocol.protocolKey, + runTimeParameterValues, + runTimeParameterFiles, + }) + } else { + createRunFromProtocolSource({ + files: srcFileObjects, + protocolKey: selectedProtocol.protocolKey, + runTimeParameterValues, + }) + } }) } else { logger.warn('failed to create protocol, no protocol selected') diff --git a/app/src/organisms/ChooseRobotToRunProtocolSlideout/index.tsx b/app/src/organisms/ChooseRobotToRunProtocolSlideout/index.tsx index 08a809e9306..e1a77129761 100644 --- a/app/src/organisms/ChooseRobotToRunProtocolSlideout/index.tsx +++ b/app/src/organisms/ChooseRobotToRunProtocolSlideout/index.tsx @@ -22,7 +22,10 @@ import { useFeatureFlag } from '../../redux/config' import { OPENTRONS_USB } from '../../redux/discovery' import { appShellRequestor } from '../../redux/shell/remote' import { useTrackCreateProtocolRunEvent } from '../Devices/hooks' -import { getRunTimeParameterValuesForRun } from '../Devices/utils' +import { + getRunTimeParameterFilesForRun, + getRunTimeParameterValuesForRun, +} from '../Devices/utils' import { ApplyHistoricOffsets } from '../ApplyHistoricOffsets' import { useOffsetCandidatesForAnalysis } from '../ApplyHistoricOffsets/hooks/useOffsetCandidatesForAnalysis' import { ChooseRobotSlideout } from '../ChooseRobotSlideout' @@ -161,6 +164,9 @@ export function ChooseRobotToRunProtocolSlideoutComponent( return { ...acc, [variableName]: uploadedFileResponse.data.id } }, {}) const runTimeParameterValues = getRunTimeParameterValuesForRun( + runTimeParametersOverrides + ) + const runTimeParameterFiles = getRunTimeParameterFilesForRun( runTimeParametersOverrides, mappedResolvedCsvVariableToFileId ) @@ -168,6 +174,7 @@ export function ChooseRobotToRunProtocolSlideoutComponent( files: srcFileObjects, protocolKey, runTimeParameterValues, + runTimeParameterFiles, }) }) } else { diff --git a/app/src/organisms/Devices/ProtocolRun/ProtocolRunRunTimeParameters.tsx b/app/src/organisms/Devices/ProtocolRun/ProtocolRunRunTimeParameters.tsx index a882e90745b..ef3740625fc 100644 --- a/app/src/organisms/Devices/ProtocolRun/ProtocolRunRunTimeParameters.tsx +++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunRunTimeParameters.tsx @@ -209,7 +209,8 @@ const StyledTableRowComponent = ( {parameter.type === 'csv_file' - ? parameter.file?.file?.name ?? '' + ? // TODO (nd, 07/17/2024): retrieve filename from parameter once backend is wired up + parameter.file?.file?.name ?? '' : formatRunTimeParameterValue(parameter, t)} {parameter.type === 'csv_file' || diff --git a/app/src/organisms/Devices/utils.ts b/app/src/organisms/Devices/utils.ts index 01b6c704d89..c5302e62208 100644 --- a/app/src/organisms/Devices/utils.ts +++ b/app/src/organisms/Devices/utils.ts @@ -9,7 +9,8 @@ import type { Instruments, PipetteData, PipetteOffsetCalibration, - RunTimeParameterCreateData, + RunTimeParameterFilesCreateData, + RunTimeParameterValuesCreateData, } from '@opentrons/api-client' import type { RunTimeParameter } from '@opentrons/shared-data' @@ -93,27 +94,42 @@ export function getShowPipetteCalibrationWarning( } /** - * prepares object to send to endpoints requiring RunTimeParameterCreateData + * prepares object to send to endpoints requiring RunTimeParameterValuesCreateData * @param {RunTimeParameter[]} runTimeParameters array of updated RunTimeParameter overrides - * @param {Record} [fileIdMap] mapping of variable name to file ID created and returned by robot server - * @returns {RunTimeParameterCreateData} object mapping variable name to value or file information + * @returns {RunTimeParameterValuesCreateData} object mapping variable name to value */ export function getRunTimeParameterValuesForRun( + runTimeParameters: RunTimeParameter[] +): RunTimeParameterValuesCreateData { + return runTimeParameters.reduce((acc, param) => { + const { variableName } = param + if (param.type !== 'csv_file' && param.value !== param.default) { + return { ...acc, [variableName]: param.value } + } + return acc + }, {}) +} + +/** + * prepares object to send to endpoints requiring RunTimeParameterFilesCreateData + * @param {RunTimeParameter[]} runTimeParameters array of updated RunTimeParameter overrides + * @param {Record} [fileIdMap] mapping of variable name to file ID created and returned by robot server + * @returns {RunTimeParameterFilesCreateData} object mapping variable name to file ID + */ +export function getRunTimeParameterFilesForRun( runTimeParameters: RunTimeParameter[], fileIdMap?: Record -): RunTimeParameterCreateData { +): RunTimeParameterFilesCreateData { return runTimeParameters.reduce((acc, param) => { const { variableName } = param if (param.type === 'csv_file' && param.file?.id != null) { - return { ...acc, [variableName]: { id: param.file.id } } + return { ...acc, [variableName]: param.file.id } } else if ( param.type === 'csv_file' && fileIdMap != null && variableName in fileIdMap ) { - return { ...acc, [variableName]: { id: fileIdMap[variableName] } } - } else if (param.type !== 'csv_file' && param.value !== param.default) { - return { ...acc, [variableName]: param.value } + return { ...acc, [variableName]: fileIdMap[variableName] } } return acc }, {}) diff --git a/app/src/organisms/ProtocolSetupParameters/index.tsx b/app/src/organisms/ProtocolSetupParameters/index.tsx index f62946dff13..7bd033f4588 100644 --- a/app/src/organisms/ProtocolSetupParameters/index.tsx +++ b/app/src/organisms/ProtocolSetupParameters/index.tsx @@ -19,7 +19,10 @@ import { sortRuntimeParameters, } from '@opentrons/shared-data' -import { getRunTimeParameterValuesForRun } from '../Devices/utils' +import { + getRunTimeParameterFilesForRun, + getRunTimeParameterValuesForRun, +} from '../Devices/utils' import { ChildNavigation } from '../ChildNavigation' import { ResetValuesModal } from './ResetValuesModal' import { ChooseEnum } from './ChooseEnum' @@ -194,17 +197,22 @@ export function ProtocolSetupParameters({ return { ...acc, [variableName]: uploadedFileResponse.data.id } }, {}) const runTimeParameterValues = getRunTimeParameterValuesForRun( + runTimeParametersOverrides + ) + const runTimeParameterFiles = getRunTimeParameterFilesForRun( runTimeParametersOverrides, mappedResolvedCsvVariableToFileId ) createProtocolAnalysis({ protocolKey: protocolId, runTimeParameterValues, + runTimeParameterFiles, }) createRun({ protocolId, labwareOffsets, runTimeParameterValues, + runTimeParameterFiles, }) }) } diff --git a/app/src/organisms/ProtocolUpload/hooks/__tests__/useCloneRun.test.tsx b/app/src/organisms/ProtocolUpload/hooks/__tests__/useCloneRun.test.tsx index 6feee78ec40..3056c99211e 100644 --- a/app/src/organisms/ProtocolUpload/hooks/__tests__/useCloneRun.test.tsx +++ b/app/src/organisms/ProtocolUpload/hooks/__tests__/useCloneRun.test.tsx @@ -113,6 +113,7 @@ describe('useCloneRun hook', () => { protocolId: 'protocolId', labwareOffsets: 'someOffset', runTimeParameterValues: {}, + runTimeParameterFiles: {}, }) }) it('should return a function that when called, calls createRun run with runTimeParameterValues overrides', async () => { @@ -129,7 +130,9 @@ describe('useCloneRun hook', () => { runTimeParameterValues: { number_param: 2, boolean_param: false, - file_param: { id: 'fileId_123' }, + }, + runTimeParameterFiles: { + file_param: 'fileId_123', }, }) }) diff --git a/app/src/organisms/ProtocolUpload/hooks/useCloneRun.ts b/app/src/organisms/ProtocolUpload/hooks/useCloneRun.ts index 08a4891aa1e..dc3b2e7902b 100644 --- a/app/src/organisms/ProtocolUpload/hooks/useCloneRun.ts +++ b/app/src/organisms/ProtocolUpload/hooks/useCloneRun.ts @@ -6,7 +6,10 @@ import { useCreateProtocolAnalysisMutation, } from '@opentrons/react-api-client' import { useNotifyRunQuery } from '../../../resources/runs' -import { getRunTimeParameterValuesForRun } from '../../Devices/utils' +import { + getRunTimeParameterValuesForRun, + getRunTimeParameterFilesForRun, +} from '../../Devices/utils' import type { Run } from '@opentrons/api-client' @@ -49,10 +52,22 @@ export function useCloneRun( const runTimeParameterValues = getRunTimeParameterValuesForRun( runTimeParameters ) + const runTimeParameterFiles = getRunTimeParameterFilesForRun( + runTimeParameters + ) if (triggerAnalysis && protocolKey != null) { - createProtocolAnalysis({ protocolKey, runTimeParameterValues }) + createProtocolAnalysis({ + protocolKey, + runTimeParameterValues, + runTimeParameterFiles, + }) } - createRun({ protocolId, labwareOffsets, runTimeParameterValues }) + createRun({ + protocolId, + labwareOffsets, + runTimeParameterValues, + runTimeParameterFiles, + }) } else { console.info('failed to clone run record, source run record not found') } diff --git a/react-api-client/src/protocols/useCreateProtocolAnalysisMutation.ts b/react-api-client/src/protocols/useCreateProtocolAnalysisMutation.ts index f8ba6e10586..f2acace20ce 100644 --- a/react-api-client/src/protocols/useCreateProtocolAnalysisMutation.ts +++ b/react-api-client/src/protocols/useCreateProtocolAnalysisMutation.ts @@ -4,7 +4,8 @@ import { useHost } from '../api' import type { ErrorResponse, HostConfig, - RunTimeParameterCreateData, + RunTimeParameterFilesCreateData, + RunTimeParameterValuesCreateData, } from '@opentrons/api-client' import type { ProtocolAnalysisSummary } from '@opentrons/shared-data' import type { AxiosError } from 'axios' @@ -16,7 +17,8 @@ import type { export interface CreateProtocolAnalysisVariables { protocolKey: string - runTimeParameterValues?: RunTimeParameterCreateData + runTimeParameterValues?: RunTimeParameterValuesCreateData + runTimeParameterFiles?: RunTimeParameterFilesCreateData forceReAnalyze?: boolean } export type UseCreateProtocolMutationResult = UseMutationResult< @@ -53,11 +55,17 @@ export function useCreateProtocolAnalysisMutation( CreateProtocolAnalysisVariables >( [host, 'protocols', protocolId, 'analyses'], - ({ protocolKey, runTimeParameterValues, forceReAnalyze }) => + ({ + protocolKey, + runTimeParameterValues, + runTimeParameterFiles, + forceReAnalyze, + }) => createProtocolAnalysis( host as HostConfig, protocolKey, runTimeParameterValues, + runTimeParameterFiles, forceReAnalyze ) .then(response => { diff --git a/react-api-client/src/protocols/useCreateProtocolMutation.ts b/react-api-client/src/protocols/useCreateProtocolMutation.ts index 49c034e071e..5bc4e974b63 100644 --- a/react-api-client/src/protocols/useCreateProtocolMutation.ts +++ b/react-api-client/src/protocols/useCreateProtocolMutation.ts @@ -11,14 +11,16 @@ import type { ErrorResponse, HostConfig, Protocol, - RunTimeParameterCreateData, + RunTimeParameterFilesCreateData, + RunTimeParameterValuesCreateData, } from '@opentrons/api-client' export interface CreateProtocolVariables { files: File[] protocolKey?: string protocolKind?: string - runTimeParameterValues?: RunTimeParameterCreateData + runTimeParameterValues?: RunTimeParameterValuesCreateData + runTimeParameterFiles?: RunTimeParameterFilesCreateData } export type UseCreateProtocolMutationResult = UseMutationResult< Protocol, @@ -58,13 +60,15 @@ export function useCreateProtocolMutation( protocolKey, protocolKind = 'standard', runTimeParameterValues, + runTimeParameterFiles, }) => createProtocol( host as HostConfig, protocolFiles, protocolKey, protocolKind, - runTimeParameterValues + runTimeParameterValues, + runTimeParameterFiles ) .then(response => { const protocolId = response.data.data.id From aa9d2f23308d533485816028c5804c1e521bdaf1 Mon Sep 17 00:00:00 2001 From: Brent Hagen Date: Wed, 17 Jul 2024 17:00:17 -0400 Subject: [PATCH 47/78] feat(app): anonymize pipette names for pipetteSpecsV2 (#15670) creates a wrapper hook usePipetteSpecsV2 that anonymizes pipette specs v2 display names when ODD is in OEM mode closes PLAT-296 --- .../QuickTransferFlow/SelectPipette.tsx | 11 ++++------- .../__tests__/SelectPipette.test.tsx | 4 ++++ app/src/resources/instruments/hooks.ts | 18 ++++++++++++++++++ shared-data/js/pipettes.ts | 2 +- 4 files changed, 27 insertions(+), 8 deletions(-) diff --git a/app/src/organisms/QuickTransferFlow/SelectPipette.tsx b/app/src/organisms/QuickTransferFlow/SelectPipette.tsx index bafce8ced26..f0dbb87f35b 100644 --- a/app/src/organisms/QuickTransferFlow/SelectPipette.tsx +++ b/app/src/organisms/QuickTransferFlow/SelectPipette.tsx @@ -8,8 +8,9 @@ import { DIRECTION_COLUMN, } from '@opentrons/components' import { useInstrumentsQuery } from '@opentrons/react-api-client' -import { getPipetteSpecsV2, RIGHT, LEFT } from '@opentrons/shared-data' +import { RIGHT, LEFT } from '@opentrons/shared-data' import { RadioButton } from '../../atoms/buttons' +import { usePipetteSpecsV2 } from '../../resources/instruments/hooks' import { ChildNavigation } from '../ChildNavigation' import type { PipetteData, Mount } from '@opentrons/api-client' @@ -35,16 +36,12 @@ export function SelectPipette(props: SelectPipetteProps): JSX.Element { const leftPipette = attachedInstruments?.data.find( (i): i is PipetteData => i.ok && i.mount === LEFT ) - const leftPipetteSpecs = - leftPipette != null ? getPipetteSpecsV2(leftPipette.instrumentModel) : null + const leftPipetteSpecs = usePipetteSpecsV2(leftPipette?.instrumentModel) const rightPipette = attachedInstruments?.data.find( (i): i is PipetteData => i.ok && i.mount === RIGHT ) - const rightPipetteSpecs = - rightPipette != null - ? getPipetteSpecsV2(rightPipette.instrumentModel) - : null + const rightPipetteSpecs = usePipetteSpecsV2(rightPipette?.instrumentModel) // automatically select 96 channel if it is attached const [selectedPipette, setSelectedPipette] = React.useState< diff --git a/app/src/organisms/QuickTransferFlow/__tests__/SelectPipette.test.tsx b/app/src/organisms/QuickTransferFlow/__tests__/SelectPipette.test.tsx index 2d6faa6ffa7..0f40c6f29b0 100644 --- a/app/src/organisms/QuickTransferFlow/__tests__/SelectPipette.test.tsx +++ b/app/src/organisms/QuickTransferFlow/__tests__/SelectPipette.test.tsx @@ -5,9 +5,12 @@ import { useInstrumentsQuery } from '@opentrons/react-api-client' import { renderWithProviders } from '../../../__testing-utils__' import { i18n } from '../../../i18n' +import { useIsOEMMode } from '../../../resources/robot-settings/hooks' import { SelectPipette } from '../SelectPipette' vi.mock('@opentrons/react-api-client') +vi.mock('../../../resources/robot-settings/hooks') + const render = (props: React.ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, @@ -53,6 +56,7 @@ describe('SelectPipette', () => { ], }, } as any) + vi.mocked(useIsOEMMode).mockReturnValue(false) }) afterEach(() => { vi.resetAllMocks() diff --git a/app/src/resources/instruments/hooks.ts b/app/src/resources/instruments/hooks.ts index 31a40f5fdd0..32135d716a4 100644 --- a/app/src/resources/instruments/hooks.ts +++ b/app/src/resources/instruments/hooks.ts @@ -2,6 +2,7 @@ import { getGripperDisplayName, getPipetteModelSpecs, getPipetteNameSpecs, + getPipetteSpecsV2, GRIPPER_MODELS, } from '@opentrons/shared-data' import { useIsOEMMode } from '../robot-settings/hooks' @@ -12,6 +13,7 @@ import type { PipetteModelSpecs, PipetteName, PipetteNameSpecs, + PipetteV2Specs, } from '@opentrons/shared-data' export function usePipetteNameSpecs( @@ -46,6 +48,22 @@ export function usePipetteModelSpecs( return { ...modelSpecificFields, displayName: pipetteNameSpecs.displayName } } +export function usePipetteSpecsV2( + name?: PipetteName | PipetteModel +): PipetteV2Specs | null { + const isOEMMode = useIsOEMMode() + const pipetteSpecs = getPipetteSpecsV2(name) + + if (pipetteSpecs == null) return null + + const brandedDisplayName = pipetteSpecs.displayName + const anonymizedDisplayName = pipetteSpecs.displayName.replace('Flex ', '') + + const displayName = isOEMMode ? anonymizedDisplayName : brandedDisplayName + + return { ...pipetteSpecs, displayName } +} + export function useGripperDisplayName(gripperModel: GripperModel): string { const isOEMMode = useIsOEMMode() diff --git a/shared-data/js/pipettes.ts b/shared-data/js/pipettes.ts index 0901d10ae42..2921696b820 100644 --- a/shared-data/js/pipettes.ts +++ b/shared-data/js/pipettes.ts @@ -191,7 +191,7 @@ or PipetteModel such as 'p300_single_v1.3' and converts it to channels, model, and version in order to return the correct pipette schema v2 json files. **/ export const getPipetteSpecsV2 = ( - name: PipetteName | PipetteModel + name?: PipetteName | PipetteModel ): PipetteV2Specs | null => { if (name == null) { return null From e721bbbfb230d9722e577c81955a8341a21ae545 Mon Sep 17 00:00:00 2001 From: pmoegenburg Date: Wed, 17 Jul 2024 17:02:05 -0400 Subject: [PATCH 48/78] fix(api): modify ZeroLengthMoveError logging from warning to debug level (#15697) # Overview ZeroLengthMoveErrors are now superfluous, and we see lots of them in api logs. This gets rid of them in default api logs by downgrading their logging level from warning to debug. # Test Plan # Changelog # Review requests # Risk assessment --- api/src/opentrons/hardware_control/backends/ot3controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/opentrons/hardware_control/backends/ot3controller.py b/api/src/opentrons/hardware_control/backends/ot3controller.py index 5bdb1621066..e23cbcdd8c3 100644 --- a/api/src/opentrons/hardware_control/backends/ot3controller.py +++ b/api/src/opentrons/hardware_control/backends/ot3controller.py @@ -643,7 +643,7 @@ async def move( origin=origin, target_list=[move_target] ) except ZeroLengthMoveError as zme: - log.warning(f"Not moving because move was zero length {str(zme)}") + log.debug(f"Not moving because move was zero length {str(zme)}") return moves = movelist[0] log.info(f"move: machine {target} from {origin} requires {moves}") From dd535088324892d7bb4c66c8bcb0b7724f2bce4d Mon Sep 17 00:00:00 2001 From: aaron-kulkarni <107003644+aaron-kulkarni@users.noreply.github.com> Date: Thu, 18 Jul 2024 08:58:27 -0400 Subject: [PATCH 49/78] refactor(shared-data): remove outdated id fields from JSON v7-v8 fixtures (#15694) replace all instances of Command `id` with `key` in the v7 protocol fixtures(v8 doesn't have any instances of `id`) continuation of #15690 # Overview # Test Plan # Changelog # Review requests # Risk assessment --- shared-data/protocol/fixtures/7/simpleV7.json | 52 +++++++++---------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/shared-data/protocol/fixtures/7/simpleV7.json b/shared-data/protocol/fixtures/7/simpleV7.json index 5ba560f83ca..aa0b78f9915 100644 --- a/shared-data/protocol/fixtures/7/simpleV7.json +++ b/shared-data/protocol/fixtures/7/simpleV7.json @@ -1208,7 +1208,7 @@ "commands": [ { "commandType": "loadPipette", - "id": "0abc123", + "key": "0abc123", "params": { "pipetteId": "pipetteId", "pipetteName": "p1000_96", @@ -1217,7 +1217,7 @@ }, { "commandType": "loadModule", - "id": "1abc123", + "key": "1abc123", "params": { "moduleId": "magneticModuleId", "model": "magneticModuleV2", @@ -1226,7 +1226,7 @@ }, { "commandType": "loadModule", - "id": "2abc123", + "key": "2abc123", "params": { "moduleId": "temperatureModuleId", "model": "temperatureModuleV2", @@ -1235,7 +1235,7 @@ }, { "commandType": "loadLabware", - "id": "3abc123", + "key": "3abc123", "params": { "labwareId": "sourcePlateId", "loadName": "armadillo_96_wellplate_200ul_pcr_full_skirt", @@ -1249,7 +1249,7 @@ }, { "commandType": "loadLabware", - "id": "4abc123", + "key": "4abc123", "params": { "labwareId": "destPlateId", "loadName": "armadillo_96_wellplate_200ul_pcr_full_skirt", @@ -1263,7 +1263,7 @@ }, { "commandType": "loadLabware", - "id": "5abc123", + "key": "5abc123", "params": { "labwareId": "tipRackId", "location": { "slotName": "8" }, @@ -1275,7 +1275,7 @@ }, { "commandType": "loadLabware", - "id": "6abc123", + "key": "6abc123", "params": { "labwareId": "fixedTrash", "location": { @@ -1289,7 +1289,7 @@ }, { "commandType": "loadLiquid", - "id": "7abc123", + "key": "7abc123", "params": { "liquidId": "waterId", "labwareId": "sourcePlateId", @@ -1301,12 +1301,12 @@ }, { "commandType": "home", - "id": "00abc123", + "key": "00abc123", "params": {} }, { "commandType": "pickUpTip", - "id": "8abc123", + "key": "8abc123", "params": { "pipetteId": "pipetteId", "labwareId": "tipRackId", @@ -1315,7 +1315,7 @@ }, { "commandType": "aspirate", - "id": "9abc123", + "key": "9abc123", "params": { "pipetteId": "pipetteId", "labwareId": "sourcePlateId", @@ -1330,14 +1330,14 @@ }, { "commandType": "waitForDuration", - "id": "10abc123", + "key": "10abc123", "params": { "seconds": 42 } }, { "commandType": "dispense", - "id": "11abc123", + "key": "11abc123", "params": { "pipetteId": "pipetteId", "labwareId": "destPlateId", @@ -1352,7 +1352,7 @@ }, { "commandType": "touchTip", - "id": "12abc123", + "key": "12abc123", "params": { "pipetteId": "pipetteId", "labwareId": "destPlateId", @@ -1365,7 +1365,7 @@ }, { "commandType": "blowout", - "id": "13abc123", + "key": "13abc123", "params": { "pipetteId": "pipetteId", "labwareId": "destPlateId", @@ -1379,7 +1379,7 @@ }, { "commandType": "moveToCoordinates", - "id": "14abc123", + "key": "14abc123", "params": { "pipetteId": "pipetteId", "coordinates": { "x": 100, "y": 100, "z": 100 } @@ -1387,7 +1387,7 @@ }, { "commandType": "moveToWell", - "id": "15abc123", + "key": "15abc123", "params": { "pipetteId": "pipetteId", "labwareId": "destPlateId", @@ -1396,7 +1396,7 @@ }, { "commandType": "moveToWell", - "id": "16abc123", + "key": "16abc123", "params": { "pipetteId": "pipetteId", "labwareId": "destPlateId", @@ -1411,7 +1411,7 @@ }, { "commandType": "dropTip", - "id": "17abc123", + "key": "17abc123", "params": { "pipetteId": "pipetteId", "labwareId": "fixedTrash", @@ -1420,7 +1420,7 @@ }, { "commandType": "waitForResume", - "id": "18abc123", + "key": "18abc123", "params": { "message": "pause command" } @@ -1428,7 +1428,7 @@ { "commandType": "moveToCoordinates", - "id": "16abc123", + "key": "16abc123", "params": { "pipetteId": "pipetteId", "coordinates": { "x": 0, "y": 0, "z": 0 }, @@ -1438,7 +1438,7 @@ }, { "commandType": "moveRelative", - "id": "18abc123", + "key": "18abc123", "params": { "pipetteId": "pipetteId", "axis": "x", @@ -1447,7 +1447,7 @@ }, { "commandType": "moveRelative", - "id": "19abc123", + "key": "19abc123", "params": { "pipetteId": "pipetteId", "axis": "y", @@ -1456,14 +1456,14 @@ }, { "commandType": "savePosition", - "id": "21abc123", + "key": "21abc123", "params": { "pipetteId": "pipetteId" } }, { "commandType": "moveRelative", - "id": "20abc123", + "key": "20abc123", "params": { "pipetteId": "pipetteId", "axis": "z", @@ -1472,7 +1472,7 @@ }, { "commandType": "savePosition", - "id": "21abc123", + "key": "21abc123", "params": { "pipetteId": "pipetteId", "positionId": "positionId" From 5b09f720c27ab909032f524c9c5e7b7f17ee8060 Mon Sep 17 00:00:00 2001 From: Laura Cox <31892318+Laura-Danielle@users.noreply.github.com> Date: Thu, 18 Jul 2024 17:57:30 +0300 Subject: [PATCH 50/78] feat(api): Update robot deck extents to check the full pipette bounds (#15532) # Overview Previously, we were only checking for a very specific set of bounds for the robot. Now, we should be able to check the exact "absolute" value that a pipette type can move to and throw and error if we exceed those bounds. # Test Plan - Put a bunch of protocols through an app and see whether the software correctly determines if something is out of bounds # Changelog - Added two new components to the deck definition for 'extents' and 'mount offsets' - Added accessors for that in the protocol engine - Modified `_is_within_pipette_extents` function to check the absolute bound per pipette type per mount without any offsets # Review requests Pls check my math, I'm almost sure I did one or two things wrong # Risk assessment Medium. If we don't get this right it could throw erroneous deck bound errors. --- .../protocol_api/core/engine/deck_conflict.py | 81 +++++++------------ .../protocol_engine/create_protocol_engine.py | 4 +- .../state/addressable_areas.py | 24 +++++- .../protocol_engine/state/geometry.py | 26 ++++++ .../protocol_engine/state/pipettes.py | 17 ++++ .../opentrons/protocol_engine/state/state.py | 4 + .../core/engine/test_deck_conflict.py | 73 +++++++++++++++-- .../test_pipette_movement_deck_conflicts.py | 39 ++++++--- .../state/test_addressable_area_state.py | 11 +++ .../state/test_addressable_area_store.py | 33 ++++++++ .../state/test_addressable_area_view.py | 11 +++ .../state/test_geometry_view.py | 17 +++- .../state/test_module_store.py | 11 +++ .../protocol_engine/state/test_module_view.py | 11 +++ .../state/test_pipette_store.py | 2 + .../state/test_pipette_view.py | 17 +++- .../protocol_engine/state/test_state_store.py | 7 ++ .../deck/definitions/4/ot2_standard.json | 7 +- .../opentrons_shared_data/robot/dev_types.py | 12 ++- shared-data/robot/definitions/1/ot2.json | 7 +- shared-data/robot/definitions/1/ot3.json | 8 +- shared-data/robot/schemas/1.json | 32 +++++++- 22 files changed, 377 insertions(+), 77 deletions(-) diff --git a/api/src/opentrons/protocol_api/core/engine/deck_conflict.py b/api/src/opentrons/protocol_api/core/engine/deck_conflict.py index 2a50964e757..405aa2256a7 100644 --- a/api/src/opentrons/protocol_api/core/engine/deck_conflict.py +++ b/api/src/opentrons/protocol_api/core/engine/deck_conflict.py @@ -16,7 +16,6 @@ from opentrons_shared_data.errors.exceptions import MotionPlanningFailureError from opentrons_shared_data.module import FLEX_TC_LID_COLLISION_ZONE -from opentrons.hardware_control.nozzle_manager import NozzleConfigurationType from opentrons.hardware_control.modules.types import ModuleType from opentrons.motion_planning import deck_conflict as wrapped_deck_conflict from opentrons.motion_planning import adjacent_slots_getters @@ -63,21 +62,6 @@ def __init__(self, message: str) -> None: _log = logging.getLogger(__name__) -# TODO (spp, 2023-12-06): move this to a location like motion planning where we can -# derive these values from geometry definitions -# Also, verify y-axis extents values for the nozzle columns. -# Bounding box measurements -A12_column_front_left_bound = Point(x=-11.03, y=2) -A12_column_back_right_bound = Point(x=526.77, y=506.2) - -_NOZZLE_PITCH = 9 -A1_column_front_left_bound = Point( - x=A12_column_front_left_bound.x - _NOZZLE_PITCH * 11, y=2 -) -A1_column_back_right_bound = Point( - x=A12_column_back_right_bound.x - _NOZZLE_PITCH * 11, y=506.2 -) - _FLEX_TC_LID_BACK_LEFT_PT = Point( x=FLEX_TC_LID_COLLISION_ZONE["back_left"]["x"], y=FLEX_TC_LID_COLLISION_ZONE["back_left"]["y"], @@ -244,8 +228,15 @@ def check_safe_for_pipette_movement( ) primary_nozzle = engine_state.pipettes.get_primary_nozzle(pipette_id) + pipette_bounds_at_well_location = ( + engine_state.pipettes.get_pipette_bounds_at_specified_move_to_position( + pipette_id=pipette_id, destination_position=well_location_point + ) + ) if not _is_within_pipette_extents( - engine_state=engine_state, pipette_id=pipette_id, location=well_location_point + engine_state=engine_state, + pipette_id=pipette_id, + pipette_bounding_box_at_loc=pipette_bounds_at_well_location, ): raise PartialTipMovementNotAllowedError( f"Requested motion with the {primary_nozzle} nozzle partial configuration" @@ -253,11 +244,7 @@ def check_safe_for_pipette_movement( ) labware_slot = engine_state.geometry.get_ancestor_slot_name(labware_id) - pipette_bounds_at_well_location = ( - engine_state.pipettes.get_pipette_bounds_at_specified_move_to_position( - pipette_id=pipette_id, destination_position=well_location_point - ) - ) + surrounding_slots = adjacent_slots_getters.get_surrounding_slots( slot=labware_slot.as_int(), robot_type=engine_state.config.robot_type ) @@ -423,42 +410,30 @@ def check_safe_for_tip_pickup_and_return( ) -# TODO (spp, 2023-02-06): update the extents check to use all nozzle bounds instead of -# just position of primary nozzle when checking if the pipette is out-of-bounds def _is_within_pipette_extents( engine_state: StateView, pipette_id: str, - location: Point, + pipette_bounding_box_at_loc: Tuple[Point, Point, Point, Point], ) -> bool: """Whether a given point is within the extents of a configured pipette on the specified robot.""" - robot_type = engine_state.config.robot_type - pipette_channels = engine_state.pipettes.get_channels(pipette_id) - nozzle_config = engine_state.pipettes.get_nozzle_layout_type(pipette_id) - primary_nozzle = engine_state.pipettes.get_primary_nozzle(pipette_id) - if robot_type == "OT-3 Standard": - if pipette_channels == 96 and nozzle_config == NozzleConfigurationType.COLUMN: - # TODO (spp, 2023-12-18): change this eventually to use column mappings in - # the pipette geometry definitions. - if primary_nozzle == "A12": - return ( - A12_column_front_left_bound.x - <= location.x - <= A12_column_back_right_bound.x - and A12_column_front_left_bound.y - <= location.y - <= A12_column_back_right_bound.y - ) - elif primary_nozzle == "A1": - return ( - A1_column_front_left_bound.x - <= location.x - <= A1_column_back_right_bound.x - and A1_column_front_left_bound.y - <= location.y - <= A1_column_back_right_bound.y - ) - # TODO (spp, 2023-11-07): check for 8-channel nozzle A1 & H1 extents on Flex & OT2 - return True + mount = engine_state.pipettes.get_mount(pipette_id) + robot_extent_per_mount = engine_state.geometry.absolute_deck_extents + pip_back_left_bound, pip_front_right_bound, _, _ = pipette_bounding_box_at_loc + pipette_bounds_offsets = engine_state.pipettes.get_pipette_bounding_box(pipette_id) + from_back_right = ( + robot_extent_per_mount.back_right[mount] + + pipette_bounds_offsets.back_right_corner + ) + from_front_left = ( + robot_extent_per_mount.front_left[mount] + + pipette_bounds_offsets.front_left_corner + ) + return ( + from_back_right.x >= pip_back_left_bound.x >= from_front_left.x + and from_back_right.y >= pip_back_left_bound.y >= from_front_left.y + and from_back_right.x >= pip_front_right_bound.x >= from_front_left.x + and from_back_right.y >= pip_front_right_bound.y >= from_front_left.y + ) def _map_labware( diff --git a/api/src/opentrons/protocol_engine/create_protocol_engine.py b/api/src/opentrons/protocol_engine/create_protocol_engine.py index fd7b1b8bd5f..8a6a4355fd7 100644 --- a/api/src/opentrons/protocol_engine/create_protocol_engine.py +++ b/api/src/opentrons/protocol_engine/create_protocol_engine.py @@ -7,6 +7,7 @@ from opentrons.hardware_control.types import DoorState from opentrons.protocol_engine.error_recovery_policy import ErrorRecoveryPolicy from opentrons.util.async_helpers import async_context_manager_in_thread +from opentrons_shared_data.robot import load as load_robot from .protocol_engine import ProtocolEngine from .resources import DeckDataProvider, ModuleDataProvider @@ -45,11 +46,12 @@ async def create_protocol_engine( else [] ) module_calibration_offsets = ModuleDataProvider.load_module_calibrations() - + robot_definition = load_robot(config.robot_type) state_store = StateStore( config=config, deck_definition=deck_definition, deck_fixed_labware=deck_fixed_labware, + robot_definition=robot_definition, is_door_open=hardware_api.door_state is DoorState.OPEN, module_calibration_offsets=module_calibration_offsets, deck_configuration=deck_configuration, diff --git a/api/src/opentrons/protocol_engine/state/addressable_areas.py b/api/src/opentrons/protocol_engine/state/addressable_areas.py index 85c61bfa917..7e3a0325ed4 100644 --- a/api/src/opentrons/protocol_engine/state/addressable_areas.py +++ b/api/src/opentrons/protocol_engine/state/addressable_areas.py @@ -1,8 +1,9 @@ """Basic addressable area data state and store.""" from dataclasses import dataclass +from functools import cached_property from typing import Dict, List, Optional, Set, Union -from opentrons_shared_data.robot.dev_types import RobotType +from opentrons_shared_data.robot.dev_types import RobotType, RobotDefinition from opentrons_shared_data.deck.dev_types import ( DeckDefinitionV5, SlotDefV3, @@ -77,6 +78,9 @@ class AddressableAreaState: use_simulated_deck_config: bool """See `Config.use_simulated_deck_config`.""" + """Information about the current robot model.""" + robot_definition: RobotDefinition + _OT2_ORDERED_SLOTS = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"] _FLEX_ORDERED_SLOTS = [ @@ -164,6 +168,7 @@ def __init__( deck_configuration: DeckConfigurationType, config: Config, deck_definition: DeckDefinitionV5, + robot_definition: RobotDefinition, ) -> None: """Initialize an addressable area store and its state.""" if config.use_simulated_deck_config: @@ -183,6 +188,7 @@ def __init__( deck_definition=deck_definition, robot_type=config.robot_type, use_simulated_deck_config=config.use_simulated_deck_config, + robot_definition=robot_definition, ) def handle_action(self, action: Action) -> None: @@ -330,6 +336,22 @@ def __init__(self, state: AddressableAreaState) -> None: """ self._state = state + @cached_property + def deck_extents(self) -> Point: + """The maximum space on the deck.""" + extents = self._state.robot_definition["extents"] + return Point(x=extents[0], y=extents[1], z=extents[2]) + + @cached_property + def mount_offsets(self) -> Dict[str, Point]: + """The left and right mount offsets of the robot.""" + left_offset = self.state.robot_definition["mountOffsets"]["left"] + right_offset = self.state.robot_definition["mountOffsets"]["right"] + return { + "left": Point(x=left_offset[0], y=left_offset[1], z=left_offset[2]), + "right": Point(x=right_offset[0], y=right_offset[1], z=right_offset[2]), + } + def get_addressable_area(self, addressable_area_name: str) -> AddressableArea: """Get addressable area.""" if not self._state.use_simulated_deck_config: diff --git a/api/src/opentrons/protocol_engine/state/geometry.py b/api/src/opentrons/protocol_engine/state/geometry.py index 112d7d60ef4..904e0c470b2 100644 --- a/api/src/opentrons/protocol_engine/state/geometry.py +++ b/api/src/opentrons/protocol_engine/state/geometry.py @@ -3,6 +3,8 @@ from numpy import array, dot, double as npdouble from numpy.typing import NDArray from typing import Optional, List, Tuple, Union, cast, TypeVar, Dict +from dataclasses import dataclass +from functools import cached_property from opentrons.types import Point, DeckSlotName, StagingSlotName, MountType @@ -71,6 +73,12 @@ class _GripperMoveType(enum.Enum): DROP_LABWARE = enum.auto() +@dataclass +class _AbsoluteRobotExtents: + front_left: Dict[MountType, Point] + back_right: Dict[MountType, Point] + + _LabwareLocation = TypeVar("_LabwareLocation", bound=LabwareLocation) @@ -95,6 +103,24 @@ def __init__( self._addressable_areas = addressable_area_view self._last_drop_tip_location_spot: Dict[str, _TipDropSection] = {} + @cached_property + def absolute_deck_extents(self) -> _AbsoluteRobotExtents: + """The absolute deck extents for a given robot deck.""" + left_offset = self._addressable_areas.mount_offsets["left"] + right_offset = self._addressable_areas.mount_offsets["right"] + + front_left_abs = { + MountType.LEFT: Point(left_offset.x, -1 * left_offset.y, left_offset.z), + MountType.RIGHT: Point(right_offset.x, -1 * right_offset.y, right_offset.z), + } + back_right_abs = { + MountType.LEFT: self._addressable_areas.deck_extents + left_offset, + MountType.RIGHT: self._addressable_areas.deck_extents + right_offset, + } + return _AbsoluteRobotExtents( + front_left=front_left_abs, back_right=back_right_abs + ) + def get_labware_highest_z(self, labware_id: str) -> float: """Get the highest Z-point of a labware.""" labware_data = self._labware.get(labware_id) diff --git a/api/src/opentrons/protocol_engine/state/pipettes.py b/api/src/opentrons/protocol_engine/state/pipettes.py index cab42ac7238..92344dd9600 100644 --- a/api/src/opentrons/protocol_engine/state/pipettes.py +++ b/api/src/opentrons/protocol_engine/state/pipettes.py @@ -97,6 +97,8 @@ class PipetteBoundingBoxOffsets: back_left_corner: Point front_right_corner: Point + back_right_corner: Point + front_left_corner: Point @dataclass(frozen=True) @@ -194,6 +196,16 @@ def _handle_command( # noqa: C901 pipette_bounding_box_offsets=PipetteBoundingBoxOffsets( back_left_corner=config.back_left_corner_offset, front_right_corner=config.front_right_corner_offset, + back_right_corner=Point( + config.front_right_corner_offset.x, + config.back_left_corner_offset.y, + config.back_left_corner_offset.z, + ), + front_left_corner=Point( + config.back_left_corner_offset.x, + config.front_right_corner_offset.y, + config.back_left_corner_offset.z, + ), ), bounding_nozzle_offsets=BoundingNozzlesOffsets( back_left_offset=config.nozzle_map.back_left_nozzle_offset, @@ -788,6 +800,10 @@ def get_pipette_bounding_nozzle_offsets( """Get the nozzle offsets of the pipette's bounding nozzles.""" return self.get_config(pipette_id).bounding_nozzle_offsets + def get_pipette_bounding_box(self, pipette_id: str) -> PipetteBoundingBoxOffsets: + """Get the bounding box of the pipette.""" + return self.get_config(pipette_id).pipette_bounding_box_offsets + def get_pipette_bounds_at_specified_move_to_position( self, pipette_id: str, @@ -796,6 +812,7 @@ def get_pipette_bounds_at_specified_move_to_position( """Get the pipette's bounding offsets when primary nozzle is at the given position.""" primary_nozzle_offset = self.get_primary_nozzle_offset(pipette_id) tip = self.get_attached_tip(pipette_id) + # TODO update this for pipette robot stackup # Primary nozzle position at destination, in deck coordinates primary_nozzle_position = destination_position + Point( x=0, y=0, z=tip.length if tip else 0 diff --git a/api/src/opentrons/protocol_engine/state/state.py b/api/src/opentrons/protocol_engine/state/state.py index aa54383b379..e343a4dfde1 100644 --- a/api/src/opentrons/protocol_engine/state/state.py +++ b/api/src/opentrons/protocol_engine/state/state.py @@ -6,6 +6,7 @@ from typing_extensions import ParamSpec from opentrons_shared_data.deck.dev_types import DeckDefinitionV5 +from opentrons_shared_data.robot.dev_types import RobotDefinition from opentrons.protocol_engine.types import ModuleOffsetData from opentrons.util.change_notifier import ChangeNotifier @@ -144,6 +145,7 @@ def __init__( config: Config, deck_definition: DeckDefinitionV5, deck_fixed_labware: Sequence[DeckFixedLabware], + robot_definition: RobotDefinition, is_door_open: bool, change_notifier: Optional[ChangeNotifier] = None, module_calibration_offsets: Optional[Dict[str, ModuleOffsetData]] = None, @@ -162,6 +164,7 @@ def __init__( change_notifier: Internal state change notifier. module_calibration_offsets: Module offsets to preload. deck_configuration: The initial deck configuration the addressable area store will be instantiated with. + robot_definition: Static information about the robot type being used. notify_publishers: Notifies robot server publishers of internal state change. """ self._command_store = CommandStore(config=config, is_door_open=is_door_open) @@ -172,6 +175,7 @@ def __init__( deck_configuration=deck_configuration, config=config, deck_definition=deck_definition, + robot_definition=robot_definition, ) self._labware_store = LabwareStore( deck_fixed_labware=deck_fixed_labware, diff --git a/api/tests/opentrons/protocol_api/core/engine/test_deck_conflict.py b/api/tests/opentrons/protocol_api/core/engine/test_deck_conflict.py index 82ce80695d3..c50ffe4687e 100644 --- a/api/tests/opentrons/protocol_api/core/engine/test_deck_conflict.py +++ b/api/tests/opentrons/protocol_api/core/engine/test_deck_conflict.py @@ -25,9 +25,12 @@ ModuleModel, StateView, ) +from opentrons.protocol_engine.state.geometry import _AbsoluteRobotExtents +from opentrons.protocol_engine.state.pipettes import PipetteBoundingBoxOffsets + from opentrons.protocol_engine.clients import SyncClient from opentrons.protocol_engine.errors import LabwareNotLoadedOnModuleError -from opentrons.types import DeckSlotName, Point, StagingSlotName +from opentrons.types import DeckSlotName, Point, StagingSlotName, MountType from opentrons.protocol_engine.types import ( DeckType, @@ -416,7 +419,7 @@ def test_maps_trash_bins( [("OT-3 Standard", DeckType.OT3_STANDARD)], ) @pytest.mark.parametrize( - ["pipette_bounds", "expected_raise"], + ["pipette_bounds", "expected_raise", "y_value"], [ ( # nozzles above highest Z ( @@ -426,6 +429,7 @@ def test_maps_trash_bins( Point(x=50, y=50, z=60), ), does_not_raise(), + 0, ), # X, Y, Z collisions ( @@ -439,6 +443,7 @@ def test_maps_trash_bins( deck_conflict.PartialTipMovementNotAllowedError, match="collision with items in deck slot D1", ), + 0, ), ( ( @@ -451,6 +456,7 @@ def test_maps_trash_bins( deck_conflict.PartialTipMovementNotAllowedError, match="collision with items in deck slot D2", ), + 0, ), ( # Collision with staging slot ( @@ -461,8 +467,9 @@ def test_maps_trash_bins( ), pytest.raises( deck_conflict.PartialTipMovementNotAllowedError, - match="collision with items in staging slot C4", + match="will result in collision with items in staging slot C4.", ), + 170, ), ], ) @@ -471,6 +478,7 @@ def test_deck_conflict_raises_for_bad_pipette_move( mock_state_view: StateView, pipette_bounds: Tuple[Point, Point, Point, Point], expected_raise: ContextManager[Any], + y_value: float, ) -> None: """It should raise errors when moving to locations with restrictions for partial pipette movement. @@ -485,7 +493,36 @@ def test_deck_conflict_raises_for_bad_pipette_move( in order to preserve readability of the test. That means the test does actual slot overlap checks. """ - destination_well_point = Point(x=123, y=123, z=123) + destination_well_point = Point(x=123, y=y_value, z=123) + decoy.when( + mock_state_view.pipettes.get_is_partially_configured("pipette-id") + ).then_return(True) + decoy.when(mock_state_view.pipettes.get_mount("pipette-id")).then_return( + MountType.LEFT + ) + decoy.when(mock_state_view.geometry.absolute_deck_extents).then_return( + _AbsoluteRobotExtents( + front_left={ + MountType.LEFT: Point(13.5, -60.5, 0.0), + MountType.RIGHT: Point(-40.5, -60.5, 0.0), + }, + back_right={ + MountType.LEFT: Point(463.7, 433.3, 0.0), + MountType.RIGHT: Point(517.7, 433.3), + }, + ) + ) + decoy.when( + mock_state_view.pipettes.get_pipette_bounding_box("pipette-id") + ).then_return( + # 96 chan outer bounds + PipetteBoundingBoxOffsets( + back_left_corner=Point(-36.0, -25.5, -259.15), + front_right_corner=Point(63.0, -88.5, -259.15), + front_left_corner=Point(-36.0, -88.5, -259.15), + back_right_corner=Point(63.0, -25.5, -259.15), + ) + ) decoy.when( mock_state_view.pipettes.get_is_partially_configured("pipette-id") ).then_return(True) @@ -589,7 +626,7 @@ def test_deck_conflict_raises_for_collision_with_tc_lid( destination_well_point = Point(x=123, y=123, z=123) pipette_bounds_at_destination = ( Point(x=50, y=350, z=204.5), - Point(x=150, y=450, z=204.5), + Point(x=150, y=429, z=204.5), Point(x=150, y=400, z=204.5), Point(x=50, y=300, z=204.5), ) @@ -616,6 +653,32 @@ def test_deck_conflict_raises_for_collision_with_tc_lid( pipette_id="pipette-id", destination_position=destination_well_point ) ).then_return(pipette_bounds_at_destination) + decoy.when(mock_state_view.pipettes.get_mount("pipette-id")).then_return( + MountType.LEFT + ) + decoy.when( + mock_state_view.pipettes.get_pipette_bounding_box("pipette-id") + ).then_return( + # 96 chan outer bounds + PipetteBoundingBoxOffsets( + back_left_corner=Point(-67.0, -3.5, -259.15), + front_right_corner=Point(94.0, -113.0, -259.15), + front_left_corner=Point(-67.0, -113.0, -259.15), + back_right_corner=Point(94.0, -3.5, -259.15), + ) + ) + decoy.when(mock_state_view.geometry.absolute_deck_extents).then_return( + _AbsoluteRobotExtents( + front_left={ + MountType.LEFT: Point(13.5, 60.5, 0.0), + MountType.RIGHT: Point(-40.5, 60.5, 0.0), + }, + back_right={ + MountType.LEFT: Point(463.7, 433.3, 0.0), + MountType.RIGHT: Point(517.7, 433.3), + }, + ) + ) decoy.when( adjacent_slots_getters.get_surrounding_slots(5, robot_type="OT-3 Standard") diff --git a/api/tests/opentrons/protocol_api_integration/test_pipette_movement_deck_conflicts.py b/api/tests/opentrons/protocol_api_integration/test_pipette_movement_deck_conflicts.py index 33e92086edb..4984cd4fa3d 100644 --- a/api/tests/opentrons/protocol_api_integration/test_pipette_movement_deck_conflicts.py +++ b/api/tests/opentrons/protocol_api_integration/test_pipette_movement_deck_conflicts.py @@ -3,7 +3,7 @@ import pytest from opentrons import simulate -from opentrons.protocol_api import COLUMN, ALL +from opentrons.protocol_api import COLUMN, ALL, SINGLE from opentrons.protocol_api.core.engine.deck_conflict import ( PartialTipMovementNotAllowedError, ) @@ -61,8 +61,14 @@ def test_deck_conflicts_for_96_ch_a12_column_configuration() -> None: ): instrument.pick_up_tip(badly_placed_tiprack.wells_by_name()["A1"]) - # No error since no tall item in west slot of destination slot - instrument.pick_up_tip(well_placed_tiprack.wells_by_name()["A1"]) + with pytest.raises( + PartialTipMovementNotAllowedError, match="outside of robot bounds" + ): + # Picking up from A1 in an east-most slot using a configuration with column 12 would + # result in a collision with the side of the robot. + instrument.pick_up_tip(well_placed_tiprack.wells_by_name()["A1"]) + + instrument.pick_up_tip(well_placed_tiprack.wells_by_name()["A12"]) instrument.aspirate(50, well_placed_labware.wells_by_name()["A4"]) with pytest.raises( @@ -75,14 +81,19 @@ def test_deck_conflicts_for_96_ch_a12_column_configuration() -> None: ): instrument.dispense(10, tc_adjacent_plate.wells_by_name()["A1"]) + instrument.dispense(10, tc_adjacent_plate.wells_by_name()["H2"]) + # No error cuz dispensing from high above plate, so it clears tuberack in west slot instrument.dispense(15, badly_placed_labware.wells_by_name()["A1"].top(150)) thermocycler.open_lid() # type: ignore[union-attr] - # Will NOT raise error since first column of TC labware is accessible - # (it is just a few mm away from the left bound) - instrument.dispense(25, accessible_plate.wells_by_name()["A1"]) + with pytest.raises( + PartialTipMovementNotAllowedError, match="outside of robot bounds" + ): + # Dispensing to A1 in an east-most slot using a configuration with column 12 would + # result in a collision with the side of the robot. + instrument.dispense(25, accessible_plate.wells_by_name()["A1"]) instrument.drop_tip() @@ -102,7 +113,7 @@ def test_deck_conflicts_for_96_ch_a12_column_configuration() -> None: @pytest.mark.ot3_only def test_close_shave_deck_conflicts_for_96_ch_a12_column_configuration() -> None: """Shouldn't raise errors for "almost collision"s.""" - protocol_context = simulate.get_protocol_api(version="2.16", robot_type="Flex") + protocol_context = simulate.get_protocol_api(version="2.20", robot_type="Flex") res12 = protocol_context.load_labware("nest_12_reservoir_15ml", "C3") # Mag block and tiprack adapter are very close to the destination reservoir labware @@ -118,13 +129,14 @@ def test_close_shave_deck_conflicts_for_96_ch_a12_column_configuration() -> None deepwell = hs_adapter.load_labware("nest_96_wellplate_2ml_deep") protocol_context.load_trash_bin("A3") p1000_96 = protocol_context.load_instrument("flex_96channel_1000") - p1000_96.configure_nozzle_layout(style=COLUMN, start="A12", tip_racks=[tiprack_8]) + p1000_96.configure_nozzle_layout(style=SINGLE, start="A12", tip_racks=[tiprack_8]) hs.close_labware_latch() # type: ignore[union-attr] + # Note p1000_96.distribute( 15, - res12.wells()[0], - deepwell.rows()[0], + res12["A6"], + deepwell.columns()[6], disposal_vol=0, ) @@ -180,8 +192,15 @@ def test_deck_conflicts_for_96_ch_a1_column_configuration() -> None: with pytest.raises( PartialTipMovementNotAllowedError, match="outside of robot bounds" ): + # Moving the 96 channel in column configuration with column 1 + # is incompatible with moving to a plate in B3 in the right most + # column. instrument.aspirate(25, well_placed_plate.wells_by_name()["A11"]) + # No error because we're moving to column 1 of the plate with + # column 1 of the 96 channel. + instrument.aspirate(25, well_placed_plate.wells_by_name()["A1"]) + # No error cuz no taller labware on the right instrument.aspirate(10, my_tuberack.wells_by_name()["A1"]) diff --git a/api/tests/opentrons/protocol_engine/state/test_addressable_area_state.py b/api/tests/opentrons/protocol_engine/state/test_addressable_area_state.py index 7209e78bb90..66fa692fe25 100644 --- a/api/tests/opentrons/protocol_engine/state/test_addressable_area_state.py +++ b/api/tests/opentrons/protocol_engine/state/test_addressable_area_state.py @@ -28,6 +28,17 @@ def test_deck_configuration_setting( deck_type=DeckType.OT3_STANDARD, ), deck_definition=ot3_standard_deck_def, + robot_definition={ + "displayName": "OT-3", + "robotType": "OT-3 Standard", + "models": ["OT-3 Standard"], + "extents": [477.2, 493.8, 0.0], + "mountOffsets": { + "left": [-13.5, -60.5, 255.675], + "right": [40.5, -60.5, 255.675], + "gripper": [84.55, -12.75, 93.85], + }, + }, ) subject_view = AddressableAreaView(subject.state) diff --git a/api/tests/opentrons/protocol_engine/state/test_addressable_area_store.py b/api/tests/opentrons/protocol_engine/state/test_addressable_area_store.py index c3d52028647..fcadb43940e 100644 --- a/api/tests/opentrons/protocol_engine/state/test_addressable_area_store.py +++ b/api/tests/opentrons/protocol_engine/state/test_addressable_area_store.py @@ -69,6 +69,17 @@ def simulated_subject( deck_type=DeckType.OT3_STANDARD, ), deck_definition=ot3_standard_deck_def, + robot_definition={ + "displayName": "OT-3", + "robotType": "OT-3 Standard", + "models": ["OT-3 Standard"], + "extents": [477.2, 493.8, 0.0], + "mountOffsets": { + "left": [-13.5, -60.5, 255.675], + "right": [40.5, -60.5, 255.675], + "gripper": [84.55, -12.75, 93.85], + }, + }, ) @@ -85,6 +96,17 @@ def subject( deck_type=DeckType.OT3_STANDARD, ), deck_definition=ot3_standard_deck_def, + robot_definition={ + "displayName": "OT-3", + "robotType": "OT-3 Standard", + "models": ["OT-3 Standard"], + "extents": [477.2, 493.8, 0.0], + "mountOffsets": { + "left": [-13.5, -60.5, 255.675], + "right": [40.5, -60.5, 255.675], + "gripper": [84.55, -12.75, 93.85], + }, + }, ) @@ -100,6 +122,17 @@ def test_initial_state_simulated( deck_configuration=[], robot_type="OT-3 Standard", use_simulated_deck_config=True, + robot_definition={ + "displayName": "OT-3", + "robotType": "OT-3 Standard", + "models": ["OT-3 Standard"], + "extents": [477.2, 493.8, 0.0], + "mountOffsets": { + "left": [-13.5, -60.5, 255.675], + "right": [40.5, -60.5, 255.675], + "gripper": [84.55, -12.75, 93.85], + }, + }, ) diff --git a/api/tests/opentrons/protocol_engine/state/test_addressable_area_view.py b/api/tests/opentrons/protocol_engine/state/test_addressable_area_view.py index 30ebe0d0341..3d1cbe9be1a 100644 --- a/api/tests/opentrons/protocol_engine/state/test_addressable_area_view.py +++ b/api/tests/opentrons/protocol_engine/state/test_addressable_area_view.py @@ -64,6 +64,17 @@ def get_addressable_area_view( potential_cutout_fixtures_by_cutout_id=potential_cutout_fixtures_by_cutout_id or {}, deck_definition=deck_definition or cast(DeckDefinitionV5, {"otId": "fake"}), + robot_definition={ + "displayName": "OT-3", + "robotType": "OT-3 Standard", + "models": ["OT-3 Standard"], + "extents": [477.2, 493.8, 0.0], + "mountOffsets": { + "left": [-13.5, -60.5, 255.675], + "right": [40.5, -60.5, 255.675], + "gripper": [84.55, -12.75, 93.85], + }, + }, deck_configuration=deck_configuration or [], robot_type=robot_type, use_simulated_deck_config=use_simulated_deck_config, diff --git a/api/tests/opentrons/protocol_engine/state/test_geometry_view.py b/api/tests/opentrons/protocol_engine/state/test_geometry_view.py index 58a4a49940e..9887a4ef76c 100644 --- a/api/tests/opentrons/protocol_engine/state/test_geometry_view.py +++ b/api/tests/opentrons/protocol_engine/state/test_geometry_view.py @@ -174,7 +174,20 @@ def addressable_area_store( ) -> AddressableAreaStore: """Get an addressable area store that can accept actions.""" return AddressableAreaStore( - deck_configuration=[], config=state_config, deck_definition=deck_definition + deck_configuration=[], + config=state_config, + deck_definition=deck_definition, + robot_definition={ + "displayName": "OT-3", + "robotType": "OT-3 Standard", + "models": ["OT-3 Standard"], + "extents": [477.2, 493.8, 0.0], + "mountOffsets": { + "left": [-13.5, -60.5, 255.675], + "right": [40.5, -60.5, 255.675], + "gripper": [84.55, -12.75, 93.85], + }, + }, ) @@ -2077,6 +2090,8 @@ def test_get_next_drop_tip_location( pipette_bounding_box_offsets=PipetteBoundingBoxOffsets( back_left_corner=Point(x=10, y=20, z=30), front_right_corner=Point(x=40, y=50, z=60), + front_left_corner=Point(x=10, y=50, z=60), + back_right_corner=Point(x=40, y=20, z=60), ), lld_settings={}, ) diff --git a/api/tests/opentrons/protocol_engine/state/test_module_store.py b/api/tests/opentrons/protocol_engine/state/test_module_store.py index e6de0a96ac0..0dabf508483 100644 --- a/api/tests/opentrons/protocol_engine/state/test_module_store.py +++ b/api/tests/opentrons/protocol_engine/state/test_module_store.py @@ -74,6 +74,17 @@ def get_addressable_area_view( or {}, deck_definition=deck_definition or cast(DeckDefinitionV5, {"otId": "fake"}), deck_configuration=deck_configuration or [], + robot_definition={ + "displayName": "OT-3", + "robotType": "OT-3 Standard", + "models": ["OT-3 Standard"], + "extents": [477.2, 493.8, 0.0], + "mountOffsets": { + "left": [-13.5, -60.5, 255.675], + "right": [40.5, -60.5, 255.675], + "gripper": [84.55, -12.75, 93.85], + }, + }, robot_type=robot_type, use_simulated_deck_config=use_simulated_deck_config, ) diff --git a/api/tests/opentrons/protocol_engine/state/test_module_view.py b/api/tests/opentrons/protocol_engine/state/test_module_view.py index b840673f2e8..e308c09407d 100644 --- a/api/tests/opentrons/protocol_engine/state/test_module_view.py +++ b/api/tests/opentrons/protocol_engine/state/test_module_view.py @@ -87,6 +87,17 @@ def get_addressable_area_view( or {}, deck_definition=deck_definition or cast(DeckDefinitionV5, {"otId": "fake"}), deck_configuration=deck_configuration or [], + robot_definition={ + "displayName": "OT-3", + "robotType": "OT-3 Standard", + "models": ["OT-3 Standard"], + "extents": [477.2, 493.8, 0.0], + "mountOffsets": { + "left": [-13.5, -60.5, 255.675], + "right": [40.5, -60.5, 255.675], + "gripper": [84.55, -12.75, 93.85], + }, + }, robot_type=robot_type, use_simulated_deck_config=use_simulated_deck_config, ) diff --git a/api/tests/opentrons/protocol_engine/state/test_pipette_store.py b/api/tests/opentrons/protocol_engine/state/test_pipette_store.py index a99ac90e9e2..8ccfc06fd07 100644 --- a/api/tests/opentrons/protocol_engine/state/test_pipette_store.py +++ b/api/tests/opentrons/protocol_engine/state/test_pipette_store.py @@ -775,6 +775,8 @@ def test_add_pipette_config( pipette_bounding_box_offsets=PipetteBoundingBoxOffsets( back_left_corner=Point(x=1, y=2, z=3), front_right_corner=Point(x=4, y=5, z=6), + front_left_corner=Point(x=1, y=5, z=3), + back_right_corner=Point(x=4, y=2, z=3), ), lld_settings={}, ) diff --git a/api/tests/opentrons/protocol_engine/state/test_pipette_view.py b/api/tests/opentrons/protocol_engine/state/test_pipette_view.py index e15c8401699..1942a9a04e1 100644 --- a/api/tests/opentrons/protocol_engine/state/test_pipette_view.py +++ b/api/tests/opentrons/protocol_engine/state/test_pipette_view.py @@ -46,7 +46,10 @@ back_left_offset=Point(x=10, y=20, z=30), front_right_offset=Point(x=40, y=50, z=60) ) _SAMPLE_PIPETTE_BOUNDING_BOX_OFFSETS = PipetteBoundingBoxOffsets( - back_left_corner=Point(x=10, y=20, z=30), front_right_corner=Point(x=40, y=50, z=60) + back_left_corner=Point(x=10, y=20, z=30), + front_right_corner=Point(x=40, y=50, z=60), + front_left_corner=Point(x=10, y=50, z=60), + back_right_corner=Point(x=40, y=20, z=60), ) @@ -594,6 +597,8 @@ class _PipetteSpecs(NamedTuple): bounding_box_offsets=PipetteBoundingBoxOffsets( back_left_corner=Point(0.0, 31.5, 35.52), front_right_corner=Point(0.0, -31.5, 35.52), + front_left_corner=Point(0.0, -31.5, 35.52), + back_right_corner=Point(0.0, 31.5, 35.52), ), nozzle_map=NozzleMap.build( physical_nozzles=EIGHT_CHANNEL_MAP, @@ -620,6 +625,8 @@ class _PipetteSpecs(NamedTuple): bounding_box_offsets=PipetteBoundingBoxOffsets( back_left_corner=Point(0.0, 31.5, 35.52), front_right_corner=Point(0.0, -31.5, 35.52), + front_left_corner=Point(0.0, -31.5, 35.52), + back_right_corner=Point(0.0, 31.5, 35.52), ), nozzle_map=NozzleMap.build( physical_nozzles=EIGHT_CHANNEL_MAP, @@ -646,6 +653,8 @@ class _PipetteSpecs(NamedTuple): bounding_box_offsets=PipetteBoundingBoxOffsets( back_left_corner=Point(-36.0, -25.5, -259.15), front_right_corner=Point(63.0, -88.5, -259.15), + front_left_corner=Point(-36.0, -88.5, -259.15), + back_right_corner=Point(63.0, -25.5, -259.15), ), nozzle_map=NozzleMap.build( physical_nozzles=NINETY_SIX_MAP, @@ -688,6 +697,8 @@ class _PipetteSpecs(NamedTuple): bounding_box_offsets=PipetteBoundingBoxOffsets( back_left_corner=Point(-36.0, -25.5, -259.15), front_right_corner=Point(63.0, -88.5, -259.15), + front_left_corner=Point(-36.0, -88.5, -259.15), + back_right_corner=Point(63.0, -25.5, -259.15), ), nozzle_map=NozzleMap.build( physical_nozzles=NINETY_SIX_MAP, @@ -712,6 +723,8 @@ class _PipetteSpecs(NamedTuple): bounding_box_offsets=PipetteBoundingBoxOffsets( back_left_corner=Point(-36.0, -25.5, -259.15), front_right_corner=Point(63.0, -88.5, -259.15), + front_left_corner=Point(-36.0, -88.5, -259.15), + back_right_corner=Point(63.0, -25.5, -259.15), ), nozzle_map=NozzleMap.build( physical_nozzles=NINETY_SIX_MAP, @@ -736,6 +749,8 @@ class _PipetteSpecs(NamedTuple): bounding_box_offsets=PipetteBoundingBoxOffsets( back_left_corner=Point(-36.0, -25.5, -259.15), front_right_corner=Point(63.0, -88.5, -259.15), + front_left_corner=Point(-36.0, -88.5, -259.15), + back_right_corner=Point(63.0, -25.5, -259.15), ), nozzle_map=NozzleMap.build( physical_nozzles=NINETY_SIX_MAP, diff --git a/api/tests/opentrons/protocol_engine/state/test_state_store.py b/api/tests/opentrons/protocol_engine/state/test_state_store.py index d69784c6834..26f50515317 100644 --- a/api/tests/opentrons/protocol_engine/state/test_state_store.py +++ b/api/tests/opentrons/protocol_engine/state/test_state_store.py @@ -39,6 +39,13 @@ def subject( return StateStore( config=engine_config, deck_definition=ot2_standard_deck_def, + robot_definition={ + "displayName": "OT-2", + "robotType": "OT-2 Standard", + "models": ["OT-2 Standard", "OT-2 Refresh"], + "extents": [446.75, 347.5, 0.0], + "mountOffsets": {"left": [-34.0, 0.0, 0.0], "right": [0.0, 0.0, 0.0]}, + }, deck_fixed_labware=[], change_notifier=change_notifier, is_door_open=False, diff --git a/shared-data/deck/definitions/4/ot2_standard.json b/shared-data/deck/definitions/4/ot2_standard.json index 344469c65d3..e576a393e2c 100644 --- a/shared-data/deck/definitions/4/ot2_standard.json +++ b/shared-data/deck/definitions/4/ot2_standard.json @@ -8,7 +8,12 @@ "tags": ["ot2", "12 slots", "standard"] }, "robot": { - "model": "OT-2 Standard" + "model": "OT-2 Standard", + "extents": [446.75, 347.5, 0.0], + "mountOffsets": { + "left": [-34, 0.0, 0.0], + "right": [0.0, 0.0, 0.0] + } }, "locations": { "addressableAreas": [ diff --git a/shared-data/python/opentrons_shared_data/robot/dev_types.py b/shared-data/python/opentrons_shared_data/robot/dev_types.py index 555ec6008ba..90f0f19c1c4 100644 --- a/shared-data/python/opentrons_shared_data/robot/dev_types.py +++ b/shared-data/python/opentrons_shared_data/robot/dev_types.py @@ -1,7 +1,7 @@ """opentrons_shared_data.robot.dev_types: types for robot def.""" import enum from typing import NewType, List, Dict, Any -from typing_extensions import Literal, TypedDict +from typing_extensions import Literal, TypedDict, NotRequired RobotSchemaVersion1 = Literal[1] @@ -29,9 +29,19 @@ def robot_literal_to_enum(cls, robot_type: RobotType) -> "RobotTypeEnum": # No final `else` statement, depend on mypy exhaustiveness checking +class mountOffset(TypedDict): + """The mount offsets for a given robot type based off the center of the carriage..""" + + left: List[float] + right: List[float] + gripper: NotRequired[List[float]] + + class RobotDefinition(TypedDict): """A python version of the robot definition type.""" displayName: str robotType: RobotType models: List[str] + extents: List[float] + mountOffsets: mountOffset diff --git a/shared-data/robot/definitions/1/ot2.json b/shared-data/robot/definitions/1/ot2.json index 9f6a48be16c..50c6eb4256a 100644 --- a/shared-data/robot/definitions/1/ot2.json +++ b/shared-data/robot/definitions/1/ot2.json @@ -1,5 +1,10 @@ { "displayName": "OT-2", "robotType": "OT-2 Standard", - "models": ["OT-2 Standard", "OT-2 Refresh"] + "models": ["OT-2 Standard", "OT-2 Refresh"], + "extents": [446.75, 347.5, 0.0], + "mountOffsets": { + "left": [-34.0, 0.0, 0.0], + "right": [0.0, 0.0, 0.0] + } } diff --git a/shared-data/robot/definitions/1/ot3.json b/shared-data/robot/definitions/1/ot3.json index 3916570adee..eb3a943d886 100644 --- a/shared-data/robot/definitions/1/ot3.json +++ b/shared-data/robot/definitions/1/ot3.json @@ -1,5 +1,11 @@ { "displayName": "OT-3", "robotType": "OT-3 Standard", - "models": ["OT-3 Standard"] + "models": ["OT-3 Standard"], + "extents": [477.2, 493.8, 0.0], + "mountOffsets": { + "left": [-13.5, -60.5, 255.675], + "right": [40.5, -60.5, 255.675], + "gripper": [84.55, -12.75, 93.85] + } } diff --git a/shared-data/robot/schemas/1.json b/shared-data/robot/schemas/1.json index 5f409032451..44e25e6caf5 100644 --- a/shared-data/robot/schemas/1.json +++ b/shared-data/robot/schemas/1.json @@ -5,11 +5,18 @@ "robotType": { "type": "string", "enum": ["OT-2 Standard", "OT-3 Standard"] + }, + "xyzArray": { + "type": "array", + "description": "Array of 3 numbers, [x, y, z]", + "items": { "type": "number" }, + "minItems": 3, + "maxItems": 3 } }, "description": "Describes an Opentrons liquid handling robot.", "type": "object", - "required": ["displayName", "robotType", "models"], + "required": ["displayName", "robotType", "models", "extents", "mountOffsets"], "properties": { "displayName": { "description": "A user-facing friendly name for the machine.", @@ -25,6 +32,29 @@ "items": { "type": "string" } + }, + "extents": { + "description": "The maximum addressable coordinates of the deck without instruments.", + "$ref": "#/definitions/xyzArray" + }, + "mountOffsets": { + "description": "The physical mount offsets from the center of the instrument carriage.", + "type": "object", + "required": ["left", "right"], + "properties": { + "left": { + "description": "The left mount offset from the center of the carriage to the center of the left mount", + "$ref": "#/definitions/xyzArray" + }, + "right": { + "description": "The right mount offset from the center of the carriage to the center of the right mount", + "$ref": "#/definitions/xyzArray" + }, + "gripper": { + "description": "The gripper mount offset from the center of the carriage to the center of the gripper, only on OT-3 Standard definitions", + "$ref": "#/definitions/xyzArray" + } + } } } } From fa230713d98c6e2e367844c125a1940e8c16cb33 Mon Sep 17 00:00:00 2001 From: Shlok Amin Date: Thu, 18 Jul 2024 10:58:59 -0400 Subject: [PATCH 51/78] feat(app-shell): update user license agreement for desktop app installers (#15631) --- app-shell/build/license_en.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app-shell/build/license_en.txt b/app-shell/build/license_en.txt index f16605697b0..cf847badf81 100644 --- a/app-shell/build/license_en.txt +++ b/app-shell/build/license_en.txt @@ -1,6 +1,6 @@ Opentrons End-User License Agreement -Last updated: June 27, 2024 +Last updated: July 10, 2024 THIS END-USER LICENSE AGREEMENT (“EULA”) is a legal agreement between you (“User”), either as an individual or on behalf of an entity, and Opentrons Labworks Inc. (“Opentrons”) regarding your use of Opentrons robots, modules, software, and associated documentation (“Opentrons Products”) including, but not limited to, the Opentrons OT-2 robot and associated modules, the Opentrons Flex robot and associated modules, the Opentrons App, the Opentrons API, the Opentrons Protocol Designer and Protocol Library, the Opentrons Labware Library, and the Opentrons Website. By installing or using the Opentrons Products, you agree to be bound by the terms and conditions of this EULA. If you do not agree to the terms of this EULA, you must immediately cease use of the Opentrons Products. @@ -9,7 +9,7 @@ Use of Opentrons Products. Permitted Use. User shall use the Opentrons Products strictly in accordance with the terms of the EULA and Related Agreements. User shall use Opentrons Product software only in conjunction with Opentrons Product hardware. Restrictions on Use. Unless otherwise specified in a separate agreement entered into between Opentrons and User, User may not, and may not permit others to: reverse engineer, decompile or otherwise derive source code from the Opentrons Products; -disassemble the Opentrons Products, except as instructed by Opentrons employees or Opentrons technical product manuals; +disassemble or bypass protection on Opentrons Products to exceed authorized access to Opentrons systems, or to analyze or modify components of the Opentrons Products for the purpose of gaining unauthorized access to confidential Opentrons or Opentrons Product information; copy, modify, or create derivative works of the Opentrons Products for the purpose of competing with Opentrons; remove or alter any proprietary notices or marks on the Opentrons Products; use the Opentrons Products in any manner that does not comply with the applicable laws in the jurisdiction(s) in which such use takes place; From 8eb14aed45da44bf8acb9f13cb309e997d5bb580 Mon Sep 17 00:00:00 2001 From: Seth Foster Date: Thu, 18 Jul 2024 11:39:03 -0400 Subject: [PATCH 52/78] refactor(app): Desktop implementation for SelectRecoveryOption (#15691) This PR got away from me a little bit (and I still have to test it - the sum of these two is why it's in draft). It's probably best to view it commit by commit. Overall, the point is to implement the desktop styling for the SelectRecoveryOption component of error recovery, here: https://www.figma.com/design/8dMeu8MuPfXoORtOV6cACO/Feature%3A-Error-Recovery-August-Release?node-id=4829-78111&t=bTwOek0mZSA61ugm-4 . This is a little weird because it is exactly semantically equivalent to the ODD panel, but has a very different styling - two columns instead of one. It also uses the radio buttons that we use only in OT-2 tip length calibration method selection, which are old and use cssmodules and need a cleanup. The approach here was to add some fundamental structure components to InterventionModal that will render two columns on desktop (as the TwoColumn structure, including min-size and wrap) and one column on the ODD at full width. Then, we can build on top of that in ErrorRecovery, with the big difference being that the ErrorRecovery component also folds in the standard ER footer (or will - already had to find/replace a bunch of stuff, going to move other components over to specifying their footers through the wrapper as we update their styles). There were also some incidental refactors. By commit, - 981e82802c06c1c5890fe2cb45ce24ec704a6775 : we have these utility components for visualizing structure components; make them handle sometimes being size limited and sometimes not being - a7917e746138d314a0b76f4398fc0bcef5ec4877 : Add a wrapper around the RadioGroup component. We're going to need to update these styles further (that's a todo) but we don't want to, or I don't want to, have to mess with cssmodules. Also I think @TamarZanzouri is touching this stuff at the same time, so keep it isolated and droppable. Did I let the worms in my brain drive and make a typescript wrapper for getting the change events to take an inferred union of button values? Yes. Also note the one change to the underlying component to specify IDs so that we can use aria label linking in the passed components, which is both good practice and allows tests to work later. - e835f8604e5bcaf3430b67d21155562919a883d5 The error recovery component for viewing failed and next commands was bundled with presenting text in the left column, which is not what we want this panel to have, so split it out. - 699cb79b035aa157aca8544b9bddb9d5035eaedf The first fun one: Add a component that can responsively present one or two columns, and by "responsively" i mean abuse css mediaquery to only do it on touchscreen and maintain the two-column responsive presentation through desktop resizes. Best appreciated in storybook. - 52150cbdcd24fa87c1bb754b6d1296444b4ed691 Simultaneous refactor and feature - we had a RecoveryContentWrapper but it was really just a single column, this isn't helpful, add a single and twocolumn version (lots of find and replace since I changed the name) and then add a OneOrTwoColumn on top of that. These wrappers also encompass rendering footers because the footer needs to be in different places in one or two column - 00f38e8e4db80b1c16f2c4395b36c963a5bffa25 Use all that stuff to render SelectRecoveryOption! Note that the tests now run on both presentations and it all works because of that aria label stuff. ## Todo - [x] Visual tests, at all (this is why it's a draft) --- .../InterventionModal/OneColumn.stories.tsx | 36 +-- .../molecules/InterventionModal/OneColumn.tsx | 25 +- .../OneColumnOrTwoColumn.stories.tsx | 68 +++++ .../OneColumnOrTwoColumn.tsx | 55 ++++ .../molecules/InterventionModal/TwoColumn.tsx | 16 +- .../molecules/InterventionModal/constants.ts | 1 + app/src/molecules/InterventionModal/index.tsx | 2 + .../InterventionModal/story-utils/StandIn.tsx | 10 +- .../story-utils/VisibleContainer.tsx | 5 +- .../ErrorRecoveryFlows/RecoveryDoorOpen.tsx | 9 +- .../ErrorRecoveryFlows/RecoveryError.tsx | 6 +- .../RecoveryOptions/CancelRun.tsx | 9 +- .../RecoveryOptions/FillWellAndSkip.tsx | 6 +- .../RecoveryOptions/IgnoreErrorSkipStep.tsx | 9 +- .../RecoveryOptions/ManageTips.tsx | 13 +- .../RecoveryOptions/SelectRecoveryOption.tsx | 132 +++++++--- .../__tests__/IgnoreErrorSkipStep.test.tsx | 4 +- .../__tests__/SelectRecoveryOptions.test.tsx | 235 ++++++++++-------- .../ErrorRecoveryFlows/RunPausedSplash.tsx | 7 +- .../ErrorRecoveryFlows/__fixtures__/index.ts | 2 +- .../organisms/ErrorRecoveryFlows/constants.ts | 13 +- .../shared/FailedStepNextStep.tsx | 62 +++++ .../shared/RecoveryContentWrapper.tsx | 82 +++++- .../shared/RecoveryRadioGroup.tsx | 42 ++++ .../ErrorRecoveryFlows/shared/ReplaceTips.tsx | 6 +- .../ErrorRecoveryFlows/shared/SelectTips.tsx | 6 +- .../TwoColTextAndFailedStepNextStep.tsx | 81 ++---- .../shared/__tests__/StepInfo.test.tsx | 7 +- .../ErrorRecoveryFlows/shared/index.ts | 8 +- components/src/forms/RadioGroup.tsx | 2 +- 30 files changed, 664 insertions(+), 295 deletions(-) create mode 100644 app/src/molecules/InterventionModal/OneColumnOrTwoColumn.stories.tsx create mode 100644 app/src/molecules/InterventionModal/OneColumnOrTwoColumn.tsx create mode 100644 app/src/molecules/InterventionModal/constants.ts create mode 100644 app/src/organisms/ErrorRecoveryFlows/shared/FailedStepNextStep.tsx create mode 100644 app/src/organisms/ErrorRecoveryFlows/shared/RecoveryRadioGroup.tsx diff --git a/app/src/molecules/InterventionModal/OneColumn.stories.tsx b/app/src/molecules/InterventionModal/OneColumn.stories.tsx index 60e4efa03b8..ef4e8a6a02f 100644 --- a/app/src/molecules/InterventionModal/OneColumn.stories.tsx +++ b/app/src/molecules/InterventionModal/OneColumn.stories.tsx @@ -1,39 +1,13 @@ import * as React from 'react' -import { - LegacyStyledText, - Box, - Flex, - BORDERS, - RESPONSIVENESS, - SPACING, - ALIGN_CENTER, - JUSTIFY_CENTER, -} from '@opentrons/components' +import { Box, RESPONSIVENESS } from '@opentrons/components' import { OneColumn as OneColumnComponent } from './' +import { StandInContent } from './story-utils/StandIn' import type { Meta, StoryObj } from '@storybook/react' -function StandInContent(): JSX.Element { - return ( - - - This is a standin for some other component - - - ) -} - -const meta: Meta> = { +const meta: Meta> = { title: 'App/Molecules/InterventionModal/OneColumn', component: OneColumnComponent, render: args => ( @@ -46,7 +20,7 @@ const meta: Meta> = { `} > - + This is a standin for another component
), @@ -54,6 +28,6 @@ const meta: Meta> = { export default meta -export type Story = StoryObj +export type Story = StoryObj export const ExampleOneColumn: Story = { args: {} } diff --git a/app/src/molecules/InterventionModal/OneColumn.tsx b/app/src/molecules/InterventionModal/OneColumn.tsx index 0c36b6ecac7..e92f3ffd51e 100644 --- a/app/src/molecules/InterventionModal/OneColumn.tsx +++ b/app/src/molecules/InterventionModal/OneColumn.tsx @@ -1,11 +1,28 @@ import * as React from 'react' -import { Box } from '@opentrons/components' +import { + Flex, + DIRECTION_COLUMN, + JUSTIFY_SPACE_BETWEEN, +} from '@opentrons/components' +import type { StyleProps } from '@opentrons/components' -export interface OneColumnProps { +export interface OneColumnProps extends StyleProps { children: React.ReactNode } -export function OneColumn({ children }: OneColumnProps): JSX.Element { - return {children} +export function OneColumn({ + children, + ...styleProps +}: OneColumnProps): JSX.Element { + return ( + + {children} + + ) } diff --git a/app/src/molecules/InterventionModal/OneColumnOrTwoColumn.stories.tsx b/app/src/molecules/InterventionModal/OneColumnOrTwoColumn.stories.tsx new file mode 100644 index 00000000000..791edcbdb83 --- /dev/null +++ b/app/src/molecules/InterventionModal/OneColumnOrTwoColumn.stories.tsx @@ -0,0 +1,68 @@ +import * as React from 'react' + +import { OneColumnOrTwoColumn } from './' + +import { StandInContent } from './story-utils/StandIn' +import { VisibleContainer } from './story-utils/VisibleContainer' +import { css } from 'styled-components' +import { + RESPONSIVENESS, + Flex, + ALIGN_CENTER, + JUSTIFY_SPACE_AROUND, + DIRECTION_COLUMN, +} from '@opentrons/components' + +import type { Meta, StoryObj } from '@storybook/react' + +function Wrapper(props: {}): JSX.Element { + return ( + + + + This component is the only one shown on the ODD. + + + + + This component is shown in the right column on desktop. + + + + ) +} + +const meta: Meta> = { + title: 'App/Molecules/InterventionModal/OneColumnOrTwoColumn', + component: Wrapper, + decorators: [ + Story => ( + + + + ), + ], +} + +export default meta + +type Story = StoryObj + +export const OneOrTwoColumn: Story = {} diff --git a/app/src/molecules/InterventionModal/OneColumnOrTwoColumn.tsx b/app/src/molecules/InterventionModal/OneColumnOrTwoColumn.tsx new file mode 100644 index 00000000000..8a6455d67e3 --- /dev/null +++ b/app/src/molecules/InterventionModal/OneColumnOrTwoColumn.tsx @@ -0,0 +1,55 @@ +import * as React from 'react' + +import { css } from 'styled-components' +import { + Flex, + Box, + DIRECTION_ROW, + SPACING, + WRAP, + RESPONSIVENESS, +} from '@opentrons/components' +import type { StyleProps } from '@opentrons/components' +import { TWO_COLUMN_ELEMENT_MIN_WIDTH } from './constants' + +export interface OneColumnOrTwoColumnProps extends StyleProps { + children: [React.ReactNode, React.ReactNode] +} + +export function OneColumnOrTwoColumn({ + children: [leftOrSingleElement, optionallyDisplayedRightElement], + ...styleProps +}: OneColumnOrTwoColumnProps): JSX.Element { + return ( + + + {leftOrSingleElement} + + + {optionallyDisplayedRightElement} + + + ) +} diff --git a/app/src/molecules/InterventionModal/TwoColumn.tsx b/app/src/molecules/InterventionModal/TwoColumn.tsx index 8e87a2d62b5..f0ed10ebf2a 100644 --- a/app/src/molecules/InterventionModal/TwoColumn.tsx +++ b/app/src/molecules/InterventionModal/TwoColumn.tsx @@ -1,20 +1,28 @@ import * as React from 'react' import { Flex, Box, DIRECTION_ROW, SPACING, WRAP } from '@opentrons/components' +import type { StyleProps } from '@opentrons/components' +import { TWO_COLUMN_ELEMENT_MIN_WIDTH } from './constants' -export interface TwoColumnProps { +export interface TwoColumnProps extends StyleProps { children: [React.ReactNode, React.ReactNode] } export function TwoColumn({ children: [leftElement, rightElement], + ...styleProps }: TwoColumnProps): JSX.Element { return ( - - + + {leftElement} - + {rightElement} diff --git a/app/src/molecules/InterventionModal/constants.ts b/app/src/molecules/InterventionModal/constants.ts new file mode 100644 index 00000000000..c5f1fbea4d0 --- /dev/null +++ b/app/src/molecules/InterventionModal/constants.ts @@ -0,0 +1 @@ +export const TWO_COLUMN_ELEMENT_MIN_WIDTH = '17.1875rem' as const diff --git a/app/src/molecules/InterventionModal/index.tsx b/app/src/molecules/InterventionModal/index.tsx index 4d2de359b60..b7f0ab17be7 100644 --- a/app/src/molecules/InterventionModal/index.tsx +++ b/app/src/molecules/InterventionModal/index.tsx @@ -23,6 +23,7 @@ import type { IconName } from '@opentrons/components' import { ModalContentOneColSimpleButtons } from './ModalContentOneColSimpleButtons' import { TwoColumn } from './TwoColumn' import { OneColumn } from './OneColumn' +import { OneColumnOrTwoColumn } from './OneColumnOrTwoColumn' import { ModalContentMixed } from './ModalContentMixed' import { DescriptionContent } from './DescriptionContent' import { DeckMapContent } from './DeckMapContent' @@ -31,6 +32,7 @@ export { ModalContentOneColSimpleButtons, TwoColumn, OneColumn, + OneColumnOrTwoColumn, ModalContentMixed, DescriptionContent, DeckMapContent, diff --git a/app/src/molecules/InterventionModal/story-utils/StandIn.tsx b/app/src/molecules/InterventionModal/story-utils/StandIn.tsx index 28992aba717..0fb46f44b8c 100644 --- a/app/src/molecules/InterventionModal/story-utils/StandIn.tsx +++ b/app/src/molecules/InterventionModal/story-utils/StandIn.tsx @@ -1,13 +1,19 @@ import * as React from 'react' import { Box, BORDERS } from '@opentrons/components' -export function StandInContent(): JSX.Element { +export function StandInContent({ + children, +}: { + children?: React.ReactNode +}): JSX.Element { return ( + > + {children} + ) } diff --git a/app/src/molecules/InterventionModal/story-utils/VisibleContainer.tsx b/app/src/molecules/InterventionModal/story-utils/VisibleContainer.tsx index ac80ecdb063..b716b3335ee 100644 --- a/app/src/molecules/InterventionModal/story-utils/VisibleContainer.tsx +++ b/app/src/molecules/InterventionModal/story-utils/VisibleContainer.tsx @@ -1,13 +1,15 @@ import * as React from 'react' import { Box, BORDERS, SPACING } from '@opentrons/components' +import type { StyleProps } from '@opentrons/components' -export interface VisibleContainerProps { +export interface VisibleContainerProps extends StyleProps { children: JSX.Element | JSX.Element[] } export function VisibleContainer({ children, + ...styleProps }: VisibleContainerProps): JSX.Element { return ( {children} diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryDoorOpen.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryDoorOpen.tsx index 2683a8e3abe..17bf1cf0379 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryDoorOpen.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryDoorOpen.tsx @@ -16,7 +16,10 @@ import { } from '@opentrons/components' import { RUN_STATUS_AWAITING_RECOVERY_BLOCKED_BY_OPEN_DOOR } from '@opentrons/api-client' -import { RecoveryContentWrapper, RecoveryFooterButtons } from './shared' +import { + RecoverySingleColumnContentWrapper, + RecoveryFooterButtons, +} from './shared' import type { RecoveryContentProps } from './types' @@ -31,7 +34,7 @@ export function RecoveryDoorOpen({ const { t } = useTranslation('error_recovery') return ( - + - + ) } diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryError.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryError.tsx index 2c125f9897a..e38647927db 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryError.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryError.tsx @@ -13,7 +13,7 @@ import { } from '@opentrons/components' import { RECOVERY_MAP } from './constants' -import { RecoveryContentWrapper } from './shared' +import { RecoverySingleColumnContentWrapper } from './shared' import type { RecoveryContentProps } from './types' import { SmallButton } from '../../atoms/buttons' @@ -168,7 +168,7 @@ export function ErrorContent({ btnOnClick: () => void }): JSX.Element | null { return ( - + - + ) } diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/CancelRun.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/CancelRun.tsx index 5cf63db7c2f..b9358a10b11 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/CancelRun.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/CancelRun.tsx @@ -12,7 +12,10 @@ import { } from '@opentrons/components' import { RECOVERY_MAP } from '../constants' -import { RecoveryFooterButtons, RecoveryContentWrapper } from '../shared' +import { + RecoveryFooterButtons, + RecoverySingleColumnContentWrapper, +} from '../shared' import { SelectRecoveryOption } from './SelectRecoveryOption' import type { RecoveryContentProps } from '../types' @@ -56,7 +59,7 @@ function CancelRunConfirmation({ }) return ( - @@ -90,7 +93,7 @@ function CancelRunConfirmation({ primaryBtnTextOverride={t('confirm')} isLoadingPrimaryBtnAction={showBtnLoadingState} /> - + ) } diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/FillWellAndSkip.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/FillWellAndSkip.tsx index 19d51269d53..5f2f8971d0a 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/FillWellAndSkip.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/FillWellAndSkip.tsx @@ -12,7 +12,7 @@ import { RECOVERY_MAP } from '../constants' import { CancelRun } from './CancelRun' import { RecoveryFooterButtons, - RecoveryContentWrapper, + RecoverySingleColumnContentWrapper, LeftColumnLabwareInfo, TwoColTextAndFailedStepNextStep, } from '../shared' @@ -49,7 +49,7 @@ export function FillWell(props: RecoveryContentProps): JSX.Element | null { const { goBackPrevStep, proceedNextStep } = routeUpdateActions return ( - + - + ) } diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/IgnoreErrorSkipStep.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/IgnoreErrorSkipStep.tsx index f3f255381ca..c5ecf84a61b 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/IgnoreErrorSkipStep.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/IgnoreErrorSkipStep.tsx @@ -11,7 +11,10 @@ import { import { ODD_SECTION_TITLE_STYLE, RECOVERY_MAP } from '../constants' import { SelectRecoveryOption } from './SelectRecoveryOption' -import { RecoveryFooterButtons, RecoveryContentWrapper } from '../shared' +import { + RecoveryFooterButtons, + RecoverySingleColumnContentWrapper, +} from '../shared' import { RadioButton } from '../../../atoms/buttons' import type { RecoveryContentProps } from '../types' @@ -78,7 +81,7 @@ export function IgnoreErrorStepHome({ } return ( - + {t('ignore_similar_errors_later_in_run')} @@ -93,7 +96,7 @@ export function IgnoreErrorStepHome({ primaryBtnOnClick={primaryOnClick} secondaryBtnOnClick={goBackPrevStep} /> - + ) } diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/ManageTips.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/ManageTips.tsx index a5bfc5eede5..324564576c3 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/ManageTips.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/ManageTips.tsx @@ -12,7 +12,10 @@ import { FLEX_ROBOT_TYPE, OT2_ROBOT_TYPE } from '@opentrons/shared-data' import { RadioButton } from '../../../atoms/buttons' import { ODD_SECTION_TITLE_STYLE, RECOVERY_MAP } from '../constants' -import { RecoveryFooterButtons, RecoveryContentWrapper } from '../shared' +import { + RecoveryFooterButtons, + RecoverySingleColumnContentWrapper, +} from '../shared' import { DropTipWizardFlows } from '../../DropTipWizardFlows' import { DT_ROUTES } from '../../DropTipWizardFlows/constants' import { SelectRecoveryOption } from './SelectRecoveryOption' @@ -87,7 +90,7 @@ export function BeginRemoval({ } return ( - + {t('you_may_want_to_remove', { mount })} @@ -110,7 +113,7 @@ export function BeginRemoval({ /> - + ) } @@ -158,7 +161,7 @@ function DropTipFlowsContainer( const fixitCommandTypeUtils = useDropTipFlowUtils(props) return ( - + - + ) } diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/SelectRecoveryOption.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/SelectRecoveryOption.tsx index 17ccd2e853d..a33f2fb7abc 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/SelectRecoveryOption.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/SelectRecoveryOption.tsx @@ -6,16 +6,22 @@ import { DIRECTION_COLUMN, Flex, SPACING, - LegacyStyledText, + StyledText, } from '@opentrons/components' import { RECOVERY_MAP, ERROR_KINDS, ODD_SECTION_TITLE_STYLE, + ODD_ONLY, + DESKTOP_ONLY, } from '../constants' import { RadioButton } from '../../../atoms/buttons' -import { RecoveryFooterButtons, RecoveryContentWrapper } from '../shared' +import { + RecoveryODDOneDesktopTwoColumnContentWrapper, + RecoveryRadioGroup, + FailedStepNextStep, +} from '../shared' import type { ErrorKind, RecoveryContentProps, RecoveryRoute } from '../types' import type { PipetteWithTip } from '../../DropTipWizardFlows' @@ -45,6 +51,7 @@ export function SelectRecoveryOptionHome({ tipStatusUtils, currentRecoveryOptionUtils, getRecoveryOptionCopy, + ...rest }: RecoveryContentProps): JSX.Element | null { const { t } = useTranslation('error_recovery') const { proceedToRouteAndStep } = routeUpdateActions @@ -58,25 +65,41 @@ export function SelectRecoveryOptionHome({ useCurrentTipStatus(determineTipStatus) return ( - - - {t('choose_a_recovery_action')} - - - - - { + { setSelectedRecoveryOption(selectedRoute) void proceedToRouteAndStep(selectedRoute as RecoveryRoute) - }} - /> - + }, + }} + > + + + {t('choose_a_recovery_action')} + + + + + + + + + + ) } @@ -87,29 +110,66 @@ interface RecoveryOptionsProps { selectedRoute?: RecoveryRoute } // For ODD use only. -export function RecoveryOptions({ +export function ODDRecoveryOptions({ validRecoveryOptions, selectedRoute, setSelectedRoute, getRecoveryOptionCopy, -}: RecoveryOptionsProps): JSX.Element[] { - return validRecoveryOptions.map((recoveryOption: RecoveryRoute) => { - const optionName = getRecoveryOptionCopy(recoveryOption) - - return ( - { - setSelectedRoute(recoveryOption) - }} - isSelected={recoveryOption === selectedRoute} - /> - ) - }) +}: RecoveryOptionsProps): JSX.Element { + return ( + + {validRecoveryOptions.map((recoveryOption: RecoveryRoute) => { + const optionName = getRecoveryOptionCopy(recoveryOption) + return ( + { + setSelectedRoute(recoveryOption) + }} + isSelected={recoveryOption === selectedRoute} + /> + ) + })} + + ) } +export function DesktopRecoveryOptions({ + validRecoveryOptions, + selectedRoute, + setSelectedRoute, + getRecoveryOptionCopy, +}: RecoveryOptionsProps): JSX.Element { + return ( + { + setSelectedRoute(e.currentTarget.value) + }} + value={selectedRoute} + options={validRecoveryOptions.map( + (option: RecoveryRoute) => + ({ + value: option, + children: ( + + {getRecoveryOptionCopy(option)} + + ), + } as const) + )} + /> + ) +} // Pre-fetch tip attachment status. Users are not blocked from proceeding at this step. export function useCurrentTipStatus( determineTipStatus: () => Promise diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/IgnoreErrorSkipStep.test.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/IgnoreErrorSkipStep.test.tsx index b0dd4ec9f1d..d6241b7dcd9 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/IgnoreErrorSkipStep.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/IgnoreErrorSkipStep.test.tsx @@ -20,7 +20,9 @@ vi.mock('../shared', async () => { const actual = await vi.importActual('../shared') return { ...actual, - RecoveryContentWrapper: vi.fn(({ children }) =>
{children}
), + RecoverySingleColumnContentWrapper: vi.fn(({ children }) => ( +
{children}
+ )), } }) vi.mock('../SelectRecoveryOption') diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/SelectRecoveryOptions.test.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/SelectRecoveryOptions.test.tsx index 0db6521b2fc..a70e66d662e 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/SelectRecoveryOptions.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/SelectRecoveryOptions.test.tsx @@ -8,7 +8,8 @@ import { i18n } from '../../../../i18n' import { mockRecoveryContentProps } from '../../__fixtures__' import { SelectRecoveryOption, - RecoveryOptions, + ODDRecoveryOptions, + DesktopRecoveryOptions, getRecoveryOptions, GENERAL_ERROR_OPTIONS, OVERPRESSURE_WHILE_ASPIRATING_OPTIONS, @@ -24,15 +25,25 @@ import type { Mock } from 'vitest' const renderSelectRecoveryOption = ( props: React.ComponentProps ) => { - return renderWithProviders(, { + return renderWithProviders( + , + { + i18nInstance: i18n, + } + )[0] +} + +const renderODDRecoveryOptions = ( + props: React.ComponentProps +) => { + return renderWithProviders(, { i18nInstance: i18n, })[0] } - -const renderRecoveryOptions = ( - props: React.ComponentProps +const renderDesktopRecoveryOptions = ( + props: React.ComponentProps ) => { - return renderWithProviders(, { + return renderWithProviders(, { i18nInstance: i18n, })[0] } @@ -101,13 +112,13 @@ describe('SelectRecoveryOption', () => { screen.getByText('Choose a recovery action') - const retryStepOption = screen.getByRole('label', { name: 'Retry step' }) + const retryStepOption = screen.getAllByRole('label', { name: 'Retry step' }) clickButtonLabeled('Continue') expect( screen.queryByRole('button', { name: 'Go back' }) ).not.toBeInTheDocument() - fireEvent.click(retryStepOption) + fireEvent.click(retryStepOption[0]) clickButtonLabeled('Continue') expect(mockProceedToRouteAndStep).toHaveBeenCalledWith( @@ -125,14 +136,14 @@ describe('SelectRecoveryOption', () => { screen.getByText('Choose a recovery action') - const retryNewTips = screen.getByRole('label', { + const retryNewTips = screen.getAllByRole('label', { name: 'Retry with new tips', }) expect( screen.queryByRole('button', { name: 'Go back' }) ).not.toBeInTheDocument() - fireEvent.click(retryNewTips) + fireEvent.click(retryNewTips[0]) clickButtonLabeled('Continue') expect(mockProceedToRouteAndStep).toHaveBeenCalledWith(RETRY_NEW_TIPS.ROUTE) @@ -148,11 +159,11 @@ describe('SelectRecoveryOption', () => { screen.getByText('Choose a recovery action') - const fillManuallyAndSkip = screen.getByRole('label', { + const fillManuallyAndSkip = screen.getAllByRole('label', { name: 'Manually fill well and skip to next step', }) - fireEvent.click(fillManuallyAndSkip) + fireEvent.click(fillManuallyAndSkip[0]) clickButtonLabeled('Continue') expect(mockProceedToRouteAndStep).toHaveBeenCalledWith( @@ -170,11 +181,11 @@ describe('SelectRecoveryOption', () => { screen.getByText('Choose a recovery action') - const retrySameTips = screen.getByRole('label', { + const retrySameTips = screen.getAllByRole('label', { name: 'Retry with same tips', }) - fireEvent.click(retrySameTips) + fireEvent.click(retrySameTips[0]) clickButtonLabeled('Continue') expect(mockProceedToRouteAndStep).toHaveBeenCalledWith( @@ -192,11 +203,11 @@ describe('SelectRecoveryOption', () => { screen.getByText('Choose a recovery action') - const skipStepWithSameTips = screen.getByRole('label', { + const skipStepWithSameTips = screen.getAllByRole('label', { name: 'Skip to next step with same tips', }) - fireEvent.click(skipStepWithSameTips) + fireEvent.click(skipStepWithSameTips[0]) clickButtonLabeled('Continue') expect(mockProceedToRouteAndStep).toHaveBeenCalledWith( @@ -204,117 +215,123 @@ describe('SelectRecoveryOption', () => { ) }) }) +;([ + ['desktop', renderDesktopRecoveryOptions] as const, + ['odd', renderODDRecoveryOptions] as const, +] as const).forEach(([target, renderer]) => { + describe(`RecoveryOptions on ${target}`, () => { + let props: React.ComponentProps + let mockSetSelectedRoute: Mock + let mockGetRecoveryOptionCopy: Mock + + beforeEach(() => { + mockSetSelectedRoute = vi.fn() + mockGetRecoveryOptionCopy = vi.fn() + const generalRecoveryOptions = getRecoveryOptions( + ERROR_KINDS.GENERAL_ERROR + ) + + props = { + validRecoveryOptions: generalRecoveryOptions, + setSelectedRoute: mockSetSelectedRoute, + getRecoveryOptionCopy: mockGetRecoveryOptionCopy, + } + + when(mockGetRecoveryOptionCopy) + .calledWith(RECOVERY_MAP.RETRY_FAILED_COMMAND.ROUTE) + .thenReturn('Retry step') + when(mockGetRecoveryOptionCopy) + .calledWith(RECOVERY_MAP.CANCEL_RUN.ROUTE) + .thenReturn('Cancel run') + when(mockGetRecoveryOptionCopy) + .calledWith(RECOVERY_MAP.RETRY_NEW_TIPS.ROUTE) + .thenReturn('Retry with new tips') + when(mockGetRecoveryOptionCopy) + .calledWith(RECOVERY_MAP.FILL_MANUALLY_AND_SKIP.ROUTE) + .thenReturn('Manually fill well and skip to next step') + when(mockGetRecoveryOptionCopy) + .calledWith(RECOVERY_MAP.RETRY_SAME_TIPS.ROUTE) + .thenReturn('Retry with same tips') + when(mockGetRecoveryOptionCopy) + .calledWith(RECOVERY_MAP.SKIP_STEP_WITH_SAME_TIPS.ROUTE) + .thenReturn('Skip to next step with same tips') + when(mockGetRecoveryOptionCopy) + .calledWith(RECOVERY_MAP.SKIP_STEP_WITH_NEW_TIPS.ROUTE) + .thenReturn('Skip to next step with new tips') + when(mockGetRecoveryOptionCopy) + .calledWith(RECOVERY_MAP.IGNORE_AND_SKIP.ROUTE) + .thenReturn('Ignore error and skip to next step') + }) -describe('RecoveryOptions', () => { - let props: React.ComponentProps - let mockSetSelectedRoute: Mock - let mockGetRecoveryOptionCopy: Mock - - beforeEach(() => { - mockSetSelectedRoute = vi.fn() - mockGetRecoveryOptionCopy = vi.fn() - const generalRecoveryOptions = getRecoveryOptions(ERROR_KINDS.GENERAL_ERROR) - - props = { - validRecoveryOptions: generalRecoveryOptions, - setSelectedRoute: mockSetSelectedRoute, - getRecoveryOptionCopy: mockGetRecoveryOptionCopy, - } - - when(mockGetRecoveryOptionCopy) - .calledWith(RECOVERY_MAP.RETRY_FAILED_COMMAND.ROUTE) - .thenReturn('Retry step') - when(mockGetRecoveryOptionCopy) - .calledWith(RECOVERY_MAP.CANCEL_RUN.ROUTE) - .thenReturn('Cancel run') - when(mockGetRecoveryOptionCopy) - .calledWith(RECOVERY_MAP.RETRY_NEW_TIPS.ROUTE) - .thenReturn('Retry with new tips') - when(mockGetRecoveryOptionCopy) - .calledWith(RECOVERY_MAP.FILL_MANUALLY_AND_SKIP.ROUTE) - .thenReturn('Manually fill well and skip to next step') - when(mockGetRecoveryOptionCopy) - .calledWith(RECOVERY_MAP.RETRY_SAME_TIPS.ROUTE) - .thenReturn('Retry with same tips') - when(mockGetRecoveryOptionCopy) - .calledWith(RECOVERY_MAP.SKIP_STEP_WITH_SAME_TIPS.ROUTE) - .thenReturn('Skip to next step with same tips') - when(mockGetRecoveryOptionCopy) - .calledWith(RECOVERY_MAP.SKIP_STEP_WITH_NEW_TIPS.ROUTE) - .thenReturn('Skip to next step with new tips') - when(mockGetRecoveryOptionCopy) - .calledWith(RECOVERY_MAP.IGNORE_AND_SKIP.ROUTE) - .thenReturn('Ignore error and skip to next step') - }) - - it('renders valid recovery options for a general error errorKind', () => { - renderRecoveryOptions(props) + it('renders valid recovery options for a general error errorKind', () => { + renderer(props) - screen.getByRole('label', { name: 'Retry step' }) - screen.getByRole('label', { name: 'Cancel run' }) - }) + screen.getByRole('label', { name: 'Retry step' }) + screen.getByRole('label', { name: 'Cancel run' }) + }) - it(`renders valid recovery options for a ${ERROR_KINDS.OVERPRESSURE_WHILE_ASPIRATING} errorKind`, () => { - props = { - ...props, - validRecoveryOptions: OVERPRESSURE_WHILE_ASPIRATING_OPTIONS, - } + it(`renders valid recovery options for a ${ERROR_KINDS.OVERPRESSURE_WHILE_ASPIRATING} errorKind`, () => { + props = { + ...props, + validRecoveryOptions: OVERPRESSURE_WHILE_ASPIRATING_OPTIONS, + } - renderRecoveryOptions(props) + renderer(props) - screen.getByRole('label', { name: 'Retry with new tips' }) - screen.getByRole('label', { name: 'Cancel run' }) - }) + screen.getByRole('label', { name: 'Retry with new tips' }) + screen.getByRole('label', { name: 'Cancel run' }) + }) - it('updates the selectedRoute when a new option is selected', () => { - renderRecoveryOptions(props) + it('updates the selectedRoute when a new option is selected', () => { + renderer(props) - fireEvent.click(screen.getByRole('label', { name: 'Cancel run' })) + fireEvent.click(screen.getByRole('label', { name: 'Cancel run' })) - expect(mockSetSelectedRoute).toHaveBeenCalledWith( - RECOVERY_MAP.CANCEL_RUN.ROUTE - ) - }) + expect(mockSetSelectedRoute).toHaveBeenCalledWith( + RECOVERY_MAP.CANCEL_RUN.ROUTE + ) + }) - it(`renders valid recovery options for a ${ERROR_KINDS.NO_LIQUID_DETECTED} errorKind`, () => { - props = { - ...props, - validRecoveryOptions: NO_LIQUID_DETECTED_OPTIONS, - } + it(`renders valid recovery options for a ${ERROR_KINDS.NO_LIQUID_DETECTED} errorKind`, () => { + props = { + ...props, + validRecoveryOptions: NO_LIQUID_DETECTED_OPTIONS, + } - renderRecoveryOptions(props) + renderer(props) - screen.getByRole('label', { - name: 'Manually fill well and skip to next step', + screen.getByRole('label', { + name: 'Manually fill well and skip to next step', + }) + screen.getByRole('label', { name: 'Ignore error and skip to next step' }) + screen.getByRole('label', { name: 'Cancel run' }) }) - screen.getByRole('label', { name: 'Ignore error and skip to next step' }) - screen.getByRole('label', { name: 'Cancel run' }) - }) - it(`renders valid recovery options for a ${ERROR_KINDS.OVERPRESSURE_PREPARE_TO_ASPIRATE} errorKind`, () => { - props = { - ...props, - validRecoveryOptions: OVERPRESSURE_PREPARE_TO_ASPIRATE, - } + it(`renders valid recovery options for a ${ERROR_KINDS.OVERPRESSURE_PREPARE_TO_ASPIRATE} errorKind`, () => { + props = { + ...props, + validRecoveryOptions: OVERPRESSURE_PREPARE_TO_ASPIRATE, + } - renderRecoveryOptions(props) + renderer(props) - screen.getByRole('label', { name: 'Retry with new tips' }) - screen.getByRole('label', { name: 'Retry with same tips' }) - screen.getByRole('label', { name: 'Cancel run' }) - }) + screen.getByRole('label', { name: 'Retry with new tips' }) + screen.getByRole('label', { name: 'Retry with same tips' }) + screen.getByRole('label', { name: 'Cancel run' }) + }) - it(`renders valid recovery options for a ${ERROR_KINDS.OVERPRESSURE_WHILE_DISPENSING} errorKind`, () => { - props = { - ...props, - validRecoveryOptions: OVERPRESSURE_WHILE_DISPENSING_OPTIONS, - } + it(`renders valid recovery options for a ${ERROR_KINDS.OVERPRESSURE_WHILE_DISPENSING} errorKind`, () => { + props = { + ...props, + validRecoveryOptions: OVERPRESSURE_WHILE_DISPENSING_OPTIONS, + } - renderRecoveryOptions(props) + renderer(props) - screen.getByRole('label', { name: 'Skip to next step with same tips' }) - screen.getByRole('label', { name: 'Skip to next step with new tips' }) - screen.getByRole('label', { name: 'Cancel run' }) + screen.getByRole('label', { name: 'Skip to next step with same tips' }) + screen.getByRole('label', { name: 'Skip to next step with new tips' }) + screen.getByRole('label', { name: 'Cancel run' }) + }) }) }) diff --git a/app/src/organisms/ErrorRecoveryFlows/RunPausedSplash.tsx b/app/src/organisms/ErrorRecoveryFlows/RunPausedSplash.tsx index 23633a8b20b..f9d253719ed 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RunPausedSplash.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RunPausedSplash.tsx @@ -30,7 +30,7 @@ import { LargeButton } from '../../atoms/buttons' import { RECOVERY_MAP } from './constants' import { RecoveryInterventionModal, - RecoveryContentWrapper, + RecoverySingleColumnContentWrapper, StepInfo, } from './shared' @@ -151,13 +151,12 @@ export function RunPausedSplash( titleHeading={buildTitleHeadingDesktop()} isOnDevice={isOnDevice} > - +
- + ) } diff --git a/app/src/organisms/ErrorRecoveryFlows/__fixtures__/index.ts b/app/src/organisms/ErrorRecoveryFlows/__fixtures__/index.ts index 51639bb3981..7666d7beebf 100644 --- a/app/src/organisms/ErrorRecoveryFlows/__fixtures__/index.ts +++ b/app/src/organisms/ErrorRecoveryFlows/__fixtures__/index.ts @@ -73,7 +73,7 @@ export const mockRecoveryContentProps: RecoveryContentProps = { failedPipetteInfo: {} as any, deckMapUtils: { setSelectedLocation: () => {} } as any, stepCounts: {} as any, - protocolAnalysis: { commands: [mockFailedCommand] } as any, + protocolAnalysis: mockRobotSideAnalysis, trackExternalMap: () => null, hasLaunchedRecovery: true, getRecoveryOptionCopy: () => 'MOCK_COPY', diff --git a/app/src/organisms/ErrorRecoveryFlows/constants.ts b/app/src/organisms/ErrorRecoveryFlows/constants.ts index 7e9a9ab2a9a..846f7e2efc0 100644 --- a/app/src/organisms/ErrorRecoveryFlows/constants.ts +++ b/app/src/organisms/ErrorRecoveryFlows/constants.ts @@ -1,6 +1,6 @@ import { css } from 'styled-components' -import { SPACING, TYPOGRAPHY } from '@opentrons/components' +import { SPACING, TYPOGRAPHY, RESPONSIVENESS } from '@opentrons/components' import type { StepOrder } from './types' @@ -211,3 +211,14 @@ export const BODY_TEXT_STYLE = css` export const ODD_SECTION_TITLE_STYLE = css` margin-bottom: ${SPACING.spacing16}; ` + +export const ODD_ONLY = css` + @media not (${RESPONSIVENESS.touchscreenMediaQuerySpecs}) { + display: none; + } +` +export const DESKTOP_ONLY = css` + @media (${RESPONSIVENESS.touchscreenMediaQuerySpecs}) { + display: none; + } +` diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/FailedStepNextStep.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/FailedStepNextStep.tsx new file mode 100644 index 00000000000..b29ade0d2eb --- /dev/null +++ b/app/src/organisms/ErrorRecoveryFlows/shared/FailedStepNextStep.tsx @@ -0,0 +1,62 @@ +import * as React from 'react' +import { useTranslation } from 'react-i18next' +import { CategorizedStepContent } from '../../../molecules/InterventionModal' +import type { RecoveryContentProps } from '../types' + +export function FailedStepNextStep({ + stepCounts, + failedCommand, + commandsAfterFailedCommand, + protocolAnalysis, + robotType, +}: Pick< + RecoveryContentProps, + | 'stepCounts' + | 'failedCommand' + | 'commandsAfterFailedCommand' + | 'protocolAnalysis' + | 'robotType' +>): JSX.Element { + const { t } = useTranslation('error_recovery') + + const nthStepAfter = (n: number): number | undefined => + stepCounts.currentStepNumber == null + ? undefined + : stepCounts.currentStepNumber + n + const nthCommand = (n: number): typeof failedCommand => + commandsAfterFailedCommand != null + ? n < commandsAfterFailedCommand.length + ? commandsAfterFailedCommand[n] + : null + : null + + const commandsAfter = [nthCommand(0), nthCommand(1)] as const + + const indexedCommandsAfter = [ + commandsAfter[0] != null + ? { command: commandsAfter[0], index: nthStepAfter(1) } + : null, + commandsAfter[1] != null + ? { command: commandsAfter[1], index: nthStepAfter(2) } + : null, + ] as const + return ( + + ) +} diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/RecoveryContentWrapper.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/RecoveryContentWrapper.tsx index 3c10279d50d..b9acdcc8cae 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/RecoveryContentWrapper.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/RecoveryContentWrapper.tsx @@ -10,19 +10,34 @@ import { Flex, RESPONSIVENESS, } from '@opentrons/components' - import type { StyleProps } from '@opentrons/components' +import { + OneColumn, + TwoColumn, + OneColumnOrTwoColumn, +} from '../../../molecules/InterventionModal' +import { RecoveryFooterButtons } from './RecoveryFooterButtons' -interface SingleColumnContentWrapperProps extends StyleProps { +interface SingleColumnContentWrapperProps { children: React.ReactNode + footerDetails?: React.ComponentProps +} + +interface TwoColumnContentWrapperProps { + children: [React.ReactNode, React.ReactNode] + footerDetails?: React.ComponentProps +} + +interface OneOrTwoColumnContentWrapperProps { + children: [React.ReactNode, React.ReactNode] + footerDetails?: React.ComponentProps } // For flex-direction: column recovery content with one column only. -// -// For ODD use only. -export function RecoveryContentWrapper({ +export function RecoverySingleColumnContentWrapper({ children, + footerDetails, ...styleProps -}: SingleColumnContentWrapperProps): JSX.Element { +}: SingleColumnContentWrapperProps & StyleProps): JSX.Element { return ( - {children} + + {children} + + {footerDetails != null ? ( + + ) : null} + + ) +} + +// For two-column recovery content +export function RecoveryTwoColumnContentWrapper({ + children, + footerDetails, +}: TwoColumnContentWrapperProps): JSX.Element { + const [leftChild, rightChild] = children + return ( + + + {leftChild} + {rightChild} + + {footerDetails != null ? ( + + ) : null} + + ) +} + +// For recovery content with one column on ODD and two columns on desktop +export function RecoveryODDOneDesktopTwoColumnContentWrapper({ + children: [leftOrSingleElement, optionallyShownRightElement], + footerDetails, +}: OneOrTwoColumnContentWrapperProps): JSX.Element { + return ( + + + {leftOrSingleElement} + {optionallyShownRightElement} + + {footerDetails != null ? ( + + ) : null} ) } @@ -38,8 +104,8 @@ export function RecoveryContentWrapper({ const STYLE = css` gap: ${SPACING.spacing24}; width: 100%; + height: 100%; @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { gap: none; - height: 100%; } ` diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/RecoveryRadioGroup.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/RecoveryRadioGroup.tsx new file mode 100644 index 00000000000..571f0b0333a --- /dev/null +++ b/app/src/organisms/ErrorRecoveryFlows/shared/RecoveryRadioGroup.tsx @@ -0,0 +1,42 @@ +import * as React from 'react' + +import type { ChangeEventHandler } from 'react' +import { RadioGroup, SPACING, Flex } from '@opentrons/components' + +// note: this typescript stuff is so that e.currentTarget.value in the ChangeEventHandler +// is deduced to a union of the values of the options passed to the radiogroup rather than +// just string +export interface Target extends Omit { + value: T +} + +export type Options = Array<{ + value: T + children: React.ReactNode +}> + +export interface RecoveryRadioGroupProps + extends Omit< + React.ComponentProps, + 'labelTextClassName' | 'options' | 'onchange' + > { + options: Options + onChange: ChangeEventHandler> +} + +export function RecoveryRadioGroup( + props: RecoveryRadioGroupProps +): JSX.Element { + return ( + ({ + name: '', + value: radioOption.value, + children: ( + {radioOption.children} + ), + }))} + /> + ) +} diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/ReplaceTips.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/ReplaceTips.tsx index e577abb7bb1..f7513af14c8 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/ReplaceTips.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/ReplaceTips.tsx @@ -3,7 +3,7 @@ import * as React from 'react' import { Flex } from '@opentrons/components' import { useTranslation } from 'react-i18next' -import { RecoveryContentWrapper } from './RecoveryContentWrapper' +import { RecoverySingleColumnContentWrapper } from './RecoveryContentWrapper' import { TwoColumn, DeckMapContent } from '../../../molecules/InterventionModal' import { RecoveryFooterButtons } from './RecoveryFooterButtons' import { LeftColumnLabwareInfo } from './LeftColumnLabwareInfo' @@ -36,7 +36,7 @@ export function ReplaceTips(props: RecoveryContentProps): JSX.Element | null { } return ( - + - + ) } diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/SelectTips.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/SelectTips.tsx index f51d9d2ddd6..0b6f66aa484 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/SelectTips.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/SelectTips.tsx @@ -2,7 +2,7 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' import { RECOVERY_MAP } from '../constants' -import { RecoveryContentWrapper } from './RecoveryContentWrapper' +import { RecoverySingleColumnContentWrapper } from './RecoveryContentWrapper' import { TwoColumn } from '../../../molecules/InterventionModal' import { RecoveryFooterButtons } from './RecoveryFooterButtons' import { LeftColumnLabwareInfo } from './LeftColumnLabwareInfo' @@ -42,7 +42,7 @@ export function SelectTips(props: RecoveryContentProps): JSX.Element | null { toggleModal={toggleModal} /> )} - + - + ) } diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/TwoColTextAndFailedStepNextStep.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/TwoColTextAndFailedStepNextStep.tsx index a9bdd50399f..4ed62e8ff8d 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/TwoColTextAndFailedStepNextStep.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/TwoColTextAndFailedStepNextStep.tsx @@ -1,5 +1,4 @@ import * as React from 'react' -import { useTranslation } from 'react-i18next' import { css } from 'styled-components' import { DIRECTION_COLUMN, @@ -9,12 +8,10 @@ import { RESPONSIVENESS, } from '@opentrons/components' -import { RecoveryContentWrapper } from './RecoveryContentWrapper' -import { - TwoColumn, - CategorizedStepContent, -} from '../../../molecules/InterventionModal' +import { RecoverySingleColumnContentWrapper } from './RecoveryContentWrapper' +import { TwoColumn } from '../../../molecules/InterventionModal' import { RecoveryFooterButtons } from './RecoveryFooterButtons' +import { FailedStepNextStep } from './FailedStepNextStep' import type { RecoveryContentProps } from '../types' @@ -24,60 +21,34 @@ type TwoColTextAndFailedStepNextStepProps = RecoveryContentProps & { primaryBtnCopy: string primaryBtnOnClick: () => void secondaryBtnOnClickOverride?: () => void - secondaryBtnOnClickCopyOverride?: string } /** * Left Column: Title + body text * Right column: FailedStepNextStep */ -export function TwoColTextAndFailedStepNextStep({ - leftColBodyText, - leftColTitle, - primaryBtnCopy, - primaryBtnOnClick, - secondaryBtnOnClickOverride, - secondaryBtnOnClickCopyOverride, - routeUpdateActions, - failedCommand, - stepCounts, - commandsAfterFailedCommand, - protocolAnalysis, - robotType, -}: TwoColTextAndFailedStepNextStepProps): JSX.Element | null { +export function TwoColTextAndFailedStepNextStep( + props: TwoColTextAndFailedStepNextStepProps +): JSX.Element | null { + const { + leftColBodyText, + leftColTitle, + primaryBtnCopy, + primaryBtnOnClick, + secondaryBtnOnClickOverride, + routeUpdateActions, + } = props const { goBackPrevStep } = routeUpdateActions - const { t } = useTranslation('error_recovery') - const nthStepAfter = (n: number): number | undefined => - stepCounts.currentStepNumber == null - ? undefined - : stepCounts.currentStepNumber + n - const nthCommand = (n: number): typeof failedCommand => - commandsAfterFailedCommand != null - ? n < commandsAfterFailedCommand.length - ? commandsAfterFailedCommand[n] - : null - : null - - const commandsAfter = [nthCommand(0), nthCommand(1)] as const - - const indexedCommandsAfter = [ - commandsAfter[0] != null - ? { command: commandsAfter[0], index: nthStepAfter(1) } - : null, - commandsAfter[1] != null - ? { command: commandsAfter[1], index: nthStepAfter(2) } - : null, - ] as const return ( - + @@ -94,29 +65,13 @@ export function TwoColTextAndFailedStepNextStep({ {leftColBodyText} - + - + ) } diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/StepInfo.test.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/StepInfo.test.tsx index 54e579daf93..4e7e8b393fa 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/StepInfo.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/StepInfo.test.tsx @@ -3,7 +3,7 @@ import { describe, it, expect, beforeEach, vi } from 'vitest' import { screen } from '@testing-library/react' import { renderWithProviders } from '../../../../__testing-utils__' -import { mockRecoveryContentProps } from '../../__fixtures__' +import { mockRecoveryContentProps, mockFailedCommand } from '../../__fixtures__' import { i18n } from '../../../../i18n' import { StepInfo } from '../StepInfo' import { CommandText } from '../../../../molecules/Command' @@ -21,7 +21,10 @@ describe('StepInfo', () => { beforeEach(() => { props = { - ...mockRecoveryContentProps, + ...{ + ...mockRecoveryContentProps, + protocolAnalysis: { commands: [mockFailedCommand] } as any, + }, textStyle: 'h4', stepCounts: { currentStepNumber: 5, diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/index.ts b/app/src/organisms/ErrorRecoveryFlows/shared/index.ts index 33c9299db44..955058e5311 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/index.ts +++ b/app/src/organisms/ErrorRecoveryFlows/shared/index.ts @@ -1,5 +1,9 @@ export { RecoveryFooterButtons } from './RecoveryFooterButtons' -export { RecoveryContentWrapper } from './RecoveryContentWrapper' +export { + RecoverySingleColumnContentWrapper, + RecoveryTwoColumnContentWrapper, + RecoveryODDOneDesktopTwoColumnContentWrapper, +} from './RecoveryContentWrapper' export { ReplaceTips } from './ReplaceTips' export { SelectTips } from './SelectTips' export { TwoColTextAndFailedStepNextStep } from './TwoColTextAndFailedStepNextStep' @@ -9,5 +13,7 @@ export { TipSelectionModal } from './TipSelectionModal' export { StepInfo } from './StepInfo' export { useErrorDetailsModal, ErrorDetailsModal } from './ErrorDetailsModal' export { RecoveryInterventionModal } from './RecoveryInterventionModal' +export { FailedStepNextStep } from './FailedStepNextStep' +export { RecoveryRadioGroup } from './RecoveryRadioGroup' export type { RecoveryInterventionModalProps } from './RecoveryInterventionModal' diff --git a/components/src/forms/RadioGroup.tsx b/components/src/forms/RadioGroup.tsx index d934616a227..5d409540032 100644 --- a/components/src/forms/RadioGroup.tsx +++ b/components/src/forms/RadioGroup.tsx @@ -50,7 +50,7 @@ export function RadioGroup(props: RadioGroupProps): JSX.Element { const useStyleUpdates = props.useBlueChecked && radio.value === props.value return ( -
diff --git a/app/src/pages/InstrumentDetail/__tests__/InstrumentDetail.test.tsx b/app/src/pages/InstrumentDetail/__tests__/InstrumentDetail.test.tsx index af9489f2c6c..6b92a5ab9be 100644 --- a/app/src/pages/InstrumentDetail/__tests__/InstrumentDetail.test.tsx +++ b/app/src/pages/InstrumentDetail/__tests__/InstrumentDetail.test.tsx @@ -19,7 +19,7 @@ import type { Instruments } from '@opentrons/api-client' vi.mock('@opentrons/react-api-client') vi.mock('react-router-dom', () => ({ useParams: vi.fn(), - useHistory: vi.fn(), + useNavigate: vi.fn(), })) vi.mock('../../../resources/instruments/hooks') vi.mock('../../../resources/robot-settings/hooks') diff --git a/app/src/pages/InstrumentsDashboard/__tests__/InstrumentsDashboard.test.tsx b/app/src/pages/InstrumentsDashboard/__tests__/InstrumentsDashboard.test.tsx index 06e040bbe39..18b6d2cfb9c 100644 --- a/app/src/pages/InstrumentsDashboard/__tests__/InstrumentsDashboard.test.tsx +++ b/app/src/pages/InstrumentsDashboard/__tests__/InstrumentsDashboard.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { Route, MemoryRouter } from 'react-router-dom' +import { Route, MemoryRouter, Routes } from 'react-router-dom' import { fireEvent, screen } from '@testing-library/react' import { renderWithProviders } from '../../../__testing-utils__' import { vi, describe, it, afterEach, beforeEach, expect } from 'vitest' @@ -93,18 +93,16 @@ vi.mock('../../../organisms/PipetteWizardFlows') vi.mock('../../../organisms/PipetteWizardFlows/ChoosePipette') vi.mock('../../../organisms/Navigation') -const render = () => { +const render = (path = '/') => { return renderWithProviders( - - - - - - - + + + } /> + } /> + , { i18nInstance: i18n } - ) + )[0] } describe('InstrumentsDashboard', () => { @@ -119,7 +117,7 @@ describe('InstrumentsDashboard', () => { vi.resetAllMocks() }) it('should render mount info for all attached mounts', () => { - render() + render('/instruments') screen.getByText('left Mount') screen.getByText('Flex 1-Channel 1000 μL') screen.getByText('right Mount') @@ -128,7 +126,7 @@ describe('InstrumentsDashboard', () => { screen.getByText('Flex Gripper') }) it('should route to left mount detail when instrument attached and clicked', () => { - render() + render('/instruments') fireEvent.click(screen.getByText('left Mount')) screen.getByText('serial number') screen.getByText(mockLeftPipetteData.serialNumber) @@ -139,7 +137,7 @@ describe('InstrumentsDashboard', () => { ) }) it('should route to right mount detail when instrument attached and clicked', () => { - render() + render('/instruments') fireEvent.click(screen.getByText('right Mount')) screen.getByText('serial number') screen.getByText(mockRightPipetteData.serialNumber) @@ -150,7 +148,7 @@ describe('InstrumentsDashboard', () => { ) }) it('should route to extension mount detail when instrument attached and clicked', () => { - render() + render('/instruments') fireEvent.click(screen.getByText('extension Mount')) screen.getByText('serial number') screen.getByText(mockGripperData.serialNumber) @@ -159,7 +157,7 @@ describe('InstrumentsDashboard', () => { vi.mocked(useInstrumentsQuery).mockReturnValue({ data: { data: [] }, } as any) - render() + render('/instruments') fireEvent.click(screen.getByText('left Mount')) expect(vi.mocked(ChoosePipette)).toHaveBeenCalled() }) @@ -167,7 +165,7 @@ describe('InstrumentsDashboard', () => { vi.mocked(useInstrumentsQuery).mockReturnValue({ data: { data: [] }, } as any) - render() + render('/instruments') fireEvent.click(screen.getByText('right Mount')) expect(vi.mocked(ChoosePipette)).toHaveBeenCalled() }) @@ -175,7 +173,7 @@ describe('InstrumentsDashboard', () => { vi.mocked(useInstrumentsQuery).mockReturnValue({ data: { data: [] }, } as any) - render() + render('/instruments') fireEvent.click(screen.getByText('extension Mount')) expect(vi.mocked(GripperWizardFlows)).toHaveBeenCalled() }) @@ -185,7 +183,7 @@ describe('InstrumentsDashboard', () => { data: [mock96ChannelData, mockGripperData], }, } as any) - render() + render('/instruments') screen.getByText('Left+Right Mounts') screen.getByText('extension Mount') }) diff --git a/app/src/pages/NameRobot/__tests__/NameRobot.test.tsx b/app/src/pages/NameRobot/__tests__/NameRobot.test.tsx index c7b9b546645..a91c108a527 100644 --- a/app/src/pages/NameRobot/__tests__/NameRobot.test.tsx +++ b/app/src/pages/NameRobot/__tests__/NameRobot.test.tsx @@ -19,20 +19,20 @@ import { } from '../../../redux/discovery/__fixtures__' import { NameRobot } from '..' -import type * as ReactRouterDom from 'react-router-dom' +import type { NavigateFunction } from 'react-router-dom' vi.mock('../../../redux/discovery/selectors') vi.mock('../../../redux/config') vi.mock('../../../redux/analytics') vi.mock('../../../organisms/RobotSettingsDashboard/NetworkSettings/hooks') -const mockPush = vi.fn() +const mockNavigate = vi.fn() vi.mock('react-router-dom', async importOriginal => { - const actual = await importOriginal() + const actual = await importOriginal() return { ...actual, - useHistory: () => ({ push: mockPush } as any), + useNavigate: () => mockNavigate, } }) @@ -142,6 +142,6 @@ describe('NameRobot', () => { vi.mocked(useIsUnboxingFlowOngoing).mockReturnValue(false) render() fireEvent.click(screen.getByTestId('name_back_button')) - expect(mockPush).toHaveBeenCalledWith('/robot-settings') + expect(mockNavigate).toHaveBeenCalledWith('/robot-settings') }) }) diff --git a/app/src/pages/NameRobot/index.tsx b/app/src/pages/NameRobot/index.tsx index eeed5b819e5..bd6f2180853 100644 --- a/app/src/pages/NameRobot/index.tsx +++ b/app/src/pages/NameRobot/index.tsx @@ -2,7 +2,7 @@ import * as React from 'react' import { Controller, useForm } from 'react-hook-form' import { useTranslation } from 'react-i18next' import { useSelector, useDispatch } from 'react-redux' -import { useHistory } from 'react-router-dom' +import { useNavigate } from 'react-router-dom' import { ALIGN_CENTER, @@ -47,7 +47,7 @@ interface FormValues { export function NameRobot(): JSX.Element { const { t } = useTranslation(['device_settings', 'shared']) - const history = useHistory() + const navigate = useNavigate() const trackEvent = useTrackEvent() const localRobot = useSelector(getLocalRobot) const ipAddress = localRobot?.ip @@ -143,7 +143,7 @@ export function NameRobot(): JSX.Element { if (data.name != null) { setNewName(data.name) if (!isUnboxingFlowOngoing) { - history.push('/robot-settings') + navigate('/robot-settings') } else { setIsShowConfirmRobotName(true) } @@ -198,9 +198,9 @@ export function NameRobot(): JSX.Element { data-testid="name_back_button" onClick={() => { if (isUnboxingFlowOngoing) { - history.push('/emergency-stop') + navigate('/emergency-stop') } else { - history.push('/robot-settings') + navigate('/robot-settings') } }} > diff --git a/app/src/pages/NetworkSetupMenu/__tests__/NetworkSetupMenu.test.tsx b/app/src/pages/NetworkSetupMenu/__tests__/NetworkSetupMenu.test.tsx index f2febd1a32d..73f9312cc3c 100644 --- a/app/src/pages/NetworkSetupMenu/__tests__/NetworkSetupMenu.test.tsx +++ b/app/src/pages/NetworkSetupMenu/__tests__/NetworkSetupMenu.test.tsx @@ -6,15 +6,15 @@ import { renderWithProviders } from '../../../__testing-utils__' import { i18n } from '../../../i18n' import { NetworkSetupMenu } from '..' -import type * as ReactRouterDom from 'react-router-dom' +import type { NavigateFunction } from 'react-router-dom' -const mockPush = vi.fn() +const mockNavigate = vi.fn() vi.mock('react-router-dom', async importOriginal => { - const actual = await importOriginal() + const actual = await importOriginal() return { ...actual, - useHistory: () => ({ push: mockPush } as any), + useNavigate: () => mockNavigate, } }) @@ -53,10 +53,10 @@ describe('NetworkSetupMenu', () => { const ethernetButton = screen.getByText('Ethernet') const usbButton = screen.getByText('USB') fireEvent.click(wifiButton) - expect(mockPush).toHaveBeenCalledWith('/network-setup/wifi') + expect(mockNavigate).toHaveBeenCalledWith('/network-setup/wifi') fireEvent.click(ethernetButton) - expect(mockPush).toHaveBeenCalledWith('/network-setup/ethernet') + expect(mockNavigate).toHaveBeenCalledWith('/network-setup/ethernet') fireEvent.click(usbButton) - expect(mockPush).toHaveBeenCalledWith('/network-setup/usb') + expect(mockNavigate).toHaveBeenCalledWith('/network-setup/usb') }) }) diff --git a/app/src/pages/ProtocolDashboard/LongPressModal.tsx b/app/src/pages/ProtocolDashboard/LongPressModal.tsx index a3287360b42..535b754259e 100644 --- a/app/src/pages/ProtocolDashboard/LongPressModal.tsx +++ b/app/src/pages/ProtocolDashboard/LongPressModal.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { useDispatch, useSelector } from 'react-redux' -import { useHistory } from 'react-router-dom' +import { useNavigate } from 'react-router-dom' import { useTranslation } from 'react-i18next' import { Flex, Icon, SPACING, LegacyStyledText } from '@opentrons/components' import { useCreateRunMutation } from '@opentrons/react-api-client' @@ -28,7 +28,7 @@ export function LongPressModal({ setShowDeleteConfirmationModal, setTargetProtocolId, }: LongPressModalProps): JSX.Element { - const history = useHistory() + const navigate = useNavigate() let pinnedProtocolIds = useSelector(getPinnedProtocolIds) ?? [] const { i18n, t } = useTranslation(['protocol_info', 'shared']) const dispatch = useDispatch() @@ -49,7 +49,7 @@ export function LongPressModal({ const createRunUse = useCreateRunMutation({ onSuccess: data => { const runId: string = data.data.id - history.push(`/runs/${runId}/setup`) + navigate(`/runs/${runId}/setup`) }, }) const createRun = diff --git a/app/src/pages/ProtocolDashboard/PinnedProtocol.tsx b/app/src/pages/ProtocolDashboard/PinnedProtocol.tsx index d5e4a8c7347..6824a67d232 100644 --- a/app/src/pages/ProtocolDashboard/PinnedProtocol.tsx +++ b/app/src/pages/ProtocolDashboard/PinnedProtocol.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' -import { useHistory } from 'react-router-dom' +import { useNavigate } from 'react-router-dom' import { formatDistance } from 'date-fns' import styled, { css } from 'styled-components' @@ -83,7 +83,7 @@ export function PinnedProtocol(props: PinnedProtocolProps): JSX.Element { isRequiredCSV = false, } = props const cardSize = size ?? 'full' - const history = useHistory() + const navigate = useNavigate() const longpress = useLongPress() const protocolName = protocol.metadata.protocolName ?? protocol.files[0].name const { t } = useTranslation('protocol_info') @@ -96,7 +96,7 @@ export function PinnedProtocol(props: PinnedProtocolProps): JSX.Element { protocolId: string ): void => { if (!longpress.isLongPressed) { - history.push(`/protocols/${protocolId}`) + navigate(`/protocols/${protocolId}`) } } React.useEffect(() => { diff --git a/app/src/pages/ProtocolDashboard/ProtocolCard.tsx b/app/src/pages/ProtocolDashboard/ProtocolCard.tsx index a210a46139b..f6acd4ed098 100644 --- a/app/src/pages/ProtocolDashboard/ProtocolCard.tsx +++ b/app/src/pages/ProtocolDashboard/ProtocolCard.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { useHistory } from 'react-router-dom' +import { useNavigate } from 'react-router-dom' import { Trans, useTranslation } from 'react-i18next' import { useQueryClient } from 'react-query' import { formatDistance } from 'date-fns' @@ -59,7 +59,7 @@ export function ProtocolCard(props: ProtocolCardProps): JSX.Element { setTargetProtocolId, setIsRequiredCSV, } = props - const history = useHistory() + const navigate = useNavigate() const [showIcon, setShowIcon] = React.useState(false) const [ showFailedAnalysisModal, @@ -120,7 +120,7 @@ export function ProtocolCard(props: ProtocolCardProps): JSX.Element { if (isFailedAnalysis) { setShowFailedAnalysisModal(true) } else if (!longpress.isLongPressed) { - history.push(`/protocols/${protocolId}`) + navigate(`/protocols/${protocolId}`) } } diff --git a/app/src/pages/ProtocolDashboard/__tests__/PinnedProtocol.test.tsx b/app/src/pages/ProtocolDashboard/__tests__/PinnedProtocol.test.tsx index e1e2125c88a..15cc38baefe 100644 --- a/app/src/pages/ProtocolDashboard/__tests__/PinnedProtocol.test.tsx +++ b/app/src/pages/ProtocolDashboard/__tests__/PinnedProtocol.test.tsx @@ -12,15 +12,15 @@ import { PinnedProtocol } from '../PinnedProtocol' import type { Chip } from '@opentrons/components' import type { ProtocolResource } from '@opentrons/shared-data' -import type * as ReactRouterDom from 'react-router-dom' +import type { NavigateFunction } from 'react-router-dom' -const mockPush = vi.fn() +const mockNavigate = vi.fn() vi.mock('react-router-dom', async importOriginal => { - const actual = await importOriginal() + const actual = await importOriginal() return { ...actual, - useHistory: () => ({ push: mockPush } as any), + useNavigate: () => mockNavigate, } }) vi.mock('@opentrons/components', async importOriginal => { @@ -119,7 +119,7 @@ describe('Pinned Protocol', () => { render(props) const name = screen.getByText('yay mock protocol') fireEvent.click(name) - expect(mockPush).toHaveBeenCalledWith('/protocols/mockProtocol1') + expect(mockNavigate).toHaveBeenCalledWith('/protocols/mockProtocol1') }) it('should display modal after long click', async () => { diff --git a/app/src/pages/ProtocolDashboard/__tests__/ProtocolCard.test.tsx b/app/src/pages/ProtocolDashboard/__tests__/ProtocolCard.test.tsx index 6ad885d780e..0e792cb8d70 100644 --- a/app/src/pages/ProtocolDashboard/__tests__/ProtocolCard.test.tsx +++ b/app/src/pages/ProtocolDashboard/__tests__/ProtocolCard.test.tsx @@ -14,7 +14,7 @@ import { i18n } from '../../../i18n' import { useFeatureFlag } from '../../../redux/config' import { ProtocolCard } from '../ProtocolCard' -import type * as ReactRouterDom from 'react-router-dom' +import type { NavigateFunction } from 'react-router-dom' import type { UseQueryResult } from 'react-query' import type { CompletedProtocolAnalysis, @@ -22,13 +22,13 @@ import type { } from '@opentrons/shared-data' import type { Chip } from '@opentrons/components' -const mockPush = vi.fn() +const mockNavigate = vi.fn() vi.mock('react-router-dom', async importOriginal => { - const actual = await importOriginal() + const actual = await importOriginal() return { ...actual, - useHistory: () => ({ push: mockPush } as any), + useNavigate: () => mockNavigate, } }) vi.mock('@opentrons/react-api-client') @@ -115,7 +115,7 @@ describe('ProtocolCard', () => { const card = screen.getByTestId('protocol_card') expect(card).toHaveStyle(`background-color: ${COLORS.grey35}`) fireEvent.click(name) - expect(mockPush).toHaveBeenCalledWith('/protocols/mockProtocol1') + expect(mockNavigate).toHaveBeenCalledWith('/protocols/mockProtocol1') }) it('should display the analysis failed error modal when clicking on the protocol', () => { diff --git a/app/src/pages/ProtocolDetails/__tests__/ProtocolDetails.test.tsx b/app/src/pages/ProtocolDetails/__tests__/ProtocolDetails.test.tsx index d4166d6a004..443677f7ae6 100644 --- a/app/src/pages/ProtocolDetails/__tests__/ProtocolDetails.test.tsx +++ b/app/src/pages/ProtocolDetails/__tests__/ProtocolDetails.test.tsx @@ -2,7 +2,7 @@ import * as React from 'react' import { vi, it, describe, expect, beforeEach, afterEach } from 'vitest' import { fireEvent, screen, waitFor } from '@testing-library/react' import { when } from 'vitest-when' -import { Route, MemoryRouter } from 'react-router-dom' +import { Route, MemoryRouter, Routes } from 'react-router-dom' import '@testing-library/jest-dom/vitest' import { renderWithProviders } from '../../../__testing-utils__' import { deleteProtocol, deleteRun, getProtocol } from '@opentrons/api-client' @@ -81,9 +81,9 @@ const MOCK_DATA = { const render = (path = '/protocols/fakeProtocolId') => { return renderWithProviders( - - - + + } /> + , { i18nInstance: i18n, diff --git a/app/src/pages/ProtocolDetails/index.tsx b/app/src/pages/ProtocolDetails/index.tsx index 6567f15e91b..cb7d979ea6f 100644 --- a/app/src/pages/ProtocolDetails/index.tsx +++ b/app/src/pages/ProtocolDetails/index.tsx @@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next' import { useQueryClient } from 'react-query' import { deleteProtocol, deleteRun, getProtocol } from '@opentrons/api-client' import { useDispatch, useSelector } from 'react-redux' -import { useHistory, useParams } from 'react-router-dom' +import { useNavigate, useParams } from 'react-router-dom' import { ALIGN_CENTER, BORDERS, @@ -80,7 +80,7 @@ const ProtocolHeader = ({ isScrolled, isProtocolFetching, }: ProtocolHeaderProps): JSX.Element => { - const history = useHistory() + const navigate = useNavigate() const { t } = useTranslation(['protocol_info, protocol_details', 'shared']) const [truncate, setTruncate] = React.useState(true) const [startSetup, setStartSetup] = React.useState(false) @@ -115,7 +115,7 @@ const ProtocolHeader = ({ paddingLeft="0rem" paddingRight={SPACING.spacing24} onClick={() => { - history.push('/protocols') + navigate('/protocols') }} width="3rem" > @@ -309,7 +309,9 @@ export function ProtocolDetails(): JSX.Element | null { 'shared', ]) const enableCsvFile = useFeatureFlag('enableCsvFile') - const { protocolId } = useParams() + const { protocolId } = useParams< + keyof OnDeviceRouteParams + >() as OnDeviceRouteParams const { missingProtocolHardware, conflictedSlots, @@ -318,7 +320,7 @@ export function ProtocolDetails(): JSX.Element | null { const runTimeParameters = useRunTimeParameters(protocolId) const dispatch = useDispatch() - const history = useHistory() + const navigate = useNavigate() const host = useHost() const { makeSnackbar } = useToaster() const [showParameters, setShowParameters] = React.useState(false) @@ -426,11 +428,11 @@ export function ProtocolDetails(): JSX.Element | null { ) .then(() => deleteProtocol(host, protocolId)) .then(() => { - history.push('/protocols') + navigate('/protocols') }) .catch((e: Error) => { console.error(`error deleting resources: ${e.message}`) - history.push('/protocols') + navigate('/protocols') }) } else { console.error( diff --git a/app/src/pages/ProtocolSetup/__tests__/ProtocolSetup.test.tsx b/app/src/pages/ProtocolSetup/__tests__/ProtocolSetup.test.tsx index 30cfe51c947..1be58ae82f8 100644 --- a/app/src/pages/ProtocolSetup/__tests__/ProtocolSetup.test.tsx +++ b/app/src/pages/ProtocolSetup/__tests__/ProtocolSetup.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { Route, MemoryRouter } from 'react-router-dom' +import { Route, MemoryRouter, Routes } from 'react-router-dom' import { fireEvent, screen } from '@testing-library/react' import { when } from 'vitest-when' import { vi, it, describe, expect, beforeEach, afterEach } from 'vitest' @@ -60,7 +60,7 @@ import { useNotifyDeckConfigurationQuery } from '../../../resources/deck_configu import type { UseQueryResult } from 'react-query' import type * as SharedData from '@opentrons/shared-data' -import type * as ReactRouterDom from 'react-router-dom' +import type { NavigateFunction } from 'react-router-dom' // Mock IntersectionObserver class IntersectionObserver { observe = vi.fn() @@ -74,7 +74,7 @@ Object.defineProperty(window, 'IntersectionObserver', { value: IntersectionObserver, }) -let mockHistoryPush = vi.fn() +let mockNavigate = vi.fn() vi.mock('@opentrons/shared-data', async importOriginal => { const sharedData = await importOriginal() @@ -85,12 +85,10 @@ vi.mock('@opentrons/shared-data', async importOriginal => { }) vi.mock('react-router-dom', async importOriginal => { - const reactRouterDom = await importOriginal() + const reactRouterDom = await importOriginal() return { ...reactRouterDom, - useHistory: () => ({ - push: mockHistoryPush, - }), + useNavigate: () => mockNavigate, } }) @@ -118,9 +116,9 @@ vi.mock('../../../resources/deck_configuration') const render = (path = '/') => { return renderWithProviders( - - - + + } /> + , { i18nInstance: i18n, @@ -193,7 +191,7 @@ describe('ProtocolSetup', () => { let mockLaunchLPC = vi.fn() beforeEach(() => { mockLaunchLPC = vi.fn() - mockHistoryPush = vi.fn() + mockNavigate = vi.fn() vi.mocked(useLPCDisabledReason).mockReturnValue(null) vi.mocked(useAttachedModules).mockReturnValue([]) vi.mocked(useModuleCalibrationStatus).mockReturnValue({ complete: true }) @@ -430,6 +428,6 @@ describe('ProtocolSetup', () => { it('should redirect to the protocols page when a run is stopped', () => { vi.mocked(useRunStatus).mockReturnValue(RUN_STATUS_STOPPED) render(`/runs/${RUN_ID}/setup/`) - expect(mockHistoryPush).toHaveBeenCalledWith('/protocols') + expect(mockNavigate).toHaveBeenCalledWith('/protocols') }) }) diff --git a/app/src/pages/ProtocolSetup/index.tsx b/app/src/pages/ProtocolSetup/index.tsx index 02ab6990070..8954e7d0b01 100644 --- a/app/src/pages/ProtocolSetup/index.tsx +++ b/app/src/pages/ProtocolSetup/index.tsx @@ -2,7 +2,7 @@ import * as React from 'react' import last from 'lodash/last' import { useSelector } from 'react-redux' import { useTranslation } from 'react-i18next' -import { useHistory, useParams } from 'react-router-dom' +import { useNavigate, useParams } from 'react-router-dom' import first from 'lodash/first' import { css } from 'styled-components' @@ -282,7 +282,7 @@ function PrepareToRun({ runRecord, }: PrepareToRunProps): JSX.Element { const { t, i18n } = useTranslation(['protocol_setup', 'shared']) - const history = useHistory() + const navigate = useNavigate() const { makeSnackbar } = useToaster() const scrollRef = React.useRef(null) const [isScrolled, setIsScrolled] = React.useState(false) @@ -323,7 +323,7 @@ function PrepareToRun({ const runStatus = useRunStatus(runId) if (runStatus === RUN_STATUS_STOPPED) { - history.push('/protocols') + navigate('/protocols') } React.useEffect(() => { @@ -339,7 +339,7 @@ function PrepareToRun({ const onConfirmCancelClose = (): void => { setShowConfirmCancelModal(false) - history.goBack() + navigate(-1) } const protocolHasModules = @@ -832,7 +832,9 @@ export type SetupScreens = | 'view only parameters' export function ProtocolSetup(): JSX.Element { - const { runId } = useParams() + const { runId } = useParams< + keyof OnDeviceRouteParams + >() as OnDeviceRouteParams const { data: runRecord } = useNotifyRunQuery(runId, { staleTime: Infinity }) const { analysisErrors } = useProtocolAnalysisErrors(runId) const localRobot = useSelector(getLocalRobot) diff --git a/app/src/pages/Protocols/ProtocolDetails/ProtocolTimeline.tsx b/app/src/pages/Protocols/ProtocolDetails/ProtocolTimeline.tsx index e537b8b4b76..b8511ad4f87 100644 --- a/app/src/pages/Protocols/ProtocolDetails/ProtocolTimeline.tsx +++ b/app/src/pages/Protocols/ProtocolDetails/ProtocolTimeline.tsx @@ -12,7 +12,9 @@ import type { Dispatch, State } from '../../../redux/types' import type { DesktopRouteParams } from '../../../App/types' export function ProtocolTimeline(): JSX.Element { - const { protocolKey } = useParams() + const { protocolKey } = useParams< + keyof DesktopRouteParams + >() as DesktopRouteParams const dispatch = useDispatch() const storedProtocol = useSelector((state: State) => getStoredProtocol(state, protocolKey) diff --git a/app/src/pages/Protocols/ProtocolDetails/__tests__/ProtocolDetails.test.tsx b/app/src/pages/Protocols/ProtocolDetails/__tests__/ProtocolDetails.test.tsx index bb0a4d8f5e4..27e728dd844 100644 --- a/app/src/pages/Protocols/ProtocolDetails/__tests__/ProtocolDetails.test.tsx +++ b/app/src/pages/Protocols/ProtocolDetails/__tests__/ProtocolDetails.test.tsx @@ -1,7 +1,7 @@ import * as React from 'react' import { vi, it, describe, expect, beforeEach, afterEach } from 'vitest' import { screen } from '@testing-library/react' -import { Route, MemoryRouter } from 'react-router-dom' +import { Route, MemoryRouter, Routes } from 'react-router-dom' import { when } from 'vitest-when' import { renderWithProviders } from '../../../../__testing-utils__' @@ -34,12 +34,10 @@ const MOCK_STATE: State = { const render = (path = '/') => { return renderWithProviders( - - - - -
protocols
-
+ + } /> + protocols} /> +
, { i18nInstance: i18n, diff --git a/app/src/pages/Protocols/ProtocolDetails/index.tsx b/app/src/pages/Protocols/ProtocolDetails/index.tsx index dc834092c5f..a75e3457540 100644 --- a/app/src/pages/Protocols/ProtocolDetails/index.tsx +++ b/app/src/pages/Protocols/ProtocolDetails/index.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { useParams, Redirect } from 'react-router-dom' +import { useParams, Navigate } from 'react-router-dom' import { useDispatch, useSelector } from 'react-redux' import { @@ -12,7 +12,9 @@ import type { Dispatch, State } from '../../../redux/types' import type { DesktopRouteParams } from '../../../App/types' export function ProtocolDetails(): JSX.Element { - const { protocolKey } = useParams() + const { protocolKey } = useParams< + keyof DesktopRouteParams + >() as DesktopRouteParams const dispatch = useDispatch() const storedProtocol = useSelector((state: State) => @@ -26,6 +28,6 @@ export function ProtocolDetails(): JSX.Element { return storedProtocol != null ? ( ) : ( - + ) } diff --git a/app/src/pages/QuickTransferDashboard/DeleteTransferConfirmationModal.tsx b/app/src/pages/QuickTransferDashboard/DeleteTransferConfirmationModal.tsx index 9916a576c1f..3bf006598b1 100644 --- a/app/src/pages/QuickTransferDashboard/DeleteTransferConfirmationModal.tsx +++ b/app/src/pages/QuickTransferDashboard/DeleteTransferConfirmationModal.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { useHistory } from 'react-router-dom' +import { useNavigate } from 'react-router-dom' import { useQueryClient } from 'react-query' import { useTranslation } from 'react-i18next' import styled from 'styled-components' @@ -35,7 +35,7 @@ export function DeleteTransferConfirmationModal({ setShowDeleteConfirmationModal, }: DeleteTransferConfirmationModalProps): JSX.Element { const { i18n, t } = useTranslation(['quick_transfer', 'shared']) - const history = useHistory() + const navigate = useNavigate() const { makeSnackbar } = useToaster() const [showIcon, setShowIcon] = React.useState(false) const modalHeader: ModalHeaderBaseProps = { @@ -77,11 +77,11 @@ export function DeleteTransferConfirmationModal({ .then(() => { setShowIcon(false) setShowDeleteConfirmationModal(false) - history.push('/quick-transfer') + navigate('/quick-transfer') makeSnackbar(t('deleted_transfer') as string) }) .catch((e: Error) => { - history.push('/quick-transfer') + navigate('/quick-transfer') console.error(`error deleting resources: ${e.message}`) }) } else { diff --git a/app/src/pages/QuickTransferDashboard/LongPressModal.tsx b/app/src/pages/QuickTransferDashboard/LongPressModal.tsx index 0cdc777287d..b856d1296cf 100644 --- a/app/src/pages/QuickTransferDashboard/LongPressModal.tsx +++ b/app/src/pages/QuickTransferDashboard/LongPressModal.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { useDispatch, useSelector } from 'react-redux' -import { useHistory } from 'react-router-dom' +import { useNavigate } from 'react-router-dom' import { useTranslation } from 'react-i18next' import { Flex, Icon, SPACING, LegacyStyledText } from '@opentrons/components' import { useCreateRunMutation } from '@opentrons/react-api-client' @@ -31,7 +31,7 @@ export function LongPressModal({ setShowDeleteConfirmationModal, setTargetTransferId, }: LongPressModalProps): JSX.Element { - const history = useHistory() + const navigate = useNavigate() let pinnedQuickTransferIds = useSelector(getPinnedQuickTransferIds) ?? [] const { i18n, t } = useTranslation(['quick_transfer', 'shared']) const dispatch = useDispatch() @@ -44,7 +44,7 @@ export function LongPressModal({ const { createRun } = useCreateRunMutation({ onSuccess: data => { const runId: string = data.data.id - history.push(`/runs/${runId}/setup`) + navigate(`/runs/${runId}/setup`) }, }) diff --git a/app/src/pages/QuickTransferDashboard/PinnedTransfer.tsx b/app/src/pages/QuickTransferDashboard/PinnedTransfer.tsx index 2ac60006813..cafcaa299a5 100644 --- a/app/src/pages/QuickTransferDashboard/PinnedTransfer.tsx +++ b/app/src/pages/QuickTransferDashboard/PinnedTransfer.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { useHistory } from 'react-router-dom' +import { useNavigate } from 'react-router-dom' import styled, { css } from 'styled-components' import { @@ -71,7 +71,7 @@ export function PinnedTransfer(props: { setTargetTransferId, } = props const cardSize = props.cardSize ?? 'full' - const history = useHistory() + const navigate = useNavigate() const longpress = useLongPress() const transferName = transfer.metadata.protocolName ?? transfer.files[0].name @@ -80,7 +80,7 @@ export function PinnedTransfer(props: { transferId: string ): void => { if (!longpress.isLongPressed) { - history.push(`/quick-transfer/${transferId}`) + navigate(`/quick-transfer/${transferId}`) } } React.useEffect(() => { diff --git a/app/src/pages/QuickTransferDashboard/QuickTransferCard.tsx b/app/src/pages/QuickTransferDashboard/QuickTransferCard.tsx index 25b6983f9a2..7f273f30658 100644 --- a/app/src/pages/QuickTransferDashboard/QuickTransferCard.tsx +++ b/app/src/pages/QuickTransferDashboard/QuickTransferCard.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { useHistory } from 'react-router-dom' +import { useNavigate } from 'react-router-dom' import { Trans, useTranslation } from 'react-i18next' import { useQueryClient } from 'react-query' import last from 'lodash/last' @@ -53,7 +53,7 @@ export function QuickTransferCard(props: { setShowDeleteConfirmationModal, setTargetTransferId, } = props - const history = useHistory() + const navigate = useNavigate() const [showIcon, setShowIcon] = React.useState(false) const [ showFailedAnalysisModal, @@ -108,7 +108,7 @@ export function QuickTransferCard(props: { if (isFailedAnalysis) { setShowFailedAnalysisModal(true) } else if (!longpress.isLongPressed) { - history.push(`/quick-transfer/${transferId}`) + navigate(`/quick-transfer/${transferId}`) } } diff --git a/app/src/pages/QuickTransferDashboard/__tests__/DeleteTransferConfirmationModal.test.tsx b/app/src/pages/QuickTransferDashboard/__tests__/DeleteTransferConfirmationModal.test.tsx index 7715f6efff8..177eb93a691 100644 --- a/app/src/pages/QuickTransferDashboard/__tests__/DeleteTransferConfirmationModal.test.tsx +++ b/app/src/pages/QuickTransferDashboard/__tests__/DeleteTransferConfirmationModal.test.tsx @@ -11,19 +11,19 @@ import { i18n } from '../../../i18n' import { useToaster } from '../../../organisms/ToasterOven' import { DeleteTransferConfirmationModal } from '../DeleteTransferConfirmationModal' -import type * as ReactRouterDom from 'react-router-dom' +import type { NavigateFunction } from 'react-router-dom' import type { HostConfig } from '@opentrons/api-client' -const mockPush = vi.fn() +const mockNavigate = vi.fn() vi.mock('@opentrons/api-client') vi.mock('@opentrons/react-api-client') vi.mock('../../../organisms/ToasterOven') vi.mock('react-router-dom', async importOriginal => { - const reactRouterDom = await importOriginal() + const reactRouterDom = await importOriginal() return { ...reactRouterDom, - useHistory: () => ({ push: mockPush } as any), + useNavigate: () => mockNavigate, } }) diff --git a/app/src/pages/QuickTransferDashboard/__tests__/PinnedTransfer.test.tsx b/app/src/pages/QuickTransferDashboard/__tests__/PinnedTransfer.test.tsx index 5ae26b3f1da..28588dbccb1 100644 --- a/app/src/pages/QuickTransferDashboard/__tests__/PinnedTransfer.test.tsx +++ b/app/src/pages/QuickTransferDashboard/__tests__/PinnedTransfer.test.tsx @@ -8,15 +8,15 @@ import { i18n } from '../../../i18n' import { PinnedTransfer } from '../PinnedTransfer' import type { ProtocolResource } from '@opentrons/shared-data' -import type * as ReactRouterDom from 'react-router-dom' +import type { NavigateFunction } from 'react-router-dom' -const mockPush = vi.fn() +const mockNavigate = vi.fn() vi.mock('react-router-dom', async importOriginal => { - const actual = await importOriginal() + const actual = await importOriginal() return { ...actual, - useHistory: () => ({ push: mockPush } as any), + useNavigate: () => mockNavigate, } }) @@ -63,7 +63,7 @@ describe('Pinned Transfer', () => { render() const name = screen.getByText('yay mock transfer') fireEvent.click(name) - expect(mockPush).toHaveBeenCalledWith('/quick-transfer/mockTransfer1') + expect(mockNavigate).toHaveBeenCalledWith('/quick-transfer/mockTransfer1') }) it('should display modal after long click', async () => { diff --git a/app/src/pages/QuickTransferDashboard/__tests__/QuickTransferCard.test.tsx b/app/src/pages/QuickTransferDashboard/__tests__/QuickTransferCard.test.tsx index de3a23700d9..6853233b08f 100644 --- a/app/src/pages/QuickTransferDashboard/__tests__/QuickTransferCard.test.tsx +++ b/app/src/pages/QuickTransferDashboard/__tests__/QuickTransferCard.test.tsx @@ -11,20 +11,20 @@ import { renderWithProviders } from '../../../__testing-utils__' import { i18n } from '../../../i18n' import { QuickTransferCard } from '../QuickTransferCard' import { LongPressModal } from '../LongPressModal' -import type * as ReactRouterDom from 'react-router-dom' +import type { NavigateFunction } from 'react-router-dom' import type { UseQueryResult } from 'react-query' import type { CompletedProtocolAnalysis, ProtocolResource, } from '@opentrons/shared-data' -const mockPush = vi.fn() +const mockNavigate = vi.fn() vi.mock('react-router-dom', async importOriginal => { - const actual = await importOriginal() + const actual = await importOriginal() return { ...actual, - useHistory: () => ({ push: mockPush } as any), + useNavigate: () => mockNavigate, } }) vi.mock('@opentrons/react-api-client') @@ -81,7 +81,7 @@ describe('QuickTransferCard', () => { render() const name = screen.getByText('yay mock transfer') fireEvent.click(name) - expect(mockPush).toHaveBeenCalledWith('/quick-transfer/mockTransfer1') + expect(mockNavigate).toHaveBeenCalledWith('/quick-transfer/mockTransfer1') }) it('should display the analysis failed error modal when clicking on the transfer', () => { diff --git a/app/src/pages/QuickTransferDashboard/index.tsx b/app/src/pages/QuickTransferDashboard/index.tsx index ed35db7fedc..b47d14f5ed8 100644 --- a/app/src/pages/QuickTransferDashboard/index.tsx +++ b/app/src/pages/QuickTransferDashboard/index.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { useHistory } from 'react-router-dom' +import { useNavigate } from 'react-router-dom' import { useDispatch, useSelector } from 'react-redux' import { useTranslation } from 'react-i18next' @@ -46,7 +46,7 @@ import type { QuickTransfersOnDeviceSortKey } from '../../redux/config/types' export function QuickTransferDashboard(): JSX.Element { const protocols = useAllProtocolsQuery() const { data: attachedInstruments } = useInstrumentsQuery() - const history = useHistory() + const navigate = useNavigate() const { t } = useTranslation(['quick_transfer', 'protocol_info']) const dispatch = useDispatch() const [navMenuIsOpened, setNavMenuIsOpened] = React.useState(false) @@ -151,7 +151,7 @@ export function QuickTransferDashboard(): JSX.Element { } else if (quickTransfersData.length >= 20) { setShowStorageLimitReachedModal(true) } else { - history.push('/quick-transfer/new') + navigate('/quick-transfer/new') } } @@ -181,7 +181,7 @@ export function QuickTransferDashboard(): JSX.Element { setShowPipetteNotAttaachedModal(false) }} onAttach={() => { - history.push('/instruments') + navigate('/instruments') }} /> ) : null} diff --git a/app/src/pages/QuickTransferDetails/__tests__/QuickTransferDetails.test.tsx b/app/src/pages/QuickTransferDetails/__tests__/QuickTransferDetails.test.tsx index f9a2c84c40c..929f5d46f82 100644 --- a/app/src/pages/QuickTransferDetails/__tests__/QuickTransferDetails.test.tsx +++ b/app/src/pages/QuickTransferDetails/__tests__/QuickTransferDetails.test.tsx @@ -2,7 +2,7 @@ import * as React from 'react' import { vi, it, describe, expect, beforeEach, afterEach } from 'vitest' import { fireEvent, screen } from '@testing-library/react' import { when } from 'vitest-when' -import { Route, MemoryRouter } from 'react-router-dom' +import { Route, MemoryRouter, Routes } from 'react-router-dom' import '@testing-library/jest-dom/vitest' import { renderWithProviders } from '../../../__testing-utils__' import { @@ -74,9 +74,12 @@ const MOCK_DATA = { const render = (path = '/quick-transfer/fakeTransferId') => { return renderWithProviders( - - - + + } + /> + , { i18nInstance: i18n, diff --git a/app/src/pages/QuickTransferDetails/index.tsx b/app/src/pages/QuickTransferDetails/index.tsx index 4bf429803b1..6742277aa95 100644 --- a/app/src/pages/QuickTransferDetails/index.tsx +++ b/app/src/pages/QuickTransferDetails/index.tsx @@ -3,7 +3,7 @@ import last from 'lodash/last' import { useTranslation } from 'react-i18next' import { useQueryClient } from 'react-query' import { useDispatch, useSelector } from 'react-redux' -import { useHistory, useParams } from 'react-router-dom' +import { useNavigate, useParams } from 'react-router-dom' import { ALIGN_CENTER, BORDERS, @@ -72,7 +72,7 @@ const QuickTransferHeader = ({ isScrolled, isTransferFetching, }: QuickTransferHeaderProps): JSX.Element => { - const history = useHistory() + const navigate = useNavigate() const { t } = useTranslation('protocol_details') const [truncate, setTruncate] = React.useState(true) const [startSetup, setStartSetup] = React.useState(false) @@ -107,7 +107,7 @@ const QuickTransferHeader = ({ paddingLeft="0rem" paddingRight={SPACING.spacing24} onClick={() => { - history.push('/quick-transfer') + navigate('/quick-transfer') }} width="3rem" > @@ -271,7 +271,9 @@ const TransferSectionContent = ({ export function QuickTransferDetails(): JSX.Element | null { const { t, i18n } = useTranslation(['quick_transfer', 'shared']) - const { quickTransferId: transferId } = useParams() + const { quickTransferId: transferId } = useParams< + keyof OnDeviceRouteParams + >() as OnDeviceRouteParams const { missingProtocolHardware, conflictedSlots, diff --git a/app/src/pages/RobotDashboard/__tests__/RobotDashboard.test.tsx b/app/src/pages/RobotDashboard/__tests__/RobotDashboard.test.tsx index f930ba4c6b7..df66762fdc9 100644 --- a/app/src/pages/RobotDashboard/__tests__/RobotDashboard.test.tsx +++ b/app/src/pages/RobotDashboard/__tests__/RobotDashboard.test.tsx @@ -19,15 +19,15 @@ import { RobotDashboard } from '..' import { useNotifyAllRunsQuery } from '../../../resources/runs' import type { ProtocolResource } from '@opentrons/shared-data' -import type * as ReactRouterDom from 'react-router-dom' +import type { NavigateFunction } from 'react-router-dom' -const mockPush = vi.fn() +const mockNavigate = vi.fn() vi.mock('react-router-dom', async importOriginal => { - const actual = await importOriginal() + const actual = await importOriginal() return { ...actual, - useHistory: () => ({ push: mockPush } as any), + useNavigate: () => mockNavigate, } }) vi.mock('@opentrons/react-api-client') diff --git a/app/src/pages/RunSummary/index.tsx b/app/src/pages/RunSummary/index.tsx index 7a57150ccab..348d0c6031f 100644 --- a/app/src/pages/RunSummary/index.tsx +++ b/app/src/pages/RunSummary/index.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { useSelector } from 'react-redux' -import { useParams, useHistory } from 'react-router-dom' +import { useParams, useNavigate } from 'react-router-dom' import { useTranslation } from 'react-i18next' import styled, { css } from 'styled-components' @@ -68,9 +68,11 @@ import type { OnDeviceRouteParams } from '../../App/types' import type { PipetteWithTip } from '../../organisms/DropTipWizardFlows' export function RunSummary(): JSX.Element { - const { runId } = useParams() + const { runId } = useParams< + keyof OnDeviceRouteParams + >() as OnDeviceRouteParams const { t } = useTranslation('run_details') - const history = useHistory() + const navigate = useNavigate() const host = useHost() const { data: runRecord } = useNotifyRunQuery(runId, { staleTime: Infinity }) const isRunCurrent = Boolean(runRecord?.data?.current) @@ -144,7 +146,7 @@ export function RunSummary(): JSX.Element { const returnToDash = (): void => { closeCurrentRun() - history.push('/') + navigate('/') } // TODO(jh, 05-30-24): EXEC-487. Refactor reset() so we can redirect to the setup page, showing the shimmer skeleton instead. diff --git a/app/src/pages/RunningProtocol/__tests__/RunningProtocol.test.tsx b/app/src/pages/RunningProtocol/__tests__/RunningProtocol.test.tsx index a9335b25c73..1114f4964eb 100644 --- a/app/src/pages/RunningProtocol/__tests__/RunningProtocol.test.tsx +++ b/app/src/pages/RunningProtocol/__tests__/RunningProtocol.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { Route, MemoryRouter } from 'react-router-dom' +import { Route, MemoryRouter, Routes } from 'react-router-dom' import { vi, it, describe, expect, beforeEach, afterEach } from 'vitest' import { when } from 'vitest-when' import { screen } from '@testing-library/react' @@ -82,9 +82,9 @@ const mockResumeRunFromRecovery = vi.fn() const render = (path = '/') => { return renderWithProviders( - - - + + } /> + ) } diff --git a/app/src/pages/RunningProtocol/index.tsx b/app/src/pages/RunningProtocol/index.tsx index e6c42bc85cb..5240196e9e1 100644 --- a/app/src/pages/RunningProtocol/index.tsx +++ b/app/src/pages/RunningProtocol/index.tsx @@ -79,7 +79,9 @@ export type ScreenOption = | 'RunningProtocolCommandList' export function RunningProtocol(): JSX.Element { - const { runId } = useParams() + const { runId } = useParams< + keyof OnDeviceRouteParams + >() as OnDeviceRouteParams const [currentOption, setCurrentOption] = React.useState( 'CurrentRunningProtocolCommand' ) diff --git a/app/src/pages/UpdateRobot/UpdateRobot.tsx b/app/src/pages/UpdateRobot/UpdateRobot.tsx index 810ff22bd40..413665365a0 100644 --- a/app/src/pages/UpdateRobot/UpdateRobot.tsx +++ b/app/src/pages/UpdateRobot/UpdateRobot.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { useSelector, useDispatch } from 'react-redux' -import { useHistory } from 'react-router-dom' +import { useNavigate } from 'react-router-dom' import { useTranslation } from 'react-i18next' import { Flex, SPACING, DIRECTION_ROW } from '@opentrons/components' @@ -23,7 +23,7 @@ import { import type { State, Dispatch } from '../../redux/types' export function UpdateRobot(): JSX.Element { - const history = useHistory() + const navigate = useNavigate() const { i18n, t } = useTranslation(['device_settings', 'shared']) const localRobot = useSelector(getLocalRobot) const robotUpdateType = useSelector((state: State) => { @@ -48,7 +48,7 @@ export function UpdateRobot(): JSX.Element { buttonText={t('cancel_software_update')} onClick={() => { dispatch(clearRobotUpdateSession()) - history.goBack() + navigate(-1) }} /> + { + navigate(-1) + }} + /> ) : ( (true) - const history = useHistory() + const navigate = useNavigate() const { i18n, t } = useTranslation(['device_settings', 'shared']) const dispatchStartRobotUpdate = useDispatchStartRobotUpdate() const dispatch = useDispatch() @@ -86,7 +86,7 @@ export function UpdateRobotDuringOnboarding(): JSX.Element { buttonText={t('proceed_without_updating')} onClick={() => { dispatch(clearRobotUpdateSession()) - history.push('/emergency-stop') + navigate('/emergency-stop') }} /> { - history.push('/emergency-stop') + navigate('/emergency-stop') }} /> ) : ( diff --git a/app/src/pages/Welcome/__tests__/Welcome.test.tsx b/app/src/pages/Welcome/__tests__/Welcome.test.tsx index 4842206d807..756b7bcb4b5 100644 --- a/app/src/pages/Welcome/__tests__/Welcome.test.tsx +++ b/app/src/pages/Welcome/__tests__/Welcome.test.tsx @@ -8,16 +8,16 @@ import { renderWithProviders } from '../../../__testing-utils__' import { i18n } from '../../../i18n' import { Welcome } from '..' -import type * as ReactRouterDom from 'react-router-dom' +import type { NavigateFunction } from 'react-router-dom' const PNG_FILE_NAME = 'welcome_background.png' -const mockPush = vi.fn() +const mockNavigate = vi.fn() vi.mock('react-router-dom', async importOriginal => { - const actual = await importOriginal() + const actual = await importOriginal() return { ...actual, - useHistory: () => ({ push: mockPush } as any), + useNavigate: () => mockNavigate, } }) @@ -44,9 +44,9 @@ describe('Welcome', () => { expect(image.getAttribute('src')).toContain(PNG_FILE_NAME) }) - it('should call mockPush when tapping Get started', () => { + it('should call mockNavigate when tapping Get started', () => { render() fireEvent.click(screen.getByRole('button', { name: 'Get started' })) - expect(mockPush).toHaveBeenCalledWith('/network-setup') + expect(mockNavigate).toHaveBeenCalledWith('/network-setup') }) }) diff --git a/app/src/pages/Welcome/index.tsx b/app/src/pages/Welcome/index.tsx index 867af7c3fd1..d43c9e9e054 100644 --- a/app/src/pages/Welcome/index.tsx +++ b/app/src/pages/Welcome/index.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' -import { useHistory } from 'react-router-dom' +import { useNavigate } from 'react-router-dom' import { COLORS, DIRECTION_COLUMN, @@ -18,7 +18,7 @@ const IMAGE_ALT = 'Welcome screen background image' export function Welcome(): JSX.Element { const { t } = useTranslation(['device_settings', 'shared', 'branded']) - const history = useHistory() + const navigate = useNavigate() return ( { - history.push('/network-setup') + navigate('/network-setup') }} /> diff --git a/app/src/redux/reducer.ts b/app/src/redux/reducer.ts index 5bf33bb3b38..44831b0d70e 100644 --- a/app/src/redux/reducer.ts +++ b/app/src/redux/reducer.ts @@ -1,7 +1,4 @@ -import createHistory from 'history/createHashHistory' import { combineReducers } from 'redux' -import { connectRouter } from 'connected-react-router' -import type { RouterState } from 'connected-react-router' // api state import { robotApiReducer } from './robot-api/reducer' @@ -53,14 +50,8 @@ import { protocolStorageReducer } from './protocol-storage/reducer' import type { Reducer } from 'redux' import type { State, Action } from './types' -import type { History } from 'history' -export const history = createHistory() - -export const rootReducer: Reducer = combineReducers< - State, - Action ->({ +export const rootReducer: Reducer = combineReducers({ robotApi: robotApiReducer, robotAdmin: robotAdminReducer, robotControls: robotControlsReducer, @@ -77,7 +68,4 @@ export const rootReducer: Reducer = combineReducers< sessions: sessionReducer, calibration: calibrationReducer, protocolStorage: protocolStorageReducer, - router: connectRouter( - history as History> - ) as Reducer, }) diff --git a/app/src/redux/store.ts b/app/src/redux/store.ts index 0567386b313..0572d875655 100644 --- a/app/src/redux/store.ts +++ b/app/src/redux/store.ts @@ -1,22 +1,17 @@ import { createStore, applyMiddleware, compose } from 'redux' import thunk from 'redux-thunk' -import { routerMiddleware } from 'connected-react-router' import { createEpicMiddleware } from 'redux-observable' -import { rootReducer, history } from './reducer' +import { rootReducer } from './reducer' import { rootEpic } from './epic' import type { StoreEnhancer } from 'redux' -import type { Action, Middleware, State } from './types' +import type { Action, State } from './types' const epicMiddleware = createEpicMiddleware() -const middleware = applyMiddleware( - thunk, - epicMiddleware, - routerMiddleware(history) as Middleware -) +const middleware = applyMiddleware(thunk, epicMiddleware) const composeEnhancers = (window as any)?.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__?.({ maxAge: 200 }) ?? diff --git a/app/src/redux/types.ts b/app/src/redux/types.ts index a46c7f2dd96..9ed69c3e71f 100644 --- a/app/src/redux/types.ts +++ b/app/src/redux/types.ts @@ -1,7 +1,7 @@ /* eslint-disable no-use-before-define */ // application types import type { Store as ReduxStore, Dispatch as ReduxDispatch } from 'redux' -import type { RouterState, RouterAction } from 'connected-react-router' +import type { RouterAction } from 'connected-react-router' import type { Observable } from 'rxjs' import type { RobotApiState, RobotApiAction } from './robot-api/types' @@ -54,7 +54,6 @@ export interface State { readonly sessions: SessionState readonly calibration: CalibrationState readonly protocolStorage: ProtocolStorageState - readonly router: RouterState } export type Action = diff --git a/components/package.json b/components/package.json index b6d71152b6a..15306b92671 100644 --- a/components/package.json +++ b/components/package.json @@ -19,9 +19,7 @@ "homepage": "https://github.com/Opentrons/opentrons#readme", "peerDependencies": { "react": "18.2.0", - "react-dom": "18.2.0", - "react-router-dom": "5.3.4", - "@types/react-router-dom": "5.3.3" + "react-dom": "18.2.0" }, "dependencies": { "@opentrons/shared-data": "link:../shared-data", @@ -38,7 +36,6 @@ "react-i18next": "13.5.0", "react-popper": "1.0.0", "react-remove-scroll": "2.4.3", - "react-router-dom": "5.3.4", "react-select": "5.4.0", "redux": "4.0.5", "styled-components": "5.3.6" diff --git a/components/src/index.ts b/components/src/index.ts index 6e38096c4a5..afb56a0a9ed 100644 --- a/components/src/index.ts +++ b/components/src/index.ts @@ -18,7 +18,6 @@ export * from './lists' export * from './modals' export * from './nav' export * from './primitives' -export * from './tabbedNav' export * from './slotmap' export * from './structure' export * from './tooltips' diff --git a/components/src/lists/ListItem.tsx b/components/src/lists/ListItem.tsx deleted file mode 100644 index ecce326ea25..00000000000 --- a/components/src/lists/ListItem.tsx +++ /dev/null @@ -1,92 +0,0 @@ -// ListItem component to be used as a child of TitledList -import * as React from 'react' -import { NavLink } from 'react-router-dom' -import classnames from 'classnames' - -import styles from './lists.module.css' -import { Icon } from '../icons' -import type { IconName } from '../icons' - -// TODO(bc, 2021-03-31): this is only used in the app -// reconsider whether this belongs in components library -interface ListItemProps { - /** click handler */ - onClick?: (event: React.SyntheticEvent) => unknown - /** mouse enter handler */ - onMouseEnter?: (event: React.MouseEvent) => unknown - /** mouse leave handler */ - onMouseLeave?: (event: React.MouseEvent) => unknown - /** mouse enter handler */ - onPointerEnter?: (event: React.PointerEvent) => unknown - /** mouse leave handler */ - onPointerLeave?: (event: React.PointerEvent) => unknown - /** if URL is specified, ListItem is wrapped in a React Router NavLink */ - url?: string | null - /** if URL is specified NavLink can receive an active class name */ - activeClassName?: string - /** if URL is specified NavLink can receive an exact property for matching routes */ - exact?: boolean - /** Additional class name */ - className?: string - /** if disabled, the onClick handler will be disabled */ - isDisabled?: boolean - /** name constant of the icon to display */ - iconName?: IconName - 'aria-describedby'?: string - ref?: { current: Element | null } | ((current: Element | null) => unknown) - children?: React.ReactNode -} - -/** - * A styled `
  • ` with an optional icon, and an optional url for a React Router `NavLink` - * - */ -export const ListItem = React.forwardRef( - (props: ListItemProps, ref: React.ForwardedRef) => { - const { url, isDisabled, iconName, activeClassName, exact } = props - const onClick = props.onClick && !isDisabled ? props.onClick : undefined - // @ts-expect-error(sa, 2021-6-23): cast value to boolean - const className = classnames(props.className, styles.list_item, { - [styles.disabled]: isDisabled, - [styles.clickable]: onClick, - }) - - const itemIcon = iconName && ( - - ) - - if (url != null) { - return ( -
  • - - {itemIcon} - {props.children} - -
  • - ) - } - - return ( -
  • - {itemIcon} - {props.children} -
  • - ) - } -) diff --git a/components/src/lists/index.ts b/components/src/lists/index.ts index cd2586912f3..c11be61e41a 100644 --- a/components/src/lists/index.ts +++ b/components/src/lists/index.ts @@ -1,4 +1,3 @@ // list and list item components export * from './SidePanelGroup' export * from './TitledList' -export * from './ListItem' diff --git a/components/src/structure/PageTabs.tsx b/components/src/structure/PageTabs.tsx deleted file mode 100644 index 3475326e423..00000000000 --- a/components/src/structure/PageTabs.tsx +++ /dev/null @@ -1,46 +0,0 @@ -// page tabs bar - -import * as React from 'react' -import classnames from 'classnames' -import { Link } from 'react-router-dom' - -import styles from './structure.module.css' - -// TODO(bc, 2021-03-29): this component is only used in RA -// reconsider whether it belongs in components library -interface TabProps { - title: string - href: string - isActive: boolean - isDisabled: boolean -} - -export interface PageTabProps { - pages: TabProps[] -} - -export function PageTabs(props: PageTabProps): JSX.Element { - return ( - - ) -} - -function Tab(props: TabProps): JSX.Element { - const { isDisabled } = props - const tabLinkClass = classnames(styles.tab_link, { - [styles.active_tab_link]: props.isActive, - }) - - // TODO(mc, 2017-12-14): make a component for proper disabling of links - const MaybeLink: any = !isDisabled ? Link : 'span' - - return ( - -

    {props.title}

    -
    - ) -} diff --git a/components/src/structure/index.ts b/components/src/structure/index.ts index 90dbf88700e..371cfc731d6 100644 --- a/components/src/structure/index.ts +++ b/components/src/structure/index.ts @@ -1,6 +1,5 @@ // structure components -export * from './PageTabs' export * from './TitleBar' export * from './Card' export * from './Splash' diff --git a/components/src/tabbedNav/index.ts b/components/src/tabbedNav/index.ts deleted file mode 100644 index 0b670948e40..00000000000 --- a/components/src/tabbedNav/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -// navigational components - -// TODO(bc, 2021-03-29): these components are only used in one place -// reconsider whether they belong in components library -export * from './TabbedNavBar' -export * from './NavTab' -export * from './OutsideLinkTab' diff --git a/labware-library/package.json b/labware-library/package.json index 9c0010a60c2..7c8e0cfa8e3 100644 --- a/labware-library/package.json +++ b/labware-library/package.json @@ -23,7 +23,6 @@ "@types/jszip": "3.1.7", "@types/mixpanel-browser": "^2.35.6", "@types/query-string": "6.2.0", - "@types/react-router-dom": "5.3.3", "@types/webpack-env": "^1.16.0", "@types/yup": "0.29.11" }, @@ -41,7 +40,7 @@ "query-string": "6.2.0", "react": "18.2.0", "react-dom": "18.2.0", - "react-router-dom": "5.3.4", + "react-router-dom": "6.24.1", "yup": "0.32.9" } } diff --git a/labware-library/src/components/App/index.tsx b/labware-library/src/components/App/index.tsx index 8b18a6d431d..bb9b907da77 100644 --- a/labware-library/src/components/App/index.tsx +++ b/labware-library/src/components/App/index.tsx @@ -1,7 +1,7 @@ // main application wrapper component import * as React from 'react' import cx from 'classnames' - +import { useLocation } from 'react-router-dom' import { DefinitionRoute } from '../../definitions' import { useFilters } from '../../filters' import { Nav, Breadcrumbs } from '../Nav' @@ -14,7 +14,8 @@ import styles from './styles.module.css' import type { DefinitionRouteRenderProps } from '../../definitions' export function AppComponent(props: DefinitionRouteRenderProps): JSX.Element { - const { definition, location } = props + const { definition } = props + const location = useLocation() const scrollRef = React.useRef(null) const filters = useFilters(location) const isDetailPage = Boolean(definition) diff --git a/labware-library/src/components/Sidebar/FilterManufacturer.tsx b/labware-library/src/components/Sidebar/FilterManufacturer.tsx index 315cc9c071e..6aed332f847 100644 --- a/labware-library/src/components/Sidebar/FilterManufacturer.tsx +++ b/labware-library/src/components/Sidebar/FilterManufacturer.tsx @@ -1,6 +1,6 @@ // filter labware by manufacturer import * as React from 'react' -import { withRouter } from 'react-router-dom' +import { useNavigate } from 'react-router-dom' import { SelectField } from '@opentrons/components' import { getAllManufacturers, buildFiltersUrl } from '../../filters' import styles from './styles.module.css' @@ -8,17 +8,17 @@ import styles from './styles.module.css' import { MANUFACTURER, MANUFACTURER_VALUES } from '../../localization' import type { SelectOptionOrGroup } from '@opentrons/components' -import type { RouteComponentProps } from 'react-router-dom' import type { FilterParams } from '../../types' -export interface FilterManufacturerProps extends RouteComponentProps { +export interface FilterManufacturerProps { filters: FilterParams } -export function FilterManufacturerComponent( +export function FilterManufacturer( props: FilterManufacturerProps ): JSX.Element { - const { history, filters } = props + const { filters } = props + const navigate = useNavigate() const manufacturers = getAllManufacturers() const options: SelectOptionOrGroup[] = manufacturers.map(value => ({ value, @@ -37,14 +37,10 @@ export function FilterManufacturerComponent( options={options} onValueChange={(_, value) => { if (value) { - history.push(buildFiltersUrl({ ...filters, manufacturer: value })) + navigate(buildFiltersUrl({ ...filters, manufacturer: value })) } }} /> ) } -// @ts-expect-error react router type not portable -export const FilterManufacturer: (props: { - filters: FilterParams -}) => JSX.Element = withRouter(FilterManufacturerComponent) diff --git a/labware-library/src/components/ui/Link.tsx b/labware-library/src/components/ui/Link.tsx index 4672b01a3dd..f51572e6695 100644 --- a/labware-library/src/components/ui/Link.tsx +++ b/labware-library/src/components/ui/Link.tsx @@ -1,16 +1,15 @@ // internal link that preserves query parameters import * as React from 'react' -import { withRouter, Link as BaseLink } from 'react-router-dom' -import type { RouteComponentProps } from 'react-router-dom' +import { Link as BaseLink, useLocation } from 'react-router-dom' -export interface LinkProps extends RouteComponentProps { +export interface LinkProps { to: string children?: React.ReactNode className?: string } -export function WrappedLink(props: LinkProps): JSX.Element { - const { to, children, className, location } = props +export function Link({ to, children, className }: LinkProps): JSX.Element { + const location = useLocation() return ( ) } - -// @ts-expect-error react router type not portable -export const Link: (props: { - to: string - children?: React.ReactNode - className?: string -}) => JSX.Element = withRouter(WrappedLink) diff --git a/labware-library/src/definitions.tsx b/labware-library/src/definitions.tsx index b1f76208177..7a3c8bd0e8e 100644 --- a/labware-library/src/definitions.tsx +++ b/labware-library/src/definitions.tsx @@ -1,16 +1,14 @@ // labware definition helpers // TODO(mc, 2019-03-18): move to shared-data? import * as React from 'react' -import { Route } from 'react-router-dom' +import { useParams } from 'react-router-dom' import groupBy from 'lodash/groupBy' import uniq from 'lodash/uniq' import { LABWAREV2_DO_NOT_LIST, getAllDefinitions as _getAllDefinitions, } from '@opentrons/shared-data' -import { getPublicPath } from './public-path' -import type { RouteComponentProps } from 'react-router-dom' import type { LabwareDefinition2 } from '@opentrons/shared-data' import type { LabwareList, LabwareDefinition } from './types' @@ -76,7 +74,7 @@ export function getDefinition( return def || null } -export interface DefinitionRouteRenderProps extends RouteComponentProps { +export interface DefinitionRouteRenderProps { definition: LabwareDefinition | null } @@ -84,21 +82,13 @@ export interface DefinitionRouteProps { render: (props: DefinitionRouteRenderProps) => React.ReactNode } -export function DefinitionRoute(props: DefinitionRouteProps): JSX.Element { - return ( - { - const { loadName } = routeProps.match.params - const definition = getDefinition(loadName) +export const DefinitionRoute: React.FC = ({ render }) => { + const { loadName } = useParams<{ loadName: string }>() + const definition = getDefinition(loadName) - // TODO(mc, 2019-04-10): handle 404 if loadName exists but definition - // isn't found + // TODO: handle 404 if loadName exists but definition isn't found - return props.render({ ...routeProps, definition }) - }} - /> - ) + return <>{render({ definition })} } export const NEW_LABWARE_DEFS = [ diff --git a/labware-library/src/filters.tsx b/labware-library/src/filters.tsx index 0ce9323879f..65e77f538b9 100644 --- a/labware-library/src/filters.tsx +++ b/labware-library/src/filters.tsx @@ -8,8 +8,8 @@ import uniq from 'lodash/uniq' import { getAllDefinitions } from './definitions' import { getPublicPath } from './public-path' -import type { Location } from 'history' import type { FilterParams, LabwareDefinition, LabwareList } from './types' +import type { Location } from 'react-router-dom' export const FILTER_OFF = 'all' @@ -30,7 +30,7 @@ export function getAllManufacturers(): string[] { return uniq([FILTER_OFF, ...brands, ...wellGroupBrands]) } -export function useFilters(location: Location): FilterParams { +export function useFilters(location: Location): FilterParams { const [params, setParams] = useState({ category: FILTER_OFF, manufacturer: FILTER_OFF, diff --git a/labware-library/src/index.tsx b/labware-library/src/index.tsx index 9bac13fe66c..7f244f10e75 100644 --- a/labware-library/src/index.tsx +++ b/labware-library/src/index.tsx @@ -1,7 +1,7 @@ // labware library entry import * as React from 'react' import { hydrate, render } from 'react-dom' -import { BrowserRouter, Route, Switch } from 'react-router-dom' +import { BrowserRouter, Route, Routes } from 'react-router-dom' import { App } from './components/App' import { LabwareCreator } from './labware-creator' @@ -19,10 +19,10 @@ if (!$root) { const Root = (): JSX.Element => ( - - - - + + } /> + } /> + ) diff --git a/package.json b/package.json index 0fe8099d97e..0727c37a148 100755 --- a/package.json +++ b/package.json @@ -63,7 +63,6 @@ "@types/react-color": "^3.0.6", "@types/react-dom": "18.2.0", "@types/react-redux": "7.1.32", - "@types/react-router-dom": "5.3.3", "@types/redux-mock-store": "^1.0.2", "@types/semver": "^7.3.6", "@typescript-eslint/eslint-plugin": "^6.20.0", diff --git a/protocol-designer/src/containers/ConnectedNav.tsx b/protocol-designer/src/containers/ConnectedNav.tsx index fcc7d2c3311..d22fd9420c7 100644 --- a/protocol-designer/src/containers/ConnectedNav.tsx +++ b/protocol-designer/src/containers/ConnectedNav.tsx @@ -2,9 +2,11 @@ import * as React from 'react' import { useDispatch, useSelector } from 'react-redux' import { useTranslation } from 'react-i18next' import { KNOWLEDGEBASE_ROOT_URL } from '../components/KnowledgeBaseLink' -import { NavTab, TabbedNavBar, OutsideLinkTab } from '@opentrons/components' import { selectors as fileSelectors } from '../file-data' import { actions, selectors } from '../navigation' +import { TabbedNavBar } from './TabbedNavBar' +import { NavTab } from './NavTab' +import { OutsideLinkTab } from './OutsideLinkTab' import type { Page } from '../navigation' export function ConnectedNav(): JSX.Element { diff --git a/components/src/tabbedNav/NavTab.tsx b/protocol-designer/src/containers/NavTab.tsx similarity index 90% rename from components/src/tabbedNav/NavTab.tsx rename to protocol-designer/src/containers/NavTab.tsx index a94f7c6e876..a409789b029 100644 --- a/components/src/tabbedNav/NavTab.tsx +++ b/protocol-designer/src/containers/NavTab.tsx @@ -2,12 +2,10 @@ import * as React from 'react' import { NavLink } from 'react-router-dom' import classnames from 'classnames' -import styles from './navbar.module.css' -import { Button } from '../buttons' -import { NotificationIcon } from '../icons' +import { Button, NotificationIcon } from '@opentrons/components' +import type { IconName, ButtonProps } from '@opentrons/components' -import type { IconName } from '../icons' -import type { ButtonProps } from '../buttons' +import styles from './navbar.module.css' export interface NavTabProps { /** optional click event for nav button */ diff --git a/components/src/tabbedNav/OutsideLinkTab.tsx b/protocol-designer/src/containers/OutsideLinkTab.tsx similarity index 92% rename from components/src/tabbedNav/OutsideLinkTab.tsx rename to protocol-designer/src/containers/OutsideLinkTab.tsx index e935df8ffd1..6ebbd5aca35 100644 --- a/components/src/tabbedNav/OutsideLinkTab.tsx +++ b/protocol-designer/src/containers/OutsideLinkTab.tsx @@ -1,11 +1,9 @@ import * as React from 'react' import cx from 'classnames' +import { Button, NotificationIcon } from '@opentrons/components' +import type { IconName } from '@opentrons/components' import styles from './navbar.module.css' -import { Button } from '../buttons' -import { NotificationIcon } from '../icons' - -import type { IconName } from '../icons' export interface OutsideLinkTabProps { /** optional click event for nav button */ diff --git a/components/src/tabbedNav/TabbedNavBar.tsx b/protocol-designer/src/containers/TabbedNavBar.tsx similarity index 100% rename from components/src/tabbedNav/TabbedNavBar.tsx rename to protocol-designer/src/containers/TabbedNavBar.tsx diff --git a/components/src/tabbedNav/navbar.module.css b/protocol-designer/src/containers/navbar.module.css similarity index 96% rename from components/src/tabbedNav/navbar.module.css rename to protocol-designer/src/containers/navbar.module.css index c1119a6a10c..078a300a395 100644 --- a/components/src/tabbedNav/navbar.module.css +++ b/protocol-designer/src/containers/navbar.module.css @@ -1,4 +1,4 @@ -@import '../index.module.css'; +@import '@opentrons/components/styles'; .navbar { flex: none; diff --git a/yarn.lock b/yarn.lock index f30ce404018..05383e56198 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1769,7 +1769,7 @@ core-js-pure "^3.30.2" regenerator-runtime "^0.14.0" -"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.5", "@babel/runtime@^7.12.0", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.17.8", "@babel/runtime@^7.18.3", "@babel/runtime@^7.21.0", "@babel/runtime@^7.22.5", "@babel/runtime@^7.23.8", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": +"@babel/runtime@^7.10.5", "@babel/runtime@^7.12.0", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.17.8", "@babel/runtime@^7.18.3", "@babel/runtime@^7.21.0", "@babel/runtime@^7.22.5", "@babel/runtime@^7.23.8", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": version "7.24.4" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.4.tgz#de795accd698007a66ba44add6cc86542aff1edd" integrity sha512-dkxf7+hn8mFBwKjs9bvBlArzLVxVbS8usaPUDd5p2a9JCL9tB8OaOVN1isD4+Xyk4ns89/xeOmbQvgdK7IIVdA== @@ -3269,7 +3269,7 @@ react-intersection-observer "^8.33.1" react-markdown "9.0.1" react-redux "8.1.2" - react-router-dom "5.3.4" + react-router-dom "6.24.1" react-select "5.4.0" react-simple-keyboard "^3.7.0" react-viewport-list "6.3.0" @@ -3300,7 +3300,6 @@ react-i18next "13.5.0" react-popper "1.0.0" react-remove-scroll "2.4.3" - react-router-dom "5.3.4" react-select "5.4.0" redux "4.0.5" styled-components "5.3.6" @@ -3338,7 +3337,7 @@ query-string "6.2.0" react "18.2.0" react-dom "18.2.0" - react-router-dom "5.3.4" + react-router-dom "6.24.1" yup "0.32.9" "@opentrons/react-api-client@link:react-api-client": @@ -5350,11 +5349,6 @@ dependencies: "@types/unist" "*" -"@types/history@^4.7.11": - version "4.7.11" - resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.11.tgz#56588b17ae8f50c53983a524fc3cc47437969d64" - integrity sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA== - "@types/hoist-non-react-statics@*", "@types/hoist-non-react-statics@^3.3.0", "@types/hoist-non-react-statics@^3.3.1": version "3.3.5" resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz#dab7867ef789d87e2b4b0003c9d65c49cc44a494" @@ -5584,23 +5578,6 @@ hoist-non-react-statics "^3.3.0" redux "^4.0.0" -"@types/react-router-dom@5.3.3": - version "5.3.3" - resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-5.3.3.tgz#e9d6b4a66fcdbd651a5f106c2656a30088cc1e83" - integrity sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw== - dependencies: - "@types/history" "^4.7.11" - "@types/react" "*" - "@types/react-router" "*" - -"@types/react-router@*": - version "5.1.20" - resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-5.1.20.tgz#88eccaa122a82405ef3efbcaaa5dcdd9f021387c" - integrity sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q== - dependencies: - "@types/history" "^4.7.11" - "@types/react" "*" - "@types/react-transition-group@^4.4.0": version "4.4.10" resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.10.tgz#6ee71127bdab1f18f11ad8fb3322c6da27c327ac" @@ -12827,18 +12804,6 @@ history@4.7.2: value-equal "^0.4.0" warning "^3.0.0" -history@^4.9.0: - version "4.10.1" - resolved "https://registry.yarnpkg.com/history/-/history-4.10.1.tgz#33371a65e3a83b267434e2b3f3b1b4c58aad4cf3" - integrity sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew== - dependencies: - "@babel/runtime" "^7.1.2" - loose-envify "^1.2.0" - resolve-pathname "^3.0.0" - tiny-invariant "^1.0.2" - tiny-warning "^1.0.0" - value-equal "^1.0.1" - hmac-drbg@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" @@ -12848,7 +12813,7 @@ hmac-drbg@^1.0.1: minimalistic-assert "^1.0.0" minimalistic-crypto-utils "^1.0.1" -hoist-non-react-statics@^3.0.0, hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2: +hoist-non-react-statics@^3.0.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== @@ -14837,7 +14802,7 @@ longest@^1.0.1: resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097" integrity sha512-k+yt5n3l48JU4k8ftnKG6V7u32wyH2NfKzeMto9F/QRE0amxy/LayxwlvjjkZEIzqR+19IrtFO8p5kB9QaYUFg== -loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1, loose-envify@^1.4.0: +loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== @@ -16994,13 +16959,6 @@ path-to-regexp@3.0.0: resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-3.0.0.tgz#c981a218f3df543fa28696be2f88e0c58d2e012a" integrity sha512-ZOtfhPttCrqp2M1PBBH4X13XlvnfhIwD7yCLx+GoGoXRPQyxGOTdQMpIzPSPKXAJT/JQrdfFrgdJOyAzvgpQ9A== -path-to-regexp@^1.7.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.8.0.tgz#887b3ba9d84393e87a0a0b9f4cb756198b53548a" - integrity sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA== - dependencies: - isarray "0.0.1" - path-type@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" @@ -18480,7 +18438,7 @@ react-is@18.1.0: resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.1.0.tgz#61aaed3096d30eacf2a2127118b5b41387d32a67" integrity sha512-Fl7FuabXsJnV5Q1qIOQwx/sagGF18kogb4gpfcG4gjLBWO0WDiiz1ko/ExayuxE7InyQkBLkxRFG5oxY6Uu3Kg== -react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0: +react-is@^16.13.1, react-is@^16.7.0: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== @@ -18579,19 +18537,6 @@ react-remove-scroll@2.5.5: use-callback-ref "^1.3.0" use-sidecar "^1.1.2" -react-router-dom@5.3.4: - version "5.3.4" - resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.3.4.tgz#2ed62ffd88cae6db134445f4a0c0ae8b91d2e5e6" - integrity sha512-m4EqFMHv/Ih4kpcBCONHbkT68KoAeHN4p3lAGoNryfHi0dMy0kCzEZakiKRsvg5wHZ/JLrLW8o8KomWiz/qbYQ== - dependencies: - "@babel/runtime" "^7.12.13" - history "^4.9.0" - loose-envify "^1.3.1" - prop-types "^15.6.2" - react-router "5.3.4" - tiny-invariant "^1.0.2" - tiny-warning "^1.0.0" - react-router-dom@6.24.1: version "6.24.1" resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.24.1.tgz#b1a22f7d6c5a1bfce30732bd370713f991ab4de4" @@ -18600,21 +18545,6 @@ react-router-dom@6.24.1: "@remix-run/router" "1.17.1" react-router "6.24.1" -react-router@5.3.4: - version "5.3.4" - resolved "https://registry.yarnpkg.com/react-router/-/react-router-5.3.4.tgz#8ca252d70fcc37841e31473c7a151cf777887bb5" - integrity sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA== - dependencies: - "@babel/runtime" "^7.12.13" - history "^4.9.0" - hoist-non-react-statics "^3.1.0" - loose-envify "^1.3.1" - path-to-regexp "^1.7.0" - prop-types "^15.6.2" - react-is "^16.6.0" - tiny-invariant "^1.0.2" - tiny-warning "^1.0.0" - react-router@6.24.1: version "6.24.1" resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.24.1.tgz#5a3bbba0000afba68d42915456ca4c806f37a7de" @@ -19301,11 +19231,6 @@ resolve-pathname@^2.2.0: resolved "https://registry.yarnpkg.com/resolve-pathname/-/resolve-pathname-2.2.0.tgz#7e9ae21ed815fd63ab189adeee64dc831eefa879" integrity sha512-bAFz9ld18RzJfddgrO2e/0S2O81710++chRMUxHjXOYKF6jTAMrUNZrEZ1PvV0zlhfjidm08iRPdTLPno1FuRg== -resolve-pathname@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/resolve-pathname/-/resolve-pathname-3.0.0.tgz#99d02224d3cf263689becbb393bc560313025dcd" - integrity sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng== - resolve-pkg-maps@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz#616b3dc2c57056b5588c31cdf4b3d64db133720f" @@ -21178,12 +21103,12 @@ tiny-case@^1.0.3: resolved "https://registry.yarnpkg.com/tiny-case/-/tiny-case-1.0.3.tgz#d980d66bc72b5d5a9ca86fb7c9ffdb9c898ddd03" integrity sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q== -tiny-invariant@^1.0.2, tiny-invariant@^1.3.1, tiny-invariant@^1.3.3: +tiny-invariant@^1.3.1, tiny-invariant@^1.3.3: version "1.3.3" resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz#46680b7a873a0d5d10005995eb90a70d74d60127" integrity sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg== -tiny-warning@^1.0.0, tiny-warning@^1.0.2: +tiny-warning@^1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754" integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== @@ -22193,11 +22118,6 @@ value-equal@^0.4.0: resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-0.4.0.tgz#c5bdd2f54ee093c04839d71ce2e4758a6890abc7" integrity sha512-x+cYdNnaA3CxvMaTX0INdTCN8m8aF2uY9BvEqmxuYp8bL09cs/kWVQPVGcA35fMktdOsP69IgU7wFj/61dJHEw== -value-equal@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-1.0.1.tgz#1e0b794c734c5c0cade179c437d356d931a34d6c" - integrity sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw== - vary@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" From aa35e2d59c2ece888d1c4bed663ade680d34dc7c Mon Sep 17 00:00:00 2001 From: Jamey Huffnagle Date: Mon, 22 Jul 2024 14:41:19 -0400 Subject: [PATCH 75/78] feat(app): add the protocol run's current action count to the ODD (#15724) Closes EXEC-561 The current run action is displayed on the desktop app but is missing from the ODD. This adds it to the RunningProtocolCommandList. --- .../RunningProtocol/RunningProtocolCommandList.tsx | 10 +++++++++- .../__tests__/RunningProtocolCommandList.test.tsx | 5 +++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/app/src/organisms/OnDeviceDisplay/RunningProtocol/RunningProtocolCommandList.tsx b/app/src/organisms/OnDeviceDisplay/RunningProtocol/RunningProtocolCommandList.tsx index f20cafd8875..9bfe2ca73fc 100644 --- a/app/src/organisms/OnDeviceDisplay/RunningProtocol/RunningProtocolCommandList.tsx +++ b/app/src/organisms/OnDeviceDisplay/RunningProtocol/RunningProtocolCommandList.tsx @@ -18,6 +18,7 @@ import { SPACING, LegacyStyledText, TYPOGRAPHY, + StyledText, } from '@opentrons/components' import { RUN_STATUS_RUNNING, RUN_STATUS_IDLE } from '@opentrons/api-client' @@ -221,8 +222,15 @@ export function RunningProtocolCommandList({ + + {index + 1} + { expect(mockShowModal).toHaveBeenCalled() }) + it("it displays the run's current action number", () => { + render({ ...props, currentRunCommandIndex: 11 }) + screen.getByText(12) + }) + // ToDo (kj:04/10/2023) once we fix the track event stuff, we can implement tests it.todo('when tapping play button, track event mock function is called') }) From 0a4b77488d4eef28ba8a7595791e770bfe7670c8 Mon Sep 17 00:00:00 2001 From: Ryan Howard Date: Mon, 22 Jul 2024 14:57:31 -0400 Subject: [PATCH 76/78] chore(hardware-testing): Add lpc protocols for lld testing (#15710) # Overview Adds protocols so we can run tests on other pipettes, and also fixes an issue with the 96 channel by slowing down the plunger enough so it doesn't stall # Test Plan # Changelog # Review requests # Risk assessment --- api/src/opentrons/hardware_control/ot3api.py | 8 ++++ .../hardware_testing/liquid_sense/__main__.py | 18 ++++---- .../liquid_sense_ot3_p1000_96_1well.py | 41 +++++++++++++++++++ .../liquid_sense_ot3_p1000_multi_12well.py | 32 +++++++++++++++ ...> liquid_sense_ot3_p1000_single_96well.py} | 2 +- ...y => liquid_sense_ot3_p50_multi_12well.py} | 3 +- ... => liquid_sense_ot3_p50_single_96well.py} | 4 +- 7 files changed, 96 insertions(+), 12 deletions(-) create mode 100644 hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p1000_96_1well.py create mode 100644 hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p1000_multi_12well.py rename hardware-testing/hardware_testing/protocols/liquid_sense_lpc/{liquid_sense_ot3_p1000_96_well.py => liquid_sense_ot3_p1000_single_96well.py} (93%) rename hardware-testing/hardware_testing/protocols/liquid_sense_lpc/{liquid_sense_ot3_p50_multi.py => liquid_sense_ot3_p50_multi_12well.py} (93%) rename hardware-testing/hardware_testing/protocols/liquid_sense_lpc/{liquid_sense_ot3_p50_single_vial.py => liquid_sense_ot3_p50_single_96well.py} (88%) diff --git a/api/src/opentrons/hardware_control/ot3api.py b/api/src/opentrons/hardware_control/ot3api.py index 6d567b7a667..ab3f94e232f 100644 --- a/api/src/opentrons/hardware_control/ot3api.py +++ b/api/src/opentrons/hardware_control/ot3api.py @@ -2631,6 +2631,14 @@ async def liquid_probe( ) max_speeds = self.config.motion_settings.default_max_speed p_prep_speed = max_speeds[self.gantry_load][OT3AxisKind.P] + # We need to significatly slow down the 96 channel liquid probe + if self.gantry_load == GantryLoad.HIGH_THROUGHPUT: + max_plunger_speed = self.config.motion_settings.max_speed_discontinuity[ + GantryLoad.HIGH_THROUGHPUT + ][OT3AxisKind.P] + probe_settings.plunger_speed = min( + max_plunger_speed, probe_settings.plunger_speed + ) error: Optional[PipetteLiquidNotFoundError] = None pos = await self.gantry_position(checked_mount, refresh=True) diff --git a/hardware-testing/hardware_testing/liquid_sense/__main__.py b/hardware-testing/hardware_testing/liquid_sense/__main__.py index ca0b632290c..f17c08677fd 100644 --- a/hardware-testing/hardware_testing/liquid_sense/__main__.py +++ b/hardware-testing/hardware_testing/liquid_sense/__main__.py @@ -37,9 +37,11 @@ from .post_process import process_csv_directory, process_google_sheet from hardware_testing.protocols.liquid_sense_lpc import ( - liquid_sense_ot3_p50_single_vial, - liquid_sense_ot3_p1000_96_well, - liquid_sense_ot3_p50_multi, + liquid_sense_ot3_p50_single_96well, + liquid_sense_ot3_p1000_96_1well, + liquid_sense_ot3_p1000_single_96well, + liquid_sense_ot3_p50_multi_12well, + liquid_sense_ot3_p1000_multi_12well, ) try: @@ -71,13 +73,13 @@ LIQUID_SENSE_CFG: Dict[int, Dict[int, Any]] = { 50: { - 1: liquid_sense_ot3_p50_single_vial, - 8: liquid_sense_ot3_p50_multi, + 1: liquid_sense_ot3_p50_single_96well, + 8: liquid_sense_ot3_p50_multi_12well, }, 1000: { - 1: liquid_sense_ot3_p1000_96_well, - 8: None, - 96: None, + 1: liquid_sense_ot3_p1000_single_96well, + 8: liquid_sense_ot3_p1000_multi_12well, + 96: liquid_sense_ot3_p1000_96_1well, }, } diff --git a/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p1000_96_1well.py b/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p1000_96_1well.py new file mode 100644 index 00000000000..ae89b4550a7 --- /dev/null +++ b/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p1000_96_1well.py @@ -0,0 +1,41 @@ +"""lld OT3 P1000.""" +from opentrons.protocol_api import ProtocolContext, OFF_DECK + +metadata = {"protocolName": "liquid-sense-ot3-p1000-96"} +requirements = {"robotType": "Flex", "apiLevel": "2.17"} + +SLOT_SCALE = 1 +SLOT_DIAL = 9 + +SLOTS_TIPRACK = { + 50: [2, 3, 4, 5, 6], + 200: [2, 3, 4, 5, 6], + 1000: [2, 3, 4, 5, 6], +} + +LABWARE_ON_SCALE = "nest_1_reservoir_195ml" + + +def run(ctx: ProtocolContext) -> None: + """Run.""" + trash = ctx.load_trash_bin("A3") + vial = ctx.load_labware(LABWARE_ON_SCALE, SLOT_SCALE) + dial = ctx.load_labware("dial_indicator", SLOT_DIAL) + pipette = ctx.load_instrument("flex_96channel_1000", "left") + adapters = [ + ctx.load_adapter("opentrons_flex_96_tiprack_adapter", slot) + for slot in SLOTS_TIPRACK[50] + ] + for size, slots in SLOTS_TIPRACK.items(): + tipracks = [ + adapter.load_labware(f"opentrons_flex_96_tiprack_{size}uL") + for adapter in adapters + ] + for rack in tipracks: + pipette.pick_up_tip(rack["A1"]) + pipette.aspirate(10, vial["A1"].top()) + pipette.dispense(10, vial["A1"].top()) + pipette.aspirate(10, dial["A1"].top()) + pipette.dispense(10, dial["A1"].top()) + pipette.drop_tip(trash) + ctx.move_labware(rack, OFF_DECK) diff --git a/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p1000_multi_12well.py b/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p1000_multi_12well.py new file mode 100644 index 00000000000..b77461598c8 --- /dev/null +++ b/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p1000_multi_12well.py @@ -0,0 +1,32 @@ +"""Liquid Sense OT3 P1000.""" +from opentrons.protocol_api import ProtocolContext, OFF_DECK + +metadata = {"protocolName": "liquid-sense-ot3-p50-multi"} +requirements = {"robotType": "Flex", "apiLevel": "2.15"} + +SLOT_SCALE = 1 +SLOT_DIAL = 9 +SLOTS_TIPRACK = { + 50: [3], + 200: [3], + 1000: [3], +} +LABWARE_ON_SCALE = "nest_12_reservoir_15ml" + + +def run(ctx: ProtocolContext) -> None: + """Run.""" + trash = ctx.load_trash_bin("A3") + vial = ctx.load_labware(LABWARE_ON_SCALE, SLOT_SCALE) + dial = ctx.load_labware("dial_indicator", SLOT_DIAL) + pipette = ctx.load_instrument("flex_8channel_1000", "left") + for size, slots in SLOTS_TIPRACK.items(): + for slot in slots: + rack = ctx.load_labware(f"opentrons_flex_96_tiprack_{size}uL", slot) + pipette.pick_up_tip(rack["A1"]) + pipette.aspirate(10, vial["A1"].top()) + pipette.dispense(10, vial["A1"].top()) + pipette.aspirate(10, dial["A1"].top()) + pipette.dispense(10, dial["A1"].top()) + pipette.drop_tip(trash) + ctx.move_labware(rack, OFF_DECK) diff --git a/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p1000_96_well.py b/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p1000_single_96well.py similarity index 93% rename from hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p1000_96_well.py rename to hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p1000_single_96well.py index 306abe2d48d..af96858af57 100644 --- a/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p1000_96_well.py +++ b/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p1000_single_96well.py @@ -1,7 +1,7 @@ """Liquid Sense OT3.""" from opentrons.protocol_api import ProtocolContext, OFF_DECK -metadata = {"protocolName": "liquid-sense-ot3-p1000-single-vial"} +metadata = {"protocolName": "liquid-sense-ot3-p1000-single-96well"} requirements = {"robotType": "Flex", "apiLevel": "2.17"} SLOT_SCALE = 1 diff --git a/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p50_multi.py b/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p50_multi_12well.py similarity index 93% rename from hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p50_multi.py rename to hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p50_multi_12well.py index 69d571f0259..455565001cf 100644 --- a/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p50_multi.py +++ b/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p50_multi_12well.py @@ -12,6 +12,7 @@ def run(ctx: ProtocolContext) -> None: """Run.""" + trash = ctx.load_trash_bin("A3") tipracks = [ ctx.load_labware(f"opentrons_flex_96_tiprack_{size}uL", slot) for size, slots in SLOTS_TIPRACK.items() @@ -26,4 +27,4 @@ def run(ctx: ProtocolContext) -> None: pipette.dispense(10, vial["A1"].top()) pipette.aspirate(1, dial["A1"].top()) pipette.dispense(1, dial["A1"].top()) - pipette.drop_tip(home_after=False) + pipette.drop_tip(trash) diff --git a/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p50_single_vial.py b/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p50_single_96well.py similarity index 88% rename from hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p50_single_vial.py rename to hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p50_single_96well.py index 7a7e607d08e..9b597074a34 100644 --- a/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p50_single_vial.py +++ b/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p50_single_96well.py @@ -1,7 +1,7 @@ """Liquid Sense OT3.""" from opentrons.protocol_api import ProtocolContext, OFF_DECK -metadata = {"protocolName": "liquid-sense-ot3-p50-single-vial"} +metadata = {"protocolName": "liquid-sense-ot3-p50-single-96well"} requirements = {"robotType": "Flex", "apiLevel": "2.17"} SLOT_SCALE = 1 @@ -9,7 +9,7 @@ SLOTS_TIPRACK = { 50: [3], } -LABWARE_ON_SCALE = "radwag_pipette_calibration_vial" +LABWARE_ON_SCALE = "corning_96_wellplate_360ul_flat" def run(ctx: ProtocolContext) -> None: From 80aa14e9a6ae8d9a55c3584d26d2371121de5758 Mon Sep 17 00:00:00 2001 From: Ryan Howard Date: Mon, 22 Jul 2024 16:14:05 -0400 Subject: [PATCH 77/78] fix(api): Lld movement adjustments (#15732) # Overview In order to remove some magic numbers we do a more clear calculation about where certain numbers in ot3api.py come from additionally we added a more robust move_to_plunger_top that incorporates backlash values, mostly due to an expectation that the 96 channel will need this due to it's current physical requirements for backlash # Test Plan # Changelog # Review requests # Risk assessment --------- Co-authored-by: Andy Sigler --- .../backends/flex_protocol.py | 1 + .../backends/ot3controller.py | 2 + .../hardware_control/backends/ot3simulator.py | 1 + api/src/opentrons/hardware_control/ot3api.py | 103 ++++++++++++++---- .../backends/test_ot3_controller.py | 1 + .../hardware_control/test_ot3_api.py | 19 ++-- .../hardware_control/tool_sensors.py | 7 +- .../hardware_control/test_tool_sensors.py | 2 + 8 files changed, 101 insertions(+), 35 deletions(-) diff --git a/api/src/opentrons/hardware_control/backends/flex_protocol.py b/api/src/opentrons/hardware_control/backends/flex_protocol.py index 6e96f3f3485..9e7218099cc 100644 --- a/api/src/opentrons/hardware_control/backends/flex_protocol.py +++ b/api/src/opentrons/hardware_control/backends/flex_protocol.py @@ -146,6 +146,7 @@ async def liquid_probe( mount_speed: float, plunger_speed: float, threshold_pascals: float, + plunger_impulse_time: float, output_format: OutputOptions = OutputOptions.can_bus_only, data_files: Optional[Dict[InstrumentProbeType, str]] = None, probe: InstrumentProbeType = InstrumentProbeType.PRIMARY, diff --git a/api/src/opentrons/hardware_control/backends/ot3controller.py b/api/src/opentrons/hardware_control/backends/ot3controller.py index e23cbcdd8c3..cd6aa9e112a 100644 --- a/api/src/opentrons/hardware_control/backends/ot3controller.py +++ b/api/src/opentrons/hardware_control/backends/ot3controller.py @@ -1357,6 +1357,7 @@ async def liquid_probe( mount_speed: float, plunger_speed: float, threshold_pascals: float, + plunger_impulse_time: float, output_option: OutputOptions = OutputOptions.can_bus_only, data_files: Optional[Dict[InstrumentProbeType, str]] = None, probe: InstrumentProbeType = InstrumentProbeType.PRIMARY, @@ -1387,6 +1388,7 @@ async def liquid_probe( plunger_speed=plunger_speed, mount_speed=mount_speed, threshold_pascals=threshold_pascals, + plunger_impulse_time=plunger_impulse_time, csv_output=csv_output, sync_buffer_output=sync_buffer_output, can_bus_only_output=can_bus_only_output, diff --git a/api/src/opentrons/hardware_control/backends/ot3simulator.py b/api/src/opentrons/hardware_control/backends/ot3simulator.py index 8e3a7f8990c..34c8fe0df68 100644 --- a/api/src/opentrons/hardware_control/backends/ot3simulator.py +++ b/api/src/opentrons/hardware_control/backends/ot3simulator.py @@ -345,6 +345,7 @@ async def liquid_probe( mount_speed: float, plunger_speed: float, threshold_pascals: float, + plunger_impulse_time: float, output_format: OutputOptions = OutputOptions.can_bus_only, data_files: Optional[Dict[InstrumentProbeType, str]] = None, probe: InstrumentProbeType = InstrumentProbeType.PRIMARY, diff --git a/api/src/opentrons/hardware_control/ot3api.py b/api/src/opentrons/hardware_control/ot3api.py index ab3f94e232f..cdc95bdd7de 100644 --- a/api/src/opentrons/hardware_control/ot3api.py +++ b/api/src/opentrons/hardware_control/ot3api.py @@ -1932,6 +1932,37 @@ async def _move_to_plunger_bottom( acquire_lock=acquire_lock, ) + async def _move_to_plunger_top_for_liquid_probe( + self, + mount: OT3Mount, + rate: float, + acquire_lock: bool = True, + ) -> None: + """ + Move an instrument's plunger to the top, to prepare for a following + liquid probe action. + + The plunger backlash distance (mm) is used to ensure the plunger is pre-loaded + in the downward direction. This means that the final position will not be + the plunger's configured "top" position, but "top" plus the "backlashDistance". + """ + max_speeds = self.config.motion_settings.default_max_speed + speed = max_speeds[self.gantry_load][OT3AxisKind.P] + instrument = self._pipette_handler.get_pipette(mount) + top_plunger_pos = target_position_from_plunger( + OT3Mount.from_mount(mount), + instrument.plunger_positions.top, + self._current_position, + ) + target_pos = top_plunger_pos.copy() + target_pos[Axis.of_main_tool_actuator(mount)] += instrument.backlash_distance + await self._move(top_plunger_pos, speed=speed * rate, acquire_lock=acquire_lock) + # NOTE: This should ALWAYS be moving DOWN. + # There should never be a time that this function is called and + # the plunger doesn't physically move DOWN. + # This is to make sure we are always engaged at the beginning of liquid-probe. + await self._move(target_pos, speed=speed * rate, acquire_lock=acquire_lock) + async def configure_for_volume( self, mount: Union[top_types.Mount, OT3Mount], volume: float ) -> None: @@ -2568,6 +2599,21 @@ def add_gripper_probe(self, probe: GripperProbe) -> None: def remove_gripper_probe(self) -> None: self._gripper_handler.remove_probe() + @staticmethod + def liquid_probe_non_responsive_z_distance(z_speed: float) -> float: + """Calculate the Z distance travelled where the LLD pass will be unresponsive.""" + # NOTE: (sigler) Here lye some magic numbers. + # The Z axis probing motion uses the first 20 samples to calculate + # a baseline for all following samples, making the very beginning of + # that Z motion unable to detect liquid. The sensor is configured for + # 4ms sample readings, and so we then assume it takes ~80ms to complete. + # If the Z is moving at 5mm/sec, then ~80ms equates to ~0.4 + baseline_during_z_sample_num = 20 # FIXME: (sigler) shouldn't be defined here? + sample_time_sec = 0.004 # FIXME: (sigler) shouldn't be defined here? + baseline_duration_sec = baseline_during_z_sample_num * sample_time_sec + non_responsive_z_mm = baseline_duration_sec * z_speed + return non_responsive_z_mm + async def _liquid_probe_pass( self, mount: OT3Mount, @@ -2583,6 +2629,7 @@ async def _liquid_probe_pass( probe_settings.mount_speed, (probe_settings.plunger_speed * plunger_direction), probe_settings.sensor_threshold_pascals, + probe_settings.plunger_impulse_time, probe_settings.output_option, probe_settings.data_files, probe=probe, @@ -2626,11 +2673,15 @@ async def liquid_probe( probe_start_pos = await self.gantry_position(checked_mount, refresh=True) - p_travel = ( + # plunger travel distance is from TOP->BOTTOM (minus the backlash distance + impulse) + # FIXME: logic for how plunger moves is divided between here and tool_sensors.py + p_impulse_mm = ( + probe_settings.plunger_impulse_time * probe_settings.plunger_speed + ) + p_total_mm = ( instrument.plunger_positions.bottom - instrument.plunger_positions.top ) - max_speeds = self.config.motion_settings.default_max_speed - p_prep_speed = max_speeds[self.gantry_load][OT3AxisKind.P] + # We need to significatly slow down the 96 channel liquid probe if self.gantry_load == GantryLoad.HIGH_THROUGHPUT: max_plunger_speed = self.config.motion_settings.max_speed_discontinuity[ @@ -2640,24 +2691,44 @@ async def liquid_probe( max_plunger_speed, probe_settings.plunger_speed ) + p_working_mm = p_total_mm - (instrument.backlash_distance + p_impulse_mm) + + # height where probe action will begin + # TODO: (sigler) add this to pipette's liquid def (per tip) + probe_pass_overlap_mm = 0.1 + non_responsive_z_mm = OT3API.liquid_probe_non_responsive_z_distance( + probe_settings.mount_speed + ) + probe_pass_z_offset_mm = non_responsive_z_mm + probe_pass_overlap_mm + + # height that is considered safe to reset the plunger without disturbing liquid + # this usually needs to at least 1-2mm from liquid, to avoid splashes from air + # TODO: (sigler) add this to pipette's liquid def (per tip) + probe_safe_reset_mm = max(2.0, probe_pass_z_offset_mm) + error: Optional[PipetteLiquidNotFoundError] = None pos = await self.gantry_position(checked_mount, refresh=True) while (probe_start_pos.z - pos.z) < max_z_dist: # safe distance so we don't accidentally aspirate liquid if we're already close to liquid - safe_plunger_pos = top_types.Point(pos.x, pos.y, pos.z + 2) + safe_plunger_pos = top_types.Point( + pos.x, pos.y, pos.z + probe_safe_reset_mm + ) # overlap amount we want to use between passes - pass_start_pos = top_types.Point(pos.x, pos.y, pos.z + 0.5) + pass_start_pos = top_types.Point( + pos.x, pos.y, pos.z + probe_pass_z_offset_mm + ) max_z_time = ( max_z_dist - (probe_start_pos.z - safe_plunger_pos.z) ) / probe_settings.mount_speed - pass_travel = min(max_z_time * probe_settings.plunger_speed, p_travel) + p_travel_required_for_z = max_z_time * probe_settings.plunger_speed + p_pass_travel = min(p_travel_required_for_z, p_working_mm) # Prep the plunger await self.move_to(checked_mount, safe_plunger_pos) if probe_settings.aspirate_while_sensing: # TODO(cm, 7/8/24): remove p_prep_speed from the rate at some point - await self._move_to_plunger_bottom(checked_mount, rate=p_prep_speed) + await self._move_to_plunger_bottom(checked_mount, rate=1) else: - await self._move_to_plunger_top(checked_mount, rate=p_prep_speed) + await self._move_to_plunger_top_for_liquid_probe(checked_mount, rate=1) try: # move to where we want to start a pass and run a pass @@ -2666,7 +2737,7 @@ async def liquid_probe( checked_mount, probe_settings, probe if probe else InstrumentProbeType.PRIMARY, - pass_travel, + p_pass_travel + p_impulse_mm, ) # if we made it here without an error we found the liquid error = None @@ -2682,20 +2753,6 @@ async def liquid_probe( raise error return height - async def _move_to_plunger_top( - self, - mount: OT3Mount, - rate: float, - acquire_lock: bool = True, - ) -> None: - instrument = self._pipette_handler.get_pipette(mount) - target_pos = target_position_from_plunger( - OT3Mount.from_mount(mount), - instrument.plunger_positions.top, - self._current_position, - ) - await self._move(target_pos, speed=rate, acquire_lock=acquire_lock) - async def capacitive_probe( self, mount: OT3Mount, diff --git a/api/tests/opentrons/hardware_control/backends/test_ot3_controller.py b/api/tests/opentrons/hardware_control/backends/test_ot3_controller.py index ae5385ff1f9..fa57c4347ff 100644 --- a/api/tests/opentrons/hardware_control/backends/test_ot3_controller.py +++ b/api/tests/opentrons/hardware_control/backends/test_ot3_controller.py @@ -719,6 +719,7 @@ async def test_liquid_probe( mount_speed=fake_liquid_settings.mount_speed, plunger_speed=fake_liquid_settings.plunger_speed, threshold_pascals=fake_liquid_settings.sensor_threshold_pascals, + plunger_impulse_time=fake_liquid_settings.plunger_impulse_time, output_option=fake_liquid_settings.output_option, ) except PipetteLiquidNotFoundError: diff --git a/api/tests/opentrons/hardware_control/test_ot3_api.py b/api/tests/opentrons/hardware_control/test_ot3_api.py index 2b77ebdcd00..0c1fff849c0 100644 --- a/api/tests/opentrons/hardware_control/test_ot3_api.py +++ b/api/tests/opentrons/hardware_control/test_ot3_api.py @@ -116,8 +116,8 @@ def fake_settings() -> CapacitivePassSettings: @pytest.fixture def fake_liquid_settings() -> LiquidProbeSettings: return LiquidProbeSettings( - mount_speed=40, - plunger_speed=10, + mount_speed=5, + plunger_speed=20, plunger_impulse_time=0.2, sensor_threshold_pascals=15, output_option=OutputOptions.can_bus_only, @@ -825,8 +825,8 @@ async def test_liquid_probe( # make sure aspirate while sensing reverses direction mock_liquid_probe.return_value = return_dict fake_settings_aspirate = LiquidProbeSettings( - mount_speed=40, - plunger_speed=10, + mount_speed=5, + plunger_speed=20, plunger_impulse_time=0.2, sensor_threshold_pascals=15, output_option=OutputOptions.can_bus_only, @@ -838,10 +838,11 @@ async def test_liquid_probe( mock_move_to_plunger_bottom.call_count == 2 mock_liquid_probe.assert_called_once_with( mount, - 3.0, + 52, fake_settings_aspirate.mount_speed, (fake_settings_aspirate.plunger_speed * -1), fake_settings_aspirate.sensor_threshold_pascals, + fake_settings_aspirate.plunger_impulse_time, fake_settings_aspirate.output_option, fake_settings_aspirate.data_files, probe=InstrumentProbeType.PRIMARY, @@ -913,10 +914,11 @@ async def test_multi_liquid_probe( assert mock_move_to_plunger_bottom.call_count == 4 mock_liquid_probe.assert_called_with( OT3Mount.LEFT, - plunger_positions.bottom - plunger_positions.top, + plunger_positions.bottom - plunger_positions.top - 0.1, fake_settings_aspirate.mount_speed, (fake_settings_aspirate.plunger_speed * -1), fake_settings_aspirate.sensor_threshold_pascals, + fake_settings_aspirate.plunger_impulse_time, fake_settings_aspirate.output_option, fake_settings_aspirate.data_files, probe=InstrumentProbeType.PRIMARY, @@ -954,6 +956,7 @@ async def _fake_pos_update_and_raise( mount_speed: float, plunger_speed: float, threshold_pascals: float, + plunger_impulse_time: float, output_format: OutputOptions = OutputOptions.can_bus_only, data_files: Optional[Dict[InstrumentProbeType, str]] = None, probe: InstrumentProbeType = InstrumentProbeType.PRIMARY, @@ -986,8 +989,8 @@ async def _fake_pos_update_and_raise( await ot3_hardware.liquid_probe( OT3Mount.LEFT, fake_max_z_dist, fake_settings_aspirate ) - # assert that it went through 3 passes and then prepared to aspirate - assert mock_move_to_plunger_bottom.call_count == 4 + # assert that it went through 4 passes and then prepared to aspirate + assert mock_move_to_plunger_bottom.call_count == 5 @pytest.mark.parametrize( diff --git a/hardware/opentrons_hardware/hardware_control/tool_sensors.py b/hardware/opentrons_hardware/hardware_control/tool_sensors.py index 6b762ef7c30..eeb4736a6d9 100644 --- a/hardware/opentrons_hardware/hardware_control/tool_sensors.py +++ b/hardware/opentrons_hardware/hardware_control/tool_sensors.py @@ -82,8 +82,6 @@ # FIXME we should organize all of these functions to use the sensor drivers. # FIXME we should restrict some of these functions by instrument type. -PLUNGER_SOLO_MOVE_TIME = 0.2 - def _fix_pass_step_for_buffer( move_group: MoveGroupStep, @@ -393,6 +391,7 @@ async def liquid_probe( plunger_speed: float, mount_speed: float, threshold_pascals: float, + plunger_impulse_time: float, csv_output: bool = False, sync_buffer_output: bool = False, can_bus_only_output: bool = False, @@ -425,7 +424,7 @@ async def liquid_probe( sensor_driver, True, ) - p_prep_distance = float(PLUNGER_SOLO_MOVE_TIME * plunger_speed) + p_prep_distance = float(plunger_impulse_time * plunger_speed) p_pass_distance = float(max_p_distance - p_prep_distance) max_z_distance = (p_pass_distance / plunger_speed) * mount_speed @@ -433,7 +432,7 @@ async def liquid_probe( distance={tool: float64(p_prep_distance)}, velocity={tool: float64(plunger_speed)}, acceleration={}, - duration=float64(PLUNGER_SOLO_MOVE_TIME), + duration=float64(plunger_impulse_time), present_nodes=[tool], ) sensor_group = _build_pass_step( diff --git a/hardware/tests/opentrons_hardware/hardware_control/test_tool_sensors.py b/hardware/tests/opentrons_hardware/hardware_control/test_tool_sensors.py index 86a1d2d40a7..f4dddc8ca37 100644 --- a/hardware/tests/opentrons_hardware/hardware_control/test_tool_sensors.py +++ b/hardware/tests/opentrons_hardware/hardware_control/test_tool_sensors.py @@ -217,6 +217,7 @@ def move_responder( mount_speed=10, plunger_speed=8, threshold_pascals=threshold_pascals, + plunger_impulse_time=0.2, csv_output=False, sync_buffer_output=False, can_bus_only_output=False, @@ -348,6 +349,7 @@ def move_responder( mount_speed=10, plunger_speed=8, threshold_pascals=14, + plunger_impulse_time=0.2, csv_output=csv_output, sync_buffer_output=sync_buffer_output, can_bus_only_output=can_bus_only_output, From 6b8f7fd121bca40e300fbad7da4226c30ae1f1c7 Mon Sep 17 00:00:00 2001 From: Seth Foster Date: Tue, 23 Jul 2024 09:54:48 -0400 Subject: [PATCH 78/78] fix(app): overpressure while aspirate styling (#15748) - Fix up styles for the overpressure while aspirating screens Closes EXEC-558 --- .../localization/en/error_recovery.json | 4 +- app/src/molecules/Command/Command.tsx | 39 ++++++++----------- app/src/molecules/Command/CommandText.tsx | 5 +-- .../ErrorRecoveryFlows/shared/ReplaceTips.tsx | 8 ++-- 4 files changed, 26 insertions(+), 30 deletions(-) diff --git a/app/src/assets/localization/en/error_recovery.json b/app/src/assets/localization/en/error_recovery.json index 53d2a07df43..eeaf8980cc8 100644 --- a/app/src/assets/localization/en/error_recovery.json +++ b/app/src/assets/localization/en/error_recovery.json @@ -41,8 +41,8 @@ "recovery_mode": "Recovery Mode", "recovery_mode_explanation": "Recovery Mode provides you with guided and manual controls for handling errors at runtime.
    You can make changes to ensure the step in progress when the error occurred can be completed or choose to cancel the protocol. When changes are made and no subsequent errors are detected, the method completes. Depending on the conditions that caused the error, you will only be provided with appropriate options.", "replace_tips_and_select_location": "It's best to replace tips and select the last location used for tip pickup.", - "replace_used_tips_in_rack_location": "Replace used tips in rack location {{location}}", - "replace_with_new_tip_rack": "Replace with new tip rack", + "replace_used_tips_in_rack_location": "Replace used tips in rack location {{location}} in slot {{slot}}", + "replace_with_new_tip_rack": "Replace with new tip rack in slot {{slot}}", "resume": "Resume", "retry_now": "Retry now", "retry_step": "Retry step", diff --git a/app/src/molecules/Command/Command.tsx b/app/src/molecules/Command/Command.tsx index b1c2c935eb7..fb8452f2a92 100644 --- a/app/src/molecules/Command/Command.tsx +++ b/app/src/molecules/Command/Command.tsx @@ -164,15 +164,11 @@ export function CenteredCommand(
    @@ -224,15 +220,11 @@ export function LeftAlignedCommand(
    @@ -242,14 +234,17 @@ export function LeftAlignedCommand( const TEXT_CLIP_STYLE = ` display: -webkit-box; - -webkit-box-orient: vertical; - overflow: hidden; - text-overflow: ellipsis; - word-wrap: break-word; - -webkit-line-clamp: 2; -} + -webkit-box-orient: vertical; + overflow: hidden; + text-overflow: ellipsis; + word-wrap: break-word; + -webkit-line-clamp: 2; ` const ODD_ONLY_TEXT_CLIP_STYLE = ` + @media not (${RESPONSIVENESS.touchscreenMediaQuerySpecs}) { + max-height: 240px; + overflow: auto; + } @media (${RESPONSIVENESS.touchscreenMediaQuerySpecs}) { ${TEXT_CLIP_STYLE} } diff --git a/app/src/molecules/Command/CommandText.tsx b/app/src/molecules/Command/CommandText.tsx index f579c41ea8d..75b12733eca 100644 --- a/app/src/molecules/Command/CommandText.tsx +++ b/app/src/molecules/Command/CommandText.tsx @@ -9,7 +9,6 @@ import { LegacyStyledText, StyledText, RESPONSIVENESS, - styleProps, } from '@opentrons/components' import { useCommandTextString } from './hooks' @@ -78,14 +77,14 @@ function CommandStyledText( {props.children} ) } else { return ( - + {props.children} ) diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/ReplaceTips.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/ReplaceTips.tsx index f7513af14c8..9d7f8adfcd7 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/ReplaceTips.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/ReplaceTips.tsx @@ -7,6 +7,7 @@ import { RecoverySingleColumnContentWrapper } from './RecoveryContentWrapper' import { TwoColumn, DeckMapContent } from '../../../molecules/InterventionModal' import { RecoveryFooterButtons } from './RecoveryFooterButtons' import { LeftColumnLabwareInfo } from './LeftColumnLabwareInfo' +import { getSlotNameAndLwLocFrom } from '../hooks/useDeckMapUtils' import type { RecoveryContentProps } from '../types' @@ -17,20 +18,21 @@ export function ReplaceTips(props: RecoveryContentProps): JSX.Element | null { failedLabwareUtils, deckMapUtils, } = props - const { relevantWellName } = failedLabwareUtils + const { relevantWellName, failedLabware } = failedLabwareUtils const { proceedNextStep } = routeUpdateActions const { t } = useTranslation('error_recovery') const primaryOnClick = (): void => { void proceedNextStep() } - + const [slot] = getSlotNameAndLwLocFrom(failedLabware?.location ?? null, false) const buildTitle = (): string => { if (failedPipetteInfo?.data.channels === 96) { - return t('replace_with_new_tip_rack') + return t('replace_with_new_tip_rack', { slot }) } else { return t('replace_used_tips_in_rack_location', { location: relevantWellName, + slot, }) } }