Skip to content

Commit

Permalink
feat(shared-data,-protocol-designer): add foundation for plate reader…
Browse files Browse the repository at this point in the history
… support in PD (#17147)

This PR introduces plate reader to PD, still hidden behind a feature
flag. Namely, it adds the ability to add plate reader to the initial
deck state and fixes logic for ModuleLabel rendering for all modules. Of
note, in this dev work, I discovered a bug in the plate reader's
shared-data definition, in which the top-level x and y dimensions were
flipped, so those values are swapped here as well.
  • Loading branch information
ncdiehl11 authored Dec 19, 2024
1 parent 74b126e commit 481a1f0
Show file tree
Hide file tree
Showing 14 changed files with 135 additions and 76 deletions.
47 changes: 34 additions & 13 deletions components/src/molecules/Tabs/index.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { css } from 'styled-components'
import { TYPOGRAPHY, SPACING, RESPONSIVENESS } from '../../ui-style-constants'
import { Tooltip } from '../../atoms'
import { COLORS, BORDERS } from '../../helix-design-system'
import { POSITION_RELATIVE, DIRECTION_ROW } from '../../styles'
import { Btn, Flex } from '../../primitives'
import { POSITION_RELATIVE, DIRECTION_ROW } from '../../styles'
import { useHoverTooltip } from '../../tooltips'
import { TYPOGRAPHY, SPACING, RESPONSIVENESS } from '../../ui-style-constants'

const DEFAULT_TAB_STYLE = css`
${TYPOGRAPHY.pSemiBold}
Expand Down Expand Up @@ -65,6 +67,7 @@ export interface TabProps {
onClick: () => void
isActive?: boolean
disabled?: boolean
disabledReasonForTooltip?: string
}

export interface TabsProps {
Expand All @@ -77,18 +80,36 @@ export function Tabs(props: TabsProps): JSX.Element {
return (
<Flex flexDirection={DIRECTION_ROW} css={DEFAULT_CONTAINER_STYLE}>
{tabs.map((tab, index) => (
<Btn
data-testid={`tab_${index}_${tab.text}`}
key={index}
onClick={() => {
tab.onClick()
}}
css={tab.isActive === true ? CURRENT_TAB_STYLE : DEFAULT_TAB_STYLE}
disabled={tab.disabled}
>
{tab.text}
</Btn>
<Tab {...tab} data-testid={`tab_${index}_${tab.text}`} key={index} />
))}
</Flex>
)
}

function Tab(props: TabProps): JSX.Element {
const {
text,
onClick,
isActive,
disabled = false,
disabledReasonForTooltip,
} = props
const [targetProps, tooltipProps] = useHoverTooltip()
return (
<>
<Btn
onClick={onClick}
css={isActive === true ? CURRENT_TAB_STYLE : DEFAULT_TAB_STYLE}
disabled={disabled}
{...targetProps}
>
{text}
</Btn>
{disabled && disabledReasonForTooltip != null ? (
<Tooltip tooltipProps={tooltipProps}>
{disabledReasonForTooltip}
</Tooltip>
) : null}
</>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"onDeck": "On deck",
"one_item": "No more than 1 {{hardware}} allowed on the deck at one time",
"only_display_rec": "Only display recommended labware",
"plate_reader_no_labware": "Labware cannot be loaded onto a plate reader. You can move labware onto the plate reader when building your protocol",
"protocol_starting_deck": "Protocol starting deck",
"read_more_gen1_gen2": "Read more about the differences between GEN1 and GEN2 Magnetic Modules",
"rename_lab": "Rename labware",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
TYPOGRAPHY,
} from '@opentrons/components'
import {
ABSORBANCE_READER_V1,
FLEX_ROBOT_TYPE,
FLEX_STAGING_AREA_SLOT_ADDRESSABLE_AREAS,
getModuleDisplayName,
Expand Down Expand Up @@ -228,7 +229,9 @@ export function DeckSetupTools(props: DeckSetupToolsProps): JSX.Element | null {
disabled:
selectedFixture === 'wasteChute' ||
selectedFixture === 'wasteChuteAndStagingArea' ||
selectedFixture === 'trashBin',
selectedFixture === 'trashBin' ||
selectedModuleModel === ABSORBANCE_READER_V1,
disabledReasonForTooltip: t('plate_reader_no_labware'),
isActive: tab === 'labware',
onClick: () => {
setTab('labware')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ export function HighlightItems(props: HighlightItemsProps): JSX.Element | null {
? hoveredItem.text ?? ''
: selectedItemModule.text ?? ''
}
slot={moduleOnDeck.slot}
/>
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ export const HoveredItems = (
orientation={orientation}
isSelected={false}
isLast={true}
slot={selectedSlot.slot}
/>
) : null}
</>
Expand Down
11 changes: 9 additions & 2 deletions protocol-designer/src/pages/Designer/DeckSetup/LabwareTools.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import { getOnlyLatestDefs } from '../../../labware-defs'
import {
ADAPTER_96_CHANNEL,
getLabwareIsCompatible as _getLabwareIsCompatible,
getLabwareCompatibleWithAbsorbanceReader,
} from '../../../utils/labwareModuleCompatibility'
import { getHas96Channel } from '../../../utils'
import { createCustomLabwareDef } from '../../../labware-defs/actions'
Expand All @@ -49,6 +50,7 @@ import {
selectLabware,
selectNestedLabware,
} from '../../../labware-ingred/actions'
import { getEnableAbsorbanceReader } from '../../../feature-flags/selectors'
import {
ALL_ORDERED_CATEGORIES,
CUSTOM_CATEGORY,
Expand Down Expand Up @@ -132,13 +134,17 @@ export function LabwareTools(props: LabwareToolsProps): JSX.Element {
robotType === OT2_ROBOT_TYPE ? isNextToHeaterShaker : false
)

const enablePlateReader = useSelector(getEnableAbsorbanceReader)

const getLabwareCompatible = useCallback(
(def: LabwareDefinition2) => {
// assume that custom (non-standard) labware is (potentially) compatible
if (moduleType == null || !getLabwareDefIsStandard(def)) {
return true
}
return _getLabwareIsCompatible(def, moduleType)
return moduleType === ABSORBANCE_READER_TYPE
? getLabwareCompatibleWithAbsorbanceReader(def)
: _getLabwareIsCompatible(def, moduleType)
},
[moduleType]
)
Expand Down Expand Up @@ -167,7 +173,8 @@ export function LabwareTools(props: LabwareToolsProps): JSX.Element {
moduleType !== HEATERSHAKER_MODULE_TYPE) ||
(isAdapter96Channel && !has96Channel) ||
(slot === 'offDeck' && isAdapter) ||
(PLATE_READER_LOADNAME === parameters.loadName &&
(!enablePlateReader &&
PLATE_READER_LOADNAME === parameters.loadName &&
moduleType !== ABSORBANCE_READER_TYPE)
)
},
Expand Down
66 changes: 36 additions & 30 deletions protocol-designer/src/pages/Designer/DeckSetup/ModuleLabel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,27 @@ import { useSelector } from 'react-redux'
import { DeckLabelSet } from '@opentrons/components'
import {
FLEX_ROBOT_TYPE,
FLEX_STANDARD_DECKID,
HEATERSHAKER_MODULE_TYPE,
MAGNETIC_MODULE_TYPE,
OT2_STANDARD_DECKID,
TEMPERATURE_MODULE_TYPE,
THERMOCYCLER_MODULE_TYPE,
getModuleDef2,
} from '@opentrons/shared-data'
import { getRobotType } from '../../../file-data/selectors'
import type { DeckLabelProps } from '@opentrons/components'
import type { CoordinateTuple, ModuleModel } from '@opentrons/shared-data'
import type {
CoordinateTuple,
DeckSlotId,
ModuleModel,
} from '@opentrons/shared-data'

interface ModuleLabelProps {
moduleModel: ModuleModel
position: CoordinateTuple
orientation: 'left' | 'right'
isSelected: boolean
isLast: boolean
slot: DeckSlotId | null
isZoomed?: boolean
labwareInfos?: DeckLabelProps[]
labelName?: string
Expand All @@ -33,6 +38,7 @@ export const ModuleLabel = (props: ModuleLabelProps): JSX.Element => {
labwareInfos = [],
isZoomed = true,
labelName,
slot,
} = props
const robotType = useSelector(getRobotType)
const labelContainerRef = useRef<HTMLDivElement>(null)
Expand All @@ -45,30 +51,21 @@ export const ModuleLabel = (props: ModuleLabelProps): JSX.Element => {
}, [labwareInfos])

const def = getModuleDef2(moduleModel)
const overhang =
def?.dimensions.labwareInterfaceXDimension != null
? def.dimensions.xDimension - def?.dimensions.labwareInterfaceXDimension
const slotTransformKey =
robotType === FLEX_ROBOT_TYPE ? FLEX_STANDARD_DECKID : OT2_STANDARD_DECKID
const cornerOffsetsFromSlotFromTransform =
slot != null && !isZoomed
? def?.slotTransforms?.[slotTransformKey]?.[slot]?.cornerOffsetFromSlot
: null
const tempAdjustmentX =
def?.moduleType === TEMPERATURE_MODULE_TYPE && orientation === 'right'
? def?.dimensions.xDimension - (def?.dimensions.footprintXDimension ?? 0) // shift depending on side of deck
: 0
const tempAdjustmentY = def?.moduleType === TEMPERATURE_MODULE_TYPE ? -1 : 0
const heaterShakerAdjustmentX =
def?.moduleType === HEATERSHAKER_MODULE_TYPE && orientation === 'right' // shift depending on side of deck
? 7 // TODO(ND: 12/18/2024): investigate further why the module definition does not contain sufficient info to find this offset
: 0
let leftOverhang = overhang

switch (def?.moduleType) {
case TEMPERATURE_MODULE_TYPE:
leftOverhang = overhang * 2
break
case HEATERSHAKER_MODULE_TYPE:
leftOverhang = overhang + 14
break
case MAGNETIC_MODULE_TYPE:
leftOverhang = overhang + 8
break
case THERMOCYCLER_MODULE_TYPE:
if (!isZoomed && robotType === FLEX_ROBOT_TYPE) {
leftOverhang = overhang + 20
}
break
default:
break
}

return (
<DeckLabelSet
Expand All @@ -84,11 +81,20 @@ export const ModuleLabel = (props: ModuleLabelProps): JSX.Element => {
...labwareInfos,
]}
x={
(orientation === 'right'
? position[0] - overhang
: position[0] - leftOverhang) - def?.cornerOffsetFromSlot.x
position[0] +
def.cornerOffsetFromSlot.x +
(cornerOffsetsFromSlotFromTransform?.[0][3] ?? 0) +
tempAdjustmentX +
heaterShakerAdjustmentX -
1
}
y={
position[1] +
def.cornerOffsetFromSlot.y +
(cornerOffsetsFromSlotFromTransform?.[1][3] ?? 0) -
labelContainerHeight +
tempAdjustmentY
}
y={position[1] + def?.cornerOffsetFromSlot.y - labelContainerHeight}
width={def?.dimensions.xDimension + 2}
height={def?.dimensions.yDimension + 2}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ export const SelectedHoveredItems = (
orientation={orientation}
isSelected={true}
labwareInfos={labwareInfos}
slot={selectedSlot.slot}
/>
) : null}
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import type { LabwareDefinition2, PipetteV2Specs } from '@opentrons/shared-data'

vi.mock('../../../../utils')
vi.mock('../../../../step-forms/selectors')
vi.mock('../../../../feature-flags/selectors')
vi.mock('../../../../file-data/selectors')
vi.mock('../../../../labware-defs/selectors')
vi.mock('../../../../labware-defs/actions')
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { describe, it, expect } from 'vitest'
import {
ABSORBANCE_READER_V1,
FLEX_ROBOT_TYPE,
HEATERSHAKER_MODULE_TYPE,
HEATERSHAKER_MODULE_V1,
Expand Down Expand Up @@ -45,12 +46,13 @@ describe('getModuleModelsBySlot', () => {
})
it('renders all flex modules for B1', () => {
expect(getModuleModelsBySlot(true, FLEX_ROBOT_TYPE, 'B1')).toEqual(
FLEX_MODULE_MODELS
FLEX_MODULE_MODELS.filter(model => model !== ABSORBANCE_READER_V1)
)
})
it('renders all flex modules for C1', () => {
const noTC = FLEX_MODULE_MODELS.filter(
model => model !== THERMOCYCLER_MODULE_V2
model =>
model !== THERMOCYCLER_MODULE_V2 && model !== ABSORBANCE_READER_V1
)
expect(getModuleModelsBySlot(true, FLEX_ROBOT_TYPE, 'C1')).toEqual(noTC)
})
Expand Down
6 changes: 3 additions & 3 deletions protocol-designer/src/pages/Designer/DeckSetup/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
HEATERSHAKER_MODULE_TYPE,
MAGNETIC_BLOCK_TYPE,
ABSORBANCE_READER_TYPE,
ABSORBANCE_READER_V1,
} from '@opentrons/shared-data'

import type { ModuleModel, ModuleType } from '@opentrons/shared-data'
Expand Down Expand Up @@ -92,16 +93,15 @@ export const RECOMMENDED_LABWARE_BY_MODULE: { [K in ModuleType]: string[] } = {
'nest_96_wellplate_2ml_deep',
'opentrons_96_wellplate_200ul_pcr_full_skirt',
],
[ABSORBANCE_READER_TYPE]: [
'opentrons_flex_lid_absorbance_plate_reader_module',
],
[ABSORBANCE_READER_TYPE]: ['nest_96_wellplate_200ul_flat'],
}

export const MOAM_MODELS_WITH_FF: ModuleModel[] = [TEMPERATURE_MODULE_V2]
export const MOAM_MODELS: ModuleModel[] = [
TEMPERATURE_MODULE_V2,
HEATERSHAKER_MODULE_V1,
MAGNETIC_BLOCK_V1,
ABSORBANCE_READER_V1,
]

export const MAX_MOAM_MODULES = 7
Expand Down
46 changes: 23 additions & 23 deletions protocol-designer/src/pages/Designer/DeckSetup/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ import {
FLEX_ROBOT_TYPE,
FLEX_STAGING_AREA_SLOT_ADDRESSABLE_AREAS,
HEATERSHAKER_MODULE_TYPE,
MAGNETIC_BLOCK_V1,
HEATERSHAKER_MODULE_V1,
OT2_ROBOT_TYPE,
TEMPERATURE_MODULE_V2,
THERMOCYCLER_MODULE_TYPE,
THERMOCYCLER_MODULE_V2,
getAreSlotsAdjacent,
Expand Down Expand Up @@ -61,33 +62,32 @@ export function getModuleModelsBySlot(
robotType: RobotType,
slot: DeckSlotId
): ModuleModel[] {
const FLEX_MIDDLE_SLOTS = ['B2', 'C2', 'A2', 'D2']
const FLEX_MIDDLE_SLOTS = new Set(['B2', 'C2', 'A2', 'D2'])
const OT2_MIDDLE_SLOTS = ['2', '5', '8', '11']
const FILTERED_MODULES = enableAbsorbanceReader
? FLEX_MODULE_MODELS
: FLEX_MODULE_MODELS.filter(model => model !== ABSORBANCE_READER_V1)

let moduleModels: ModuleModel[] = FILTERED_MODULES
const FLEX_RIGHT_SLOTS = new Set(['A3', 'B3', 'C3', 'D3'])

let moduleModels: ModuleModel[] = FLEX_MODULE_MODELS

switch (robotType) {
case FLEX_ROBOT_TYPE: {
if (slot !== 'B1' && !FLEX_MIDDLE_SLOTS.includes(slot)) {
moduleModels = FILTERED_MODULES.filter(
model => model !== THERMOCYCLER_MODULE_V2
)
}
if (FLEX_MIDDLE_SLOTS.includes(slot)) {
moduleModels = FILTERED_MODULES.filter(
model => model === MAGNETIC_BLOCK_V1
)
}
if (
FLEX_STAGING_AREA_SLOT_ADDRESSABLE_AREAS.includes(
slot as AddressableAreaName
)
) {
moduleModels = []
}
moduleModels = FLEX_STAGING_AREA_SLOT_ADDRESSABLE_AREAS.includes(
slot as AddressableAreaName
)
? []
: FLEX_MODULE_MODELS.filter(model => {
if (model === THERMOCYCLER_MODULE_V2) {
return slot === 'B1'
} else if (model === ABSORBANCE_READER_V1) {
return FLEX_RIGHT_SLOTS.has(slot) && enableAbsorbanceReader
} else if (
model === TEMPERATURE_MODULE_V2 ||
model === HEATERSHAKER_MODULE_V1
) {
return !FLEX_MIDDLE_SLOTS.has(slot)
}
return true
})
break
}
case OT2_ROBOT_TYPE: {
Expand Down
Loading

0 comments on commit 481a1f0

Please sign in to comment.