Skip to content

Commit

Permalink
PD timeline scrubber
Browse files Browse the repository at this point in the history
  • Loading branch information
jerader committed Jan 16, 2025
1 parent 5e8417a commit e072793
Show file tree
Hide file tree
Showing 7 changed files with 152 additions and 31 deletions.
46 changes: 29 additions & 17 deletions components/src/organisms/ProtocolTimelineScrubber/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,14 @@ import type {
RobotType,
RunTimeCommand,
} from '@opentrons/shared-data'
import type { ModuleTemporalProperties } from '@opentrons/step-generation'
import type {
InvariantContext,
ModuleTemporalProperties,
Timeline,

Check failure on line 45 in components/src/organisms/ProtocolTimelineScrubber/index.tsx

View workflow job for this annotation

GitHub Actions / js checks

'Timeline' is defined but never used
TimelineFrame,
} from '@opentrons/step-generation'
import type { LabwareOnDeck, Module } from '../..'
import { constructInvariantContextFromRunCommands } from '@opentrons/step-generation'

Check failure on line 49 in components/src/organisms/ProtocolTimelineScrubber/index.tsx

View workflow job for this annotation

GitHub Actions / js checks

'/home/runner/work/opentrons/opentrons/node_modules/@opentrons/step-generation/src/index.ts' imported multiple times
export * from './types'
export * from './utils'

Expand All @@ -51,6 +57,8 @@ interface ProtocolTimelineScrubberProps {
commands: RunTimeCommand[]
analysis: CompletedProtocolAnalysis | ProtocolAnalysisOutput
robotType?: RobotType
invariantContextFromPD?: InvariantContext
initialRobotStateFromPD?: TimelineFrame
}

export const DECK_LAYER_BLOCKLIST = [
Expand All @@ -62,23 +70,30 @@ export const DECK_LAYER_BLOCKLIST = [
'removableDeckOutline',
'screwHoles',
]
export const VIEWBOX_MIN_X = -84
export const VIEWBOX_MIN_Y = -10
export const VIEWBOX_WIDTH = 600
export const VIEWBOX_HEIGHT = 460

export function ProtocolTimelineScrubber(
props: ProtocolTimelineScrubberProps
): JSX.Element {
const { commands, analysis, robotType = FLEX_ROBOT_TYPE } = props
const {
commands,
analysis,
robotType = FLEX_ROBOT_TYPE,
invariantContextFromPD,
initialRobotStateFromPD,
} = props
const wrapperRef = useRef<HTMLDivElement>(null)
const commandListRef = useRef<ViewportListRef>(null)
const [currentCommandIndex, setCurrentCommandIndex] = useState<number>(0)
const [isPlaying, setIsPlaying] = useState<boolean>(true)

const currentCommandsSlice = commands.slice(0, currentCommandIndex + 1)
const invariantContextFromRunCommands = constructInvariantContextFromRunCommands(
commands
)
const { frame, invariantContext } = getResultingTimelineFrameFromRunCommands(
currentCommandsSlice
currentCommandsSlice,
invariantContextFromPD ?? invariantContextFromRunCommands,
initialRobotStateFromPD
)
const handlePlayPause = (): void => {
setIsPlaying(!isPlaying)
Expand All @@ -100,7 +115,7 @@ export function ProtocolTimelineScrubber(
}
}, [isPlaying, commands])

const { robotState } = frame
const robotState = frame.robotState

const [leftPipetteId] = Object.entries(robotState.pipettes).find(
([_pipetteId, pipette]) => pipette?.mount === 'left'
Expand All @@ -120,29 +135,26 @@ export function ProtocolTimelineScrubber(

const allWellContentsForActiveItem = getAllWellContentsForActiveItem(
invariantContext.labwareEntities,
frame
robotState
)
const liquidDisplayColors = analysis.liquids.map(
liquid => liquid.displayColor ?? COLORS.blue50
)

const isValidRobotSideAnalysis = analysis != null
const allRunDefs = useMemo(
() =>
analysis != null
? getLabwareDefinitionsFromCommands(analysis.commands)
: [],
() => getLabwareDefinitionsFromCommands(commands),
[isValidRobotSideAnalysis]
)

return (
<Flex
height="95vh"
height="auto"
flexDirection={DIRECTION_COLUMN}
gridGap={SPACING.spacing8}
>
<Flex gridGap={SPACING.spacing8} flex="1 1 0">
<Flex height="60vh">
<Flex height="40vh">
<BaseDeck
robotType={robotType}
deckConfig={getSimplestDeckConfigForProtocol(analysis)}
Expand Down Expand Up @@ -263,14 +275,14 @@ export function ProtocolTimelineScrubber(
mount="left"
pipetteId={leftPipetteId}
pipetteEntity={leftPipetteEntity}
timelineFrame={frame.robotState}
timelineFrame={robotState}
analysis={analysis}
/>
<PipetteMountViz
mount="right"
pipetteId={rightPipetteId}
pipetteEntity={rightPipetteEntity}
timelineFrame={frame.robotState}
timelineFrame={robotState}
analysis={analysis}
/>
</Flex>
Expand Down
7 changes: 4 additions & 3 deletions components/src/organisms/ProtocolTimelineScrubber/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import type {
LocationLiquidState,
RunCommandTimelineFrame,

Check failure on line 16 in components/src/organisms/ProtocolTimelineScrubber/utils.ts

View workflow job for this annotation

GitHub Actions / js checks

'RunCommandTimelineFrame' is defined but never used
SingleLabwareLiquidState,
TimelineFrame,
} from '@opentrons/step-generation'
import type { CommandTextData } from './types'

Expand Down Expand Up @@ -127,11 +128,11 @@ export const wellFillFromWellContents = (

export function getAllWellContentsForActiveItem(
labwareEntities: LabwareEntities,
timelineFrame: RunCommandTimelineFrame
robotState: TimelineFrame
): WellContentsByLabware | null {
if (timelineFrame == null) return null
if (robotState == null) return null

const liquidState = timelineFrame.robotState.liquidState.labware
const liquidState = robotState.liquidState.labware
const wellContentsByLabwareId = mapValues(
liquidState,
(labwareLiquids: SingleLabwareLiquidState, labwareId: string) => {
Expand Down
3 changes: 2 additions & 1 deletion protocol-designer/src/assets/localization/en/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import shared from './shared.json'
import starting_deck_state from './starting_deck_state.json'
import tooltip from './tooltip.json'
import well_selection from './well_selection.json'

import { protocolCommandTextEn as protocol_command_text } from '@opentrons/shared-data'
export const en = {
alert,
application,
Expand All @@ -38,4 +38,5 @@ export const en = {
starting_deck_state,
tooltip,
well_selection,
protocol_command_text,
}
101 changes: 101 additions & 0 deletions protocol-designer/src/pages/ProtocolOverview/ScrubberContainer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { LoadedPipettes, ProtocolTimelineScrubber } from '@opentrons/components'

Check failure on line 1 in protocol-designer/src/pages/ProtocolOverview/ScrubberContainer.tsx

View workflow job for this annotation

GitHub Actions / js checks

'LoadedPipettes' is defined but never used
import { useSelector } from 'react-redux'
import reduce from 'lodash/reduce'

Check failure on line 3 in protocol-designer/src/pages/ProtocolOverview/ScrubberContainer.tsx

View workflow job for this annotation

GitHub Actions / js checks

'reduce' is defined but never used
import {
getInitialRobotState,
getRobotStateTimeline,
getRobotType,
} from '../../file-data/selectors'
import {

Check failure on line 9 in protocol-designer/src/pages/ProtocolOverview/ScrubberContainer.tsx

View workflow job for this annotation

GitHub Actions / js checks

All imports in the declaration are only used as types. Use `import type`
CompletedProtocolAnalysis,
Liquid,
LoadedLabware,
LoadedModule,
LoadedPipette,
RunTimeCommand,
} from '@opentrons/shared-data'
import { flatMap } from 'lodash'
import { uuid } from '../../utils'
import {
getInitialDeckSetup,
getInvariantContext,
} from '../../step-forms/selectors'
import { getLabwareNicknamesById } from '../../ui/labware/selectors'
import { selectors as ingredSelectors } from '../../labware-ingred/selectors'
import { swatchColors } from '../../organisms/DefineLiquidsModal/swatchColors'
import { LiquidGroup } from '../../labware-ingred/types'

Check failure on line 26 in protocol-designer/src/pages/ProtocolOverview/ScrubberContainer.tsx

View workflow job for this annotation

GitHub Actions / js checks

'LiquidGroup' is defined but never used
import { getNextRobotStateAndWarnings } from '@opentrons/step-generation'

export function ScrubberContainer(): JSX.Element {
const robotType = useSelector(getRobotType)
const labwareNickNames = useSelector(getLabwareNicknamesById)
const robotStateTimeline = useSelector(getRobotStateTimeline)
const initialRobotState = useSelector(getInitialRobotState)
const ingredients = useSelector(ingredSelectors.getLiquidGroupsById)
const invariantContext = useSelector(getInvariantContext)
const initialDeckSetup = useSelector(getInitialDeckSetup)
const { pipettes, modules, labware } = initialDeckSetup

const nonLoadCommands: RunTimeCommand[] = flatMap(

Check failure on line 39 in protocol-designer/src/pages/ProtocolOverview/ScrubberContainer.tsx

View workflow job for this annotation

GitHub Actions / js checks

Type 'CreateCommand[]' is not assignable to type 'RunTimeCommand[]'.
robotStateTimeline.timeline,

Check failure on line 40 in protocol-designer/src/pages/ProtocolOverview/ScrubberContainer.tsx

View workflow job for this annotation

GitHub Actions / protocol designer unit tests

protocol-designer/src/pages/ProtocolOverview/__tests__/ProtocolOverview.test.tsx > ProtocolOverview > renders each section with text

TypeError: Cannot read properties of undefined (reading 'timeline') ❯ ScrubberContainer protocol-designer/src/pages/ProtocolOverview/ScrubberContainer.tsx:40:24 ❯ renderWithHooks node_modules/react-dom/cjs/react-dom.development.js:16305:18 ❯ mountIndeterminateComponent node_modules/react-dom/cjs/react-dom.development.js:20074:13 ❯ beginWork node_modules/react-dom/cjs/react-dom.development.js:21587:16 ❯ beginWork$1 node_modules/react-dom/cjs/react-dom.development.js:27426:14 ❯ performUnitOfWork node_modules/react-dom/cjs/react-dom.development.js:26560:12 ❯ workLoopSync node_modules/react-dom/cjs/react-dom.development.js:26466:5 ❯ renderRootSync node_modules/react-dom/cjs/react-dom.development.js:26434:7 ❯ recoverFromConcurrentError node_modules/react-dom/cjs/react-dom.development.js:25850:20 ❯ performConcurrentWorkOnRoot node_modules/react-dom/cjs/react-dom.development.js:25750:22

Check failure on line 40 in protocol-designer/src/pages/ProtocolOverview/ScrubberContainer.tsx

View workflow job for this annotation

GitHub Actions / protocol designer unit tests

protocol-designer/src/pages/ProtocolOverview/__tests__/ProtocolOverview.test.tsx > ProtocolOverview > navigates to starting deck state

TypeError: Cannot read properties of undefined (reading 'timeline') ❯ ScrubberContainer protocol-designer/src/pages/ProtocolOverview/ScrubberContainer.tsx:40:24 ❯ renderWithHooks node_modules/react-dom/cjs/react-dom.development.js:16305:18 ❯ mountIndeterminateComponent node_modules/react-dom/cjs/react-dom.development.js:20074:13 ❯ beginWork node_modules/react-dom/cjs/react-dom.development.js:21587:16 ❯ beginWork$1 node_modules/react-dom/cjs/react-dom.development.js:27426:14 ❯ performUnitOfWork node_modules/react-dom/cjs/react-dom.development.js:26560:12 ❯ workLoopSync node_modules/react-dom/cjs/react-dom.development.js:26466:5 ❯ renderRootSync node_modules/react-dom/cjs/react-dom.development.js:26434:7 ❯ recoverFromConcurrentError node_modules/react-dom/cjs/react-dom.development.js:25850:20 ❯ performConcurrentWorkOnRoot node_modules/react-dom/cjs/react-dom.development.js:25750:22
timelineFrame => timelineFrame.commands
)
const loadPipettes: LoadedPipette[] = Object.values(pipettes).map(
pipette => ({
id: pipette.id,
pipetteName: pipette.name,
mount: pipette.mount,
})
)
const loadModules: LoadedModule[] = Object.values(modules).map(module => ({
id: module.id,
model: module.model,
serialNumber: '1', // todo what is this,
location: {
slotName: module.slot,
},
}))
const loadLabware: LoadedLabware[] = Object.values(labware).map(lw => ({
id: lw.id,
loadName: lw.def.parameters.loadName,
definitionUri: lw.labwareDefURI,
location: { slotName: lw.slot }, // todo fix this
displayName: labwareNickNames[lw.id],
}))
const liquids: Liquid[] = Object.entries(ingredients).map(
([liquidId, liquidData]) => ({
id: liquidId,
displayName: liquidData.name ?? 'undefined liquid',
description: liquidData.description ?? '',
displayColor: liquidData.displayColor ?? swatchColors(liquidId),
})
)

const analysis: CompletedProtocolAnalysis = {
id: uuid(),
result: 'ok',
pipettes: loadPipettes,
labware: loadLabware,
modules: loadModules,
liquids,
commands: nonLoadCommands,
errors: [],
robotType,
}

const robotStateAndWarnings = getNextRobotStateAndWarnings(

Check failure on line 86 in protocol-designer/src/pages/ProtocolOverview/ScrubberContainer.tsx

View workflow job for this annotation

GitHub Actions / js checks

'robotStateAndWarnings' is assigned a value but never used
nonLoadCommands,
invariantContext,
initialRobotState
)

return (
<ProtocolTimelineScrubber
commands={nonLoadCommands}
analysis={analysis}
robotType={robotType}
invariantContextFromPD={invariantContext}
initialRobotStateFromPD={initialRobotState}
/>
)
}
4 changes: 3 additions & 1 deletion protocol-designer/src/pages/ProtocolOverview/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
JUSTIFY_SPACE_BETWEEN,
LargeButton,
NO_WRAP,
ProtocolTimelineScrubber,

Check failure on line 18 in protocol-designer/src/pages/ProtocolOverview/index.tsx

View workflow job for this annotation

GitHub Actions / js checks

'ProtocolTimelineScrubber' is defined but never used
SPACING,
StyledText,
WRAP,
Expand Down Expand Up @@ -47,7 +48,7 @@ import {
getUnusedStagingAreas,
getUnusedTrash,
} from './utils'

import { ScrubberContainer } from './ScrubberContainer'
import type { CreateCommand } from '@opentrons/shared-data'
import type { ThunkDispatch } from '../../types'

Expand Down Expand Up @@ -320,6 +321,7 @@ export function ProtocolOverview(): JSX.Element {
css={COLUMN_STYLE}
gridGap={SPACING.spacing12}
>
<ScrubberContainer />
<StartingDeck
robotType={robotType}
setShowMaterialsListModal={setShowMaterialsListModal}
Expand Down
20 changes: 11 additions & 9 deletions step-generation/src/utils/createTimelineFromRunCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type {
InvariantContext,
RobotState,
RobotStateAndWarnings,
TimelineFrame,
} from '../types'

export type RunCommandTimelineFrame = RobotStateAndWarnings & {
Expand All @@ -21,15 +22,16 @@ interface ResultingTimelineFrame {
invariantContext: InvariantContext
}
export function getResultingTimelineFrameFromRunCommands(
commands: RunTimeCommand[]
commands: RunTimeCommand[],
invariantContext: InvariantContext,
initialRobotStateFromPd?: TimelineFrame
): ResultingTimelineFrame {
const invariantContext = constructInvariantContextFromRunCommands(commands)
const pipetteLocations = commands.reduce<RobotState['pipettes']>(
(acc, command) => {
if (command.commandType === 'loadPipette' && command.result != null) {
if (command.commandType === 'loadPipette') {
return {
...acc,
[command.result.pipetteId]: {
[command.params.pipetteId]: {
mount: command.params.mount,
},
}
Expand All @@ -41,7 +43,7 @@ export function getResultingTimelineFrameFromRunCommands(

const labwareLocations = commands.reduce<RobotState['labware']>(
(acc, command) => {
if (command.commandType === 'loadLabware' && command.result != null) {
if (command.commandType === 'loadLabware') {
let slot
if (command.params.location === 'offDeck') {
slot = command.params.location
Expand All @@ -56,7 +58,7 @@ export function getResultingTimelineFrameFromRunCommands(
}
return {
...acc,
[command.result.labwareId]: {
[command.result?.labwareId ?? command.params.labwareId ?? 'test']: {
slot: slot,
},
}
Expand All @@ -67,11 +69,11 @@ export function getResultingTimelineFrameFromRunCommands(
)
const moduleLocations = commands.reduce<RobotState['modules']>(
(acc, command) => {
if (command.commandType === 'loadModule' && command.result != null) {
if (command.commandType === 'loadModule') {
const moduleType = getModuleDef2(command.params.model).moduleType
return {
...acc,
[command.result.moduleId]: {
[command.result?.moduleId ?? command.params.moduleId ?? 'test']: {
slot: command.params.location.slotName,
moduleState: MODULE_INITIAL_STATE_BY_TYPE[moduleType],
},
Expand All @@ -92,7 +94,7 @@ export function getResultingTimelineFrameFromRunCommands(
...getNextRobotStateAndWarnings(
commands,
invariantContext,
initialRobotState
initialRobotStateFromPd ?? initialRobotState
),
command: commands[commands.length - 1],
},
Expand Down
2 changes: 2 additions & 0 deletions step-generation/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,6 @@ export * from './misc'
export * from './movableTrashCommandsUtil'
export * from './safePipetteMovements'
export * from './wasteChuteCommandsUtil'
export * from './createTimelineFromRunCommands'
export * from './constructInvariantContextFromRunCommands'
export const uuid: () => string = uuidv4

0 comments on commit e072793

Please sign in to comment.