Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(app): ODD protocol detail and protocol setup status for deck configuration #13855

Merged
merged 11 commits into from
Nov 7, 2023
8 changes: 5 additions & 3 deletions app/src/assets/localization/en/device_details.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,13 @@
"magdeck_gen1_height": "Height: {{height}}",
"magdeck_gen2_height": "Height: {{height}} mm",
"max_engage_height": "Max Engage Height",
"missing_both": "missing hardware",
"missing_hardware": "missing hardware",
"missing_module_plural": "missing {{count}} modules",
"missing_module": "missing {{num}} module",
"missing_pipette": "missing {{num}} pipette",
"missing_pipettes_plural": "missing {{count}} pipettes",
"missing_instrument": "missing {{num}} instrument",
"missing_instruments_plural": "missing {{count}} instruments",
"missing_fixture": "missing {{num}} fixture",
"missing_fixtures_plural": "missing {{count}} fixtures",
"module_actions_unavailable": "Module actions unavailable while protocol is running",
"module_calibration_required_no_pipette_attached": "Module calibration required. Attach a pipette before running module calibration.",
"module_calibration_required_update_pipette_FW": "Update pipette firmware before proceeding with required module calibration.",
Expand Down
14 changes: 12 additions & 2 deletions app/src/assets/localization/en/protocol_setup.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,14 +62,19 @@
"feedback_form_link": "Let us know!",
"fixture_name": "fixture",
"fixture": "Fixture",
"fixtures_connected_plural": "{{count}} fixtures attached",
"fixture_connected": "{{count}} fixture attached",
ncdiehl11 marked this conversation as resolved.
Show resolved Hide resolved
"get_labware_offset_data": "Get Labware Offset Data",
"hardware_missing": "Hardware missing",
"heater_shaker_extra_attention": "Use latch controls for easy placement of labware.",
"heater_shaker_labware_list_view": "To add labware, use the toggle to control the latch",
"how_offset_data_works": "How labware offsets work",
"initial_liquids_num_plural": "{{count}} initial liquids",
"initial_liquids_num": "{{count}} initial liquid",
"initial_location": "Initial Location",
"install_modules_and_fixtures": "Install the required modules and power them on. Install the required fixtures and review the deck configuration.",
"instrument_calibrations_missing_plural": "Missing {{num}} calibrations",
ncdiehl11 marked this conversation as resolved.
Show resolved Hide resolved
"instrument_calibration_missing": "Missing {{num}} calibration",
"instruments_connected_plural": "{{count}} instruments attached",
"instruments_connected": "{{count}} instrument attached",
"instruments": "Instruments",
Expand Down Expand Up @@ -114,6 +119,10 @@
"magnetic_module_extra_attention": "Opentrons recommends securing labware with the module’s bracket",
"map_view": "Map View",
"missing": "Missing",
"missing_gripper": "Missing gripper",
"missing_instruments_plural": "Missing {{num}}",
ncdiehl11 marked this conversation as resolved.
Show resolved Hide resolved
"missing_pipettes_plural": "Missing {{num}} pipettes",
"missing_pipette": "Missing {{num}} pipette",
"modal_instructions_title": "{{moduleName}} Setup Instructions",
"modal_instructions": "For step-by-step instructions on setting up your module, consult the Quickstart Guide that came in its box. You can also click the link below or scan the QR code to visit the modules section of the Opentrons Help Center.",
"module_and_deck_setup": "Modules & deck",
Expand All @@ -135,11 +144,12 @@
"modules": "Modules",
"mount_title": "{{mount}} MOUNT:",
"mount": "{{mount}} mount",
"multiple_fixtures_missing": "{{count}} fixtures missing",
"multiple_modules_example": "Your protocol has two Temperature Modules. The Temperature Module attached to the first port starting from the left will be related to the first Temperature Module in your protocol while the second Temperature Module loaded would be related to the Temperature Module connected to the next port to the right. If using a hub, follow the same logic with the port ordering.",
"multiple_modules_explanation": "To use more than one of the same module in a protocol, you first need to plug in the module that’s called first in your protocol to the lowest numbered USB port on the robot. Continue in the same manner with additional modules.",
"multiple_modules_help_link_title": "See How To Set Up Modules of the Same Type",
"multiple_modules_learn_more": "Learn more about using multiple modules of the same type",
"multiple_modules_missing": "Multiple modules missing",
"multiple_modules_missing": "{{count}} modules missing",
ncdiehl11 marked this conversation as resolved.
Show resolved Hide resolved
"multiple_modules_modal": "Setting up multiple modules of the same type",
"multiple_modules": "Multiple modules of the same type",
"multiple_of_most_modules": "You can use multiples of most module types within a single Python protocol by connecting and loading the modules in a specific order. The robot will initialize the matching module attached to the lowest numbered port first, regardless of what deck slot it occupies.",
Expand All @@ -149,7 +159,7 @@
"no_labware_offset_data": "no labware offset data yet",
"no_modules_or_fixtures": "No modules or fixtures are specified for this protocol.",
"no_modules_specified": "no modules are specified for this protocol.",
"no_modules_used_in_this_protocol": "No modules used in this protocol",
"no_modules_used_in_this_protocol": "No hardware used in this protocol",
"no_tiprack_loaded": "Protocol must load a tip rack",
"no_tiprack_used": "Protocol must pick up a tip",
"no_usb_connection_required": "No USB connection required",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ describe('useHardwareStatusText', () => {
)
expect(result.current).toEqual('Missing 2 modules')
})
it('should return missing 1 pipette', () => {
it('should return missing 1 instrument', () => {
const { result } = renderHook(
() =>
useHardwareStatusText(
Expand All @@ -93,9 +93,9 @@ describe('useHardwareStatusText', () => {
wrapper,
}
)
expect(result.current).toEqual('Missing 1 pipette')
expect(result.current).toEqual('Missing 1 instrument')
})
it('should return missing 2 pipettes', () => {
it('should return missing 2 instruments', () => {
const { result } = renderHook(
() =>
useHardwareStatusText(
Expand All @@ -119,7 +119,7 @@ describe('useHardwareStatusText', () => {
wrapper,
}
)
expect(result.current).toEqual('Missing 2 pipettes')
expect(result.current).toEqual('Missing 2 instruments')
})
it('should return missing hardware', () => {
const { result } = renderHook(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,51 +1,72 @@
import { useTranslation } from 'react-i18next'

import { useFeatureFlag } from '../../../../redux/config'

import type { ProtocolHardware } from '../../../../pages/Protocols/hooks'

export function useHardwareStatusText(
ncdiehl11 marked this conversation as resolved.
Show resolved Hide resolved
missingProtocolHardware: ProtocolHardware[],
conflictedSlots: string[]
): string {
const { t, i18n } = useTranslation('device_details')
const enableDeckConfig = useFeatureFlag('enableDeckConfiguration')

const missingProtocolHardwareType = missingProtocolHardware.map(
hardware => hardware.hardwareType
const missingProtocolHardwareType = missingProtocolHardware.map(hardware =>
hardware.hardwareType === 'pipette' || hardware.hardwareType === 'gripper'
? 'instrument'
: hardware.hardwareType
)
const countMissingHardwareType = (hwType: 'pipette' | 'module'): number => {
const countMissingHardwareType = (
hwType: 'instrument' | 'module' | 'fixture'
): number => {
return missingProtocolHardwareType.filter(
hardwareType => hardwareType === hwType
).length
}
const countMissingPipettes = countMissingHardwareType('pipette')

const countMissingInstruments = countMissingHardwareType('instrument')
const countMissingModules = countMissingHardwareType('module')
let chipText: string = t('ready_to_run')
if (enableDeckConfig && conflictedSlots.length > 0) {
const countMissingFixtures = countMissingHardwareType('fixture')

const noHardwareMissing =
[countMissingInstruments, countMissingModules, countMissingFixtures].filter(
count => count > 0
).length === 0
const multipleHardwareTypesMissing =
[countMissingInstruments, countMissingModules, countMissingFixtures].filter(
count => count > 0
).length > 1

let chipText: string

if (noHardwareMissing) {
chipText = t('ready_to_run')
} else if (conflictedSlots.length > 0) {
chipText = t('location_conflicts')
} else if (countMissingPipettes === 0 && countMissingModules > 0) {
if (countMissingModules === 1) {
chipText = t('missing_module', {
num: countMissingModules,
})
} else {
chipText = t('missing_module_plural', {
count: countMissingModules,
})
}
} else if (countMissingPipettes > 0 && countMissingModules === 0) {
if (countMissingPipettes === 1) {
chipText = t('missing_pipette', {
num: countMissingPipettes,
})
} else if (multipleHardwareTypesMissing) {
chipText = t('missing_hardware')
} else {
// exactly one hardware type missing
if (countMissingFixtures > 0) {
chipText =

Check warning on line 47 in app/src/organisms/OnDeviceDisplay/RobotDashboard/hooks/useHardwareStatusText.ts

View check run for this annotation

Codecov / codecov/patch

app/src/organisms/OnDeviceDisplay/RobotDashboard/hooks/useHardwareStatusText.ts#L47

Added line #L47 was not covered by tests
countMissingFixtures === 1
? t('missing_fixture', { num: countMissingFixtures })
: t('missing_fixtures_plural', { count: countMissingFixtures })
} else if (countMissingModules > 0) {
chipText =
countMissingModules === 1
? t('missing_module', {
num: countMissingModules,
})
: t('missing_module_plural', {
count: countMissingModules,
})
} else {
chipText = t('missing_pipettes_plural', {
count: countMissingPipettes,
})
chipText =
countMissingInstruments === 1
? t('missing_instrument', {
num: countMissingInstruments,
})
: t('missing_instruments_plural', {
count: countMissingInstruments,
})
}
} else if (countMissingPipettes > 0 && countMissingModules > 0) {
chipText = t('missing_both')
}
return i18n.format(chipText, 'capitalize')
}
22 changes: 22 additions & 0 deletions app/src/organisms/ProtocolSetupInstruments/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,25 @@ export function getAreInstrumentsReady(

return allSpeccedPipettesReady && isExtensionMountReady
}

export function getNumUnreadyInstruments(
ncdiehl11 marked this conversation as resolved.
Show resolved Hide resolved
analysis: CompletedProtocolAnalysis,
attachedInstruments: Instruments
): number {
const speccedPipettes = analysis?.pipettes ?? []

const numUnreadySpeccedPipettes = speccedPipettes.filter(loadedPipette => {
ncdiehl11 marked this conversation as resolved.
Show resolved Hide resolved
const attachedPipetteMatch = getPipetteMatch(
loadedPipette,
attachedInstruments
)
return attachedPipetteMatch?.data.calibratedOffset?.last_modified == null
}).length

const isExtensionMountReady = getProtocolUsesGripper(analysis)
? getAttachedGripper(attachedInstruments)?.data.calibratedOffset
?.last_modified != null
: true

return numUnreadySpeccedPipettes + (isExtensionMountReady ? 0 : 1)
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,13 @@ describe('Hardware', () => {
hardwareType: 'fixture',
fixtureName: WASTE_CHUTE_LOAD_NAME,
location: { cutout: WASTE_CHUTE_SLOT },
hasSlotConflict: false,
},
{
hardwareType: 'fixture',
fixtureName: STAGING_AREA_LOAD_NAME,
location: { cutout: 'B3' },
hasSlotConflict: false,
},
],
isLoading: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as React from 'react'
import { Route } from 'react-router'
import { MemoryRouter } from 'react-router-dom'
import { when, resetAllWhenMocks } from 'jest-when'
import { UseQueryResult } from 'react-query'
ncdiehl11 marked this conversation as resolved.
Show resolved Hide resolved

import { RUN_STATUS_IDLE } from '@opentrons/api-client'
import {
Expand All @@ -10,11 +11,15 @@ import {
useRunQuery,
useProtocolQuery,
useDoorQuery,
useModulesQuery,
useDeckConfigurationQuery,
} from '@opentrons/react-api-client'
import { renderWithProviders } from '@opentrons/components'
import { mockHeaterShaker } from '../../../../redux/modules/__fixtures__'
import {
FLEX_ROBOT_TYPE,
getDeckDefFromRobotType,
STAGING_AREA_LOAD_NAME,
} from '@opentrons/shared-data'
import ot3StandardDeckDef from '@opentrons/shared-data/deck/definitions/3/ot3_standard.json'

Expand Down Expand Up @@ -46,7 +51,11 @@ import { ConfirmAttachedModal } from '../ConfirmAttachedModal'
import { useFeatureFlag } from '../../../../redux/config'
import { ProtocolSetup } from '..'

import type { CompletedProtocolAnalysis } from '@opentrons/shared-data'
import type {
DeckConfiguration,
CompletedProtocolAnalysis,
Fixture,
} from '@opentrons/shared-data'

// Mock IntersectionObserver
class IntersectionObserver {
Expand Down Expand Up @@ -143,6 +152,12 @@ const mockConfirmAttachedModal = ConfirmAttachedModal as jest.MockedFunction<
const mockUseDoorQuery = useDoorQuery as jest.MockedFunction<
typeof useDoorQuery
>
const mockUseModulesQuery = useModulesQuery as jest.MockedFunction<
typeof useModulesQuery
>
const mockUseDeckConfigurationQuery = useDeckConfigurationQuery as jest.MockedFunction<
typeof useDeckConfigurationQuery
>
const mockUseToaster = useToaster as jest.MockedFunction<typeof useToaster>
const mockUseFeatureFlag = useFeatureFlag as jest.MockedFunction<
typeof useFeatureFlag
Expand Down Expand Up @@ -219,6 +234,12 @@ const mockDoorStatus = {
doorRequiredClosedForProtocol: true,
},
}
const mockFixture = {
fixtureId: 'mockId',
fixtureLocation: 'D1',
loadName: STAGING_AREA_LOAD_NAME,
} as Fixture

const MOCK_MAKE_SNACKBAR = jest.fn()

describe('ProtocolSetup', () => {
Expand Down Expand Up @@ -305,6 +326,12 @@ describe('ProtocolSetup', () => {
<div>mock ConfirmAttachedModal</div>
)
mockUseDoorQuery.mockReturnValue({ data: mockDoorStatus } as any)
mockUseModulesQuery.mockReturnValue({
data: { data: [mockHeaterShaker] },
} as any)
when(mockUseDeckConfigurationQuery).mockReturnValue({
data: [mockFixture],
} as UseQueryResult<DeckConfiguration>)
ncdiehl11 marked this conversation as resolved.
Show resolved Hide resolved
when(mockUseToaster)
.calledWith()
.mockReturnValue(({
Expand Down
Loading
Loading