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, shared-data, step-generation): frontend low volume support #13526

Merged
merged 13 commits into from
Sep 13, 2023
Merged
2 changes: 2 additions & 0 deletions app/src/assets/localization/en/protocol_command_text.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"aspirate": "Aspirating {{volume}} µL from well {{well_name}} of {{labware}} in {{labware_location}} at {{flow_rate}} µL/sec",
"blowout": "Blowing out at well {{well_name}} of {{labware}} in {{labware_location}} at {{flow_rate}} µL/sec",
"configure_for_volume": "Configure {{pipette}} to aspirate {{volume}} µL",
"closing_tc_lid": "Closing Thermocycler lid",
"comment": "Comment",
"confirm_and_resume": "Confirm and resume",
Expand All @@ -12,6 +13,7 @@
"degrees_c": "{{temp}}°C",
"disengaging_magnetic_module": "Disengaging Magnetic Module",
"dispense": "Dispensing {{volume}} µL into well {{well_name}} of {{labware}} in {{labware_location}} at {{flow_rate}} µL/sec",
"dispense_push_out": "Dispensing {{volume}} µL into well {{well_name}} of {{labware}} in {{labware_location}} at {{flow_rate}} µL/sec and pushing out {{push_out_volume}} µL",
"drop_tip": "Dropping tip in {{well_name}} of {{labware}}",
"engaging_magnetic_module": "Engaging Magnetic Module",
"fixed_trash": "Fixed Trash",
Expand Down
25 changes: 17 additions & 8 deletions app/src/organisms/CommandText/PipettingCommandText.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,23 @@ export const PipettingCommandText = ({
})
}
case 'dispense': {
const { volume, flowRate } = command.params
return t('dispense', {
well_name: wellName,
labware: getLabwareName(robotSideAnalysis, labwareId),
labware_location: displayLocation,
volume: volume,
flow_rate: flowRate,
})
const { volume, flowRate, pushOut } = command.params
return pushOut
? t('dispense_push_out', {
well_name: wellName,
labware: getLabwareName(robotSideAnalysis, labwareId),
labware_location: displayLocation,
volume: volume,
flow_rate: flowRate,
push_out_volume: pushOut,
})
: t('dispense', {
well_name: wellName,
labware: getLabwareName(robotSideAnalysis, labwareId),
labware_location: displayLocation,
volume: volume,
flow_rate: flowRate,
})
}
case 'blowout': {
const { flowRate } = command.params
Expand Down
51 changes: 49 additions & 2 deletions app/src/organisms/CommandText/__tests__/CommandText.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ import { CommandText } from '../'
import { mockRobotSideAnalysis } from '../__fixtures__'

import type { MoveToWellRunTimeCommand } from '@opentrons/shared-data/protocol/types/schemaV7/command/gantry'
import type { BlowoutRunTimeCommand } from '@opentrons/shared-data/protocol/types/schemaV7/command/pipetting'
import type {
BlowoutRunTimeCommand,
DispenseRunTimeCommand,
ConfigureForVolumeRunTimeCommand,
} from '@opentrons/shared-data/protocol/types/schemaV7/command/pipetting'
import type {
LoadLabwareRunTimeCommand,
LoadLiquidRunTimeCommand,
Expand All @@ -31,7 +35,7 @@ describe('CommandText', () => {
)
}
})
it('renders correct text for dispense', () => {
it('renders correct text for dispense without pushOut', () => {
const command = mockRobotSideAnalysis.commands.find(
c => c.commandType === 'dispense'
)
Expand All @@ -49,6 +53,31 @@ describe('CommandText', () => {
)
}
})
it('renders correct text for dispense with pushOut', () => {
const command = mockRobotSideAnalysis.commands.find(
c => c.commandType === 'dispense'
)
const pushOutDispenseCommand = {
...command,
params: {
...command?.params,
pushOut: 10,
},
} as DispenseRunTimeCommand
expect(pushOutDispenseCommand).not.toBeUndefined()
if (pushOutDispenseCommand != null) {
const { getByText } = renderWithProviders(
<CommandText
robotSideAnalysis={mockRobotSideAnalysis}
command={pushOutDispenseCommand}
/>,
{ i18nInstance: i18n }
)[0]
getByText(
'Dispensing 100 µL into well A1 of NEST 96 Well Plate 100 µL PCR Full Skirt (1) in Magnetic Module GEN2 in Slot 1 at 300 µL/sec and pushing out 10 µL'
)
}
})
it('renders correct text for blowout', () => {
const dispenseCommand = mockRobotSideAnalysis.commands.find(
c => c.commandType === 'dispense'
Expand Down Expand Up @@ -91,6 +120,24 @@ describe('CommandText', () => {
getByText('Moving to well A1 of NEST 1 Well Reservoir 195 mL in Slot 5')
}
})
it('renders correct text for configureForVolume', () => {
const command = {
commandType: 'configureForVolume',
params: {
volume: 1,
pipetteId: 'f6d1c83c-9d1b-4d0d-9de3-e6d649739cfb',
},
} as ConfigureForVolumeRunTimeCommand

const { getByText } = renderWithProviders(
<CommandText
robotSideAnalysis={mockRobotSideAnalysis}
command={command}
/>,
{ i18nInstance: i18n }
)[0]
getByText('Configure P300 Single-Channel GEN1 to aspirate 1 µL')
})
it('renders correct text for dropTip', () => {
const command = mockRobotSideAnalysis.commands.find(
c => c.commandType === 'dropTip'
Expand Down
20 changes: 19 additions & 1 deletion app/src/organisms/CommandText/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ import * as React from 'react'
import { useTranslation } from 'react-i18next'

import { Flex, DIRECTION_COLUMN, SPACING } from '@opentrons/components'
import { getPipetteNameSpecs, RunTimeCommand } from '@opentrons/shared-data'
import { StyledText } from '../../atoms/text'
import { LoadCommandText } from './LoadCommandText'
import { PipettingCommandText } from './PipettingCommandText'
import { TemperatureCommandText } from './TemperatureCommandText'
import { MoveLabwareCommandText } from './MoveLabwareCommandText'

import type { RunTimeCommand } from '@opentrons/shared-data'
import type { CompletedProtocolAnalysis } from '@opentrons/shared-data/js'
import type { StyleProps } from '@opentrons/components'

Expand Down Expand Up @@ -139,6 +139,24 @@ export function CommandText(props: Props): JSX.Element | null {
</StyledText>
)
}
case 'configureForVolume': {
const { volume, pipetteId } = command.params
const pipetteName = robotSideAnalysis.pipettes.find(
pip => pip.id === pipetteId
)?.pipetteName

return (
<StyledText as="p" {...styleProps}>
{t('configure_for_volume', {
volume,
pipette:
pipetteName != null
? getPipetteNameSpecs(pipetteName)?.displayName
: '',
})}
</StyledText>
)
}
case 'touchTip':
case 'home':
case 'savePosition':
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,14 +176,16 @@ def _pipette_with_liquid_settings(
# FIXME: stop using hwapi, and get those functions into core software
hw_api = ctx._core.get_hardware()
hw_mount = OT3Mount.LEFT if pipette.mount == "left" else OT3Mount.RIGHT
hw_pipette = hw_api.hardware_pipettes[hw_mount.to_mount()]
_check_aspirate_dispense_args(aspirate, dispense)

def _dispense_with_added_blow_out() -> None:
# dispense all liquid, plus some air by calling `pipette.blow_out(location, volume)`
# FIXME: this is a hack, until there's an equivalent `pipette.blow_out(location, volume)`
# dispense all liquid, plus some air
# FIXME: push-out is not supported in Legacy core, so here
# we again use the hardware controller
hw_api = ctx._core.get_hardware()
hw_mount = OT3Mount.LEFT if pipette.mount == "left" else OT3Mount.RIGHT
hw_api.blow_out(hw_mount, liquid_class.dispense.blow_out_submerged)
hw_api.dispense(hw_mount, push_out=liquid_class.dispense.blow_out_submerged)

# ASPIRATE/DISPENSE SEQUENCE HAS THREE PHASES:
# 1. APPROACH
Expand Down Expand Up @@ -211,14 +213,18 @@ def _dispense_with_added_blow_out() -> None:

# CREATE CALLBACKS FOR EACH PHASE
def _aspirate_on_approach() -> None:
if hw_pipette.current_volume > 0:
print(
"WARNING: removing trailing air-gap from pipette, "
"this should only happen during blank trials"
)
hw_api.dispense(hw_mount)
hw_api.configure_for_volume(hw_mount, aspirate if aspirate else dispense)
hw_api.prepare_for_aspirate(hw_mount)
if liquid_class.aspirate.leading_air_gap > 0:
pipette.aspirate(liquid_class.aspirate.leading_air_gap)

def _aspirate_on_submerge() -> None:
# TODO: re-implement mixing once we have a real use for it
# and once the rest of the script settles down
if mix:
raise NotImplementedError("mixing is not currently implemented")
# aspirate specified volume
callbacks.on_aspirating()
pipette.aspirate(aspirate)
Expand Down Expand Up @@ -308,8 +314,9 @@ def aspirate_with_liquid_class(
touch_tip: bool = False,
) -> None:
"""Aspirate with liquid class."""
pip_size = 50 if "50" in pipette.name else 1000
liquid_class = get_liquid_class(
int(pipette.max_volume), pipette.channels, tip_volume, int(aspirate_volume)
pip_size, pipette.channels, tip_volume, int(aspirate_volume)
)
_pipette_with_liquid_settings(
ctx,
Expand Down Expand Up @@ -345,8 +352,9 @@ def dispense_with_liquid_class(
touch_tip: bool = False,
) -> None:
"""Dispense with liquid class."""
pip_size = 50 if "50" in pipette.name else 1000
liquid_class = get_liquid_class(
int(pipette.max_volume), pipette.channels, tip_volume, int(dispense_volume)
pip_size, pipette.channels, tip_volume, int(dispense_volume)
)
_pipette_with_liquid_settings(
ctx,
Expand Down
Loading
Loading