Skip to content

Commit

Permalink
feat(protocol-designer): timeline scrubber to the overview page (#17301)
Browse files Browse the repository at this point in the history
Some skunkwork that adds the timeline scrubber to PD's overview behind a
feature flag.
  • Loading branch information
jerader authored Jan 29, 2025
1 parent 2661488 commit 16bd893
Show file tree
Hide file tree
Showing 15 changed files with 529 additions and 209 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,7 @@ export function ProtocolTimeline(): JSX.Element {

return storedProtocol != null && storedProtocol.mostRecentAnalysis != null ? (
<Box padding={SPACING.spacing16}>
<ProtocolTimelineScrubber
commands={storedProtocol.mostRecentAnalysis.commands}
analysis={storedProtocol.mostRecentAnalysis}
robotType={storedProtocol.mostRecentAnalysis.robotType}
/>
<ProtocolTimelineScrubber analysis={storedProtocol.mostRecentAnalysis} />
</Box>
) : (
<Icon size="8rem" name="ot-spinner" spin />
Expand Down
47 changes: 23 additions & 24 deletions components/src/organisms/ProtocolTimelineScrubber/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ import map from 'lodash/map'
import reduce from 'lodash/reduce'
import ViewportList from 'react-viewport-list'

import { getResultingTimelineFrameFromRunCommands } from '@opentrons/step-generation'
import {
constructInvariantContextFromRunCommands,
getResultingTimelineFrameFromRunCommands,
} from '@opentrons/step-generation'
import {
FLEX_ROBOT_TYPE,
THERMOCYCLER_MODULE_TYPE,
Expand Down Expand Up @@ -36,21 +39,19 @@ import type {
CompletedProtocolAnalysis,
LabwareLocation,
ProtocolAnalysisOutput,
RobotType,
RunTimeCommand,
} from '@opentrons/shared-data'
import type { ModuleTemporalProperties } from '@opentrons/step-generation'
import type { LabwareOnDeck, Module } from '../..'

export * from './types'
export * from './utils'

const SEC_PER_FRAME = 1000
export const COMMAND_WIDTH_PX = 240

interface ProtocolTimelineScrubberProps {
commands: RunTimeCommand[]
analysis: CompletedProtocolAnalysis | ProtocolAnalysisOutput
robotType?: RobotType
height?: string
}

export const DECK_LAYER_BLOCKLIST = [
Expand All @@ -62,23 +63,24 @@ 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 { analysis, height } = props
const { commands, robotType, liquids } = analysis
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,
invariantContextFromRunCommands
)
const handlePlayPause = (): void => {
setIsPlaying(!isPlaying)
Expand Down Expand Up @@ -120,31 +122,28 @@ export function ProtocolTimelineScrubber(

const allWellContentsForActiveItem = getAllWellContentsForActiveItem(
invariantContext.labwareEntities,
frame
robotState
)
const liquidDisplayColors = analysis.liquids.map(
liquid => liquid.displayColor ?? COLORS.blue50
const liquidDisplayColors = liquids.map(
({ displayColor }) => 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={height ?? '60vh'}>
<BaseDeck
robotType={robotType}
robotType={robotType ?? FLEX_ROBOT_TYPE}
deckConfig={getSimplestDeckConfigForProtocol(analysis)}
modulesOnDeck={map(robotState.modules, (module, moduleId) => {
const labwareInModuleId =
Expand Down Expand Up @@ -263,14 +262,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 All @@ -293,7 +292,7 @@ export function ProtocolTimelineScrubber(
currentCommandIndex={currentCommandIndex}
setCurrentCommandIndex={setCurrentCommandIndex}
analysis={analysis}
robotType={robotType}
robotType={robotType ?? FLEX_ROBOT_TYPE}
allRunDefs={allRunDefs}
/>
)}
Expand Down
8 changes: 4 additions & 4 deletions components/src/organisms/ProtocolTimelineScrubber/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ import type {
import type {
LabwareEntities,
LocationLiquidState,
RunCommandTimelineFrame,
SingleLabwareLiquidState,
TimelineFrame,
} from '@opentrons/step-generation'
import type { CommandTextData } from './types'

Expand Down Expand Up @@ -127,11 +127,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
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,9 @@
"OT_PD_ENABLE_LIQUID_CLASSES": {
"title": "Enable liquid classes",
"description": "Enable liquid classes support"
},
"OT_PD_ENABLE_TIMELINE_SCRUBBER": {
"title": "Enable timeline scrubber",
"description": "See the protocol timeline visualization in overview"
}
}
2 changes: 2 additions & 0 deletions protocol-designer/src/assets/localization/en/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import liquids from './liquids.json'
import modal from './modal.json'
import modules from './modules.json'
import nav from './nav.json'
import protocol_command_text from './protocol_command_text.json'
import protocol_overview from './protocol_overview.json'
import protocol_steps from './protocol_steps.json'
import shared from './shared.json'
Expand All @@ -32,6 +33,7 @@ export const en = {
modal,
modules,
nav,
protocol_command_text,
protocol_overview,
protocol_steps,
shared,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
{
"absorbance_reader_close_lid": "Closing Absorbance Reader lid",
"absorbance_reader_initialize": "Initializing Absorbance Reader to perform {{mode}} measurement at {{wavelengths}}",
"absorbance_reader_open_lid": "Opening Absorbance Reader lid",
"absorbance_reader_read": "Reading plate in Absorbance Reader",
"adapter_in_mod_in_slot": "{{adapter}} on {{module}} in Slot {{slot}}",
"adapter_in_slot": "{{adapter}} in Slot {{slot}}",
"air_gap_in_place": "Air gapping {{volume}} µL",
"all_nozzles": "all nozzles",
"aspirate": "Aspirating {{volume}} µL from well {{well_name}} of {{labware}} in {{labware_location}} at {{flow_rate}} µL/sec",
"aspirate_in_place": "Aspirating {{volume}} µL in place at {{flow_rate}} µL/sec ",
"blowout": "Blowing out at well {{well_name}} of {{labware}} in {{labware_location}} at {{flow_rate}} µL/sec",
"blowout_in_place": "Blowing out in place at {{flow_rate}} µL/sec",
"closing_tc_lid": "Closing Thermocycler lid",
"column_layout": "column layout",
"comment": "Comment",
"configure_for_volume": "Configure {{pipette}} to aspirate {{volume}} µL",
"configure_nozzle_layout": "Configure {{pipette}} to use {{layout}}",
"confirm_and_resume": "Confirm and resume",
"deactivate_hs_shake": "Deactivating shaker",
"deactivate_temperature_module": "Deactivating Temperature Module",
"deactivating_hs_heater": "Deactivating heater",
"deactivating_tc_block": "Deactivating Thermocycler block",
"deactivating_tc_lid": "Deactivating Thermocycler lid",
"degrees_c": "{{temp}}°C",
"detect_liquid_presence": "Detecting liquid presence in well {{well_name}} of {{labware}} in {{labware_location}}",
"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_in_place": "Dispensing {{volume}} µL in place 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}}",
"drop_tip_in_place": "Dropping tip in place",
"dropping_tip_in_trash": "Dropping tip in {{trash}}",
"engaging_magnetic_module": "Engaging Magnetic Module",
"fixed_trash": "Fixed Trash",
"home_gantry": "Homing all gantry, pipette, and plunger axes",
"in_location": "in {{location}}",
"latching_hs_latch": "Latching labware on Heater-Shaker",
"left": "Left",
"load_labware_to_display_location": "Load {{labware}} {{display_location}}",
"load_liquids_info_protocol_setup": "Load {{liquid}} into {{labware}}",
"load_module_protocol_setup": "Load {{module}} in Slot {{slot_name}}",
"load_pipette_protocol_setup": "Load {{pipette_name}} in {{mount_name}} Mount",
"module_in_slot": "{{module}} in Slot {{slot_name}}",
"module_in_slot_plural": "{{module}}",
"move_labware": "Move Labware",
"move_labware_manually": "Manually move {{labware}} from {{old_location}} to {{new_location}}",
"move_labware_on": "Move labware on {{robot_name}}",
"move_labware_using_gripper": "Moving {{labware}} using gripper from {{old_location}} to {{new_location}}",
"move_relative": "Moving {{distance}} mm along {{axis}} axis",
"move_to_addressable_area": "Moving to {{addressable_area}}",
"move_to_addressable_area_drop_tip": "Moving to {{addressable_area}}",
"move_to_coordinates": "Moving to (X: {{x}}, Y: {{y}}, Z: {{z}})",
"move_to_slot": "Moving to Slot {{slot_name}}",
"move_to_well": "Moving to well {{well_name}} of {{labware}} in {{labware_location}}",
"multiple": "multiple",
"notes": "notes",
"off_deck": "off deck",
"offdeck": "offdeck",
"on_location": "on {{location}}",
"opening_tc_lid": "Opening Thermocycler lid",
"pause": "Pause",
"pause_on": "Pause on {{robot_name}}",
"partial_layout": "partial layout",
"pickup_tip": "Picking up tip(s) from {{well_range}} of {{labware}} in {{labware_location}}",
"prepare_to_aspirate": "Preparing {{pipette}} to aspirate",
"reloading_labware": "Reloading {{labware}}",
"return_tip": "Returning tip to {{well_name}} of {{labware}} in {{labware_location}}",
"right": "Right",
"row_layout": "row layout",
"save_position": "Saving position",
"set_and_await_hs_shake": "Setting Heater-Shaker to shake at {{rpm}} rpm and waiting until reached",
"setting_hs_temp": "Setting Target Temperature of Heater-Shaker to {{temp}}",
"setting_temperature_module_temp": "Setting Temperature Module to {{temp}} (rounded to nearest integer)",
"setting_thermocycler_block_temp": "Setting Thermocycler block temperature to {{temp}} with hold time of {{hold_time_seconds}} seconds after target reached",
"setting_thermocycler_lid_temp": "Setting Thermocycler lid temperature to {{temp}}",
"single": "single",
"single_nozzle_layout": "single nozzle layout",
"slot": "Slot {{slot_name}}",
"target_temperature": "target temperature",
"tc_awaiting_for_duration": "Waiting for Thermocycler profile to complete",
"tc_run_profile_steps": "Temperature: {{celsius}}°C, hold time: {{duration}}",
"tc_starting_extended_profile": "Running thermocycler profile with {{elementCount}} total steps and cycles:",
"tc_starting_extended_profile_cycle": "{{repetitions}} repetitions of the following steps:",
"tc_starting_profile": "Running thermocycler profile with {{stepCount}} steps:",
"touch_tip": "Touching tip",
"trash_bin": "Trash Bin",
"trash_bin_in_slot": "Trash Bin in {{slot_name}}",
"turning_rail_lights_off": "Turning rail lights off",
"turning_rail_lights_on": "Turning rail lights on",
"unlatching_hs_latch": "Unlatching labware on Heater-Shaker",
"wait_for_duration": "Pausing for {{seconds}} seconds. {{message}}",
"wait_for_resume": "Pausing protocol",
"waiting_for_hs_to_reach": "Waiting for Heater-Shaker to reach target temperature",
"waiting_for_tc_block_to_reach": "Waiting for Thermocycler block to reach target temperature and holding for specified time",
"waiting_for_tc_lid_to_reach": "Waiting for Thermocycler lid to reach target temperature",
"waiting_to_reach_temp_module": "Waiting for Temperature Module to reach {{temp}}",
"waste_chute": "Waste Chute",
"with_reference_of": "with reference of {{wavelength}} nm"
}
2 changes: 2 additions & 0 deletions protocol-designer/src/feature-flags/reducers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ const initialFlags: Flags = {
OT_PD_ENABLE_REACT_SCAN: process.env.OT_PD_ENABLE_REACT_SCAN === '1' || false,
OT_PD_ENABLE_LIQUID_CLASSES:
process.env.OT_PD_ENABLE_LIQUID_CLASSES === '1' || false,
OT_PD_ENABLE_TIMELINE_SCRUBBER:
process.env.OT_PD_ENABLE_TIMELINE_SCRUBBER === '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
Expand Down
4 changes: 4 additions & 0 deletions protocol-designer/src/feature-flags/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,7 @@ export const getEnableLiquidClasses: Selector<boolean> = createSelector(
getFeatureFlagData,
flags => flags.OT_PD_ENABLE_LIQUID_CLASSES ?? false
)
export const getEnableTimelineScrubber: Selector<boolean> = createSelector(
getFeatureFlagData,
flags => flags.OT_PD_ENABLE_TIMELINE_SCRUBBER ?? false
)
2 changes: 2 additions & 0 deletions protocol-designer/src/feature-flags/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export type FlagTypes =
| 'OT_PD_ENABLE_HOT_KEYS_DISPLAY'
| 'OT_PD_ENABLE_REACT_SCAN'
| 'OT_PD_ENABLE_LIQUID_CLASSES'
| 'OT_PD_ENABLE_TIMELINE_SCRUBBER'
// flags that are not in this list only show in prerelease mode
export const userFacingFlags: FlagTypes[] = [
'OT_PD_DISABLE_MODULE_RESTRICTIONS',
Expand All @@ -50,5 +51,6 @@ export const allFlags: FlagTypes[] = [
'OT_PD_ENABLE_RETURN_TIP',
'OT_PD_ENABLE_REACT_SCAN',
'OT_PD_ENABLE_LIQUID_CLASSES',
'OT_PD_ENABLE_TIMELINE_SCRUBBER',
]
export type Flags = Partial<Record<FlagTypes, boolean | null | undefined>>
Loading

0 comments on commit 16bd893

Please sign in to comment.