diff --git a/app/src/analytics/__tests__/sessions-events.test.js b/app/src/analytics/__tests__/sessions-events.test.js deleted file mode 100644 index 4758afbd25b..00000000000 --- a/app/src/analytics/__tests__/sessions-events.test.js +++ /dev/null @@ -1,52 +0,0 @@ -// @flow - -import { makeEvent } from '../make-event' - -import * as Sessions from '../../sessions' -import * as sessionsFixtures from '../../sessions/__fixtures__' - -import type { State } from '../../types' -import type { SessionAnalyticsProps } from '../../sessions/types' - -jest.mock('../../sessions/selectors') - -const getAnalyticsPropsForRobotSessionById: JestMockFn< - [State, string, string], - SessionAnalyticsProps | null -> = Sessions.getAnalyticsPropsForRobotSessionById - -const MOCK_STATE: State = ({ mockState: true }: any) - -describe('events with calibration check session data', () => { - afterEach(() => { - jest.resetAllMocks() - }) - - it('sessions:DELETE_SESSION of type Calibration Check > sessionExit', () => { - getAnalyticsPropsForRobotSessionById.mockImplementation(state => { - expect(state).toBe(MOCK_STATE) - return sessionsFixtures.mockCalibrationCheckSessionAnalyticsProps - }) - const deleteSession = Sessions.deleteSession( - 'sacrosanct_coelacanth', - 'fake_session_id' - ) - - return expect(makeEvent(deleteSession, MOCK_STATE)).resolves.toEqual({ - name: 'calibrationCheckSessionExit', - properties: sessionsFixtures.mockCalibrationCheckSessionAnalyticsProps, - }) - }) - it('sessions:DELETE_SESSION of type Calibration Check > null', () => { - getAnalyticsPropsForRobotSessionById.mockImplementation(state => { - expect(state).toBe(MOCK_STATE) - return null - }) - const deleteSession = Sessions.deleteSession( - 'sacrosanct_coelacanth', - 'fake_session_id' - ) - - return expect(makeEvent(deleteSession, MOCK_STATE)).resolves.toEqual(null) - }) -}) diff --git a/app/src/analytics/make-event.js b/app/src/analytics/make-event.js index 28cf4c983d6..6e8010250cc 100644 --- a/app/src/analytics/make-event.js +++ b/app/src/analytics/make-event.js @@ -272,12 +272,7 @@ export function makeEvent( } case Sessions.DELETE_SESSION: { - const { robotName, sessionId } = action.payload - const analyticsProps = Sessions.getAnalyticsPropsForRobotSessionById( - state, - robotName, - sessionId - ) + const analyticsProps = null if (analyticsProps) { return Promise.resolve({ name: `${analyticsProps.sessionType}SessionExit`, diff --git a/app/src/calibration/pipette-offset/epic/fetchPipetteOffsetCalibrationsOnCalibrationEnd.js b/app/src/calibration/pipette-offset/epic/fetchPipetteOffsetCalibrationsOnCalibrationEnd.js index 872f5a7d23d..b10b7f7e95f 100644 --- a/app/src/calibration/pipette-offset/epic/fetchPipetteOffsetCalibrationsOnCalibrationEnd.js +++ b/app/src/calibration/pipette-offset/epic/fetchPipetteOffsetCalibrationsOnCalibrationEnd.js @@ -10,7 +10,7 @@ import type { Epic, State } from '../../../types' import type { SessionType } from '../../../sessions/types' import { DELETE_SESSION, - SESSION_TYPE_CALIBRATION_CHECK, + SESSION_TYPE_CALIBRATION_HEALTH_CHECK, SESSION_TYPE_TIP_LENGTH_CALIBRATION, SESSION_TYPE_DECK_CALIBRATION, SESSION_TYPE_PIPETTE_OFFSET_CALIBRATION, @@ -19,7 +19,7 @@ import { getRobotSessionById } from '../../../sessions/selectors' const isTargetSessionType: SessionType => boolean = sessionType => [ - SESSION_TYPE_CALIBRATION_CHECK, + SESSION_TYPE_CALIBRATION_HEALTH_CHECK, SESSION_TYPE_TIP_LENGTH_CALIBRATION, SESSION_TYPE_DECK_CALIBRATION, SESSION_TYPE_PIPETTE_OFFSET_CALIBRATION, diff --git a/app/src/calibration/tip-length/epic/fetchTipLengthCalibrationsOnCalibrationEnd.js b/app/src/calibration/tip-length/epic/fetchTipLengthCalibrationsOnCalibrationEnd.js index 63e2b64d361..5bc7d4ce8df 100644 --- a/app/src/calibration/tip-length/epic/fetchTipLengthCalibrationsOnCalibrationEnd.js +++ b/app/src/calibration/tip-length/epic/fetchTipLengthCalibrationsOnCalibrationEnd.js @@ -10,7 +10,7 @@ import type { Epic, State } from '../../../types' import type { SessionType } from '../../../sessions/types' import { DELETE_SESSION, - SESSION_TYPE_CALIBRATION_CHECK, + SESSION_TYPE_CALIBRATION_HEALTH_CHECK, SESSION_TYPE_TIP_LENGTH_CALIBRATION, SESSION_TYPE_PIPETTE_OFFSET_CALIBRATION, } from '../../../sessions/constants' @@ -18,7 +18,7 @@ import { getRobotSessionById } from '../../../sessions/selectors' const isTargetSessionType: SessionType => boolean = sessionType => [ - SESSION_TYPE_CALIBRATION_CHECK, + SESSION_TYPE_CALIBRATION_HEALTH_CHECK, SESSION_TYPE_TIP_LENGTH_CALIBRATION, SESSION_TYPE_PIPETTE_OFFSET_CALIBRATION, ].includes(sessionType) diff --git a/app/src/components/CalibrationPanels/DeckSetup.js b/app/src/components/CalibrationPanels/DeckSetup.js index 0fa26d81c8b..26aa61227d9 100644 --- a/app/src/components/CalibrationPanels/DeckSetup.js +++ b/app/src/components/CalibrationPanels/DeckSetup.js @@ -41,6 +41,9 @@ const contentsBySessionType: { [Sessions.SESSION_TYPE_PIPETTE_OFFSET_CALIBRATION]: { moveCommandString: Sessions.sharedCalCommands.MOVE_TO_TIP_RACK, }, + [Sessions.SESSION_TYPE_CALIBRATION_HEALTH_CHECK]: { + moveCommandString: Sessions.sharedCalCommands.MOVE_TO_TIP_RACK, + }, [Sessions.SESSION_TYPE_TIP_LENGTH_CALIBRATION]: { moveCommandString: Sessions.sharedCalCommands.MOVE_TO_REFERENCE_POINT, }, diff --git a/app/src/components/CalibrationPanels/Introduction.js b/app/src/components/CalibrationPanels/Introduction.js index fd943882216..73282a06fd0 100644 --- a/app/src/components/CalibrationPanels/Introduction.js +++ b/app/src/components/CalibrationPanels/Introduction.js @@ -37,6 +37,11 @@ const DECK_CAL_HEADER = 'deck calibration' const DECK_CAL_BODY = 'Deck calibration ensures positional accuracy so that your robot moves as expected. It will accurately establish the OT-2’s deck orientation relative to the gantry.' +const HEALTH_CHECK_HEADER = 'calibration health check' +const HEALTH_CHECK_BODY = + 'Checking the OT-2’s calibration is a first step towards diagnosing and troubleshooting common pipette positioning problems you may be experiencing.' +const HEALTH_CHECK_PROCEDURE = 'to calibration health check' + const PIP_OFFSET_CAL_HEADER = 'pipette offset calibration' const PIP_OFFSET_CAL_BODY = 'Calibrating pipette offset enables robot to accurately establish the location of the mounted pipette’s nozzle, relative to the deck.' @@ -54,6 +59,8 @@ const NOTE_BODY_OUTSIDE_PROTOCOL = 'important you perform this calibration using the Opentrons tips and tip racks specified above, as the robot determines accuracy based on the measurements of these tips.' const NOTE_BODY_PRE_PROTOCOL = 'important you perform this calibration using the exact tips specified in your protocol, as the robot uses the corresponding labware definition data to find the tip.' +const NOTE_HEALTH_CHECK_OUTCOMES = + 'If the difference between the two coordinates falls within the acceptable tolerance range for the given pipette, the check will pass. Otherwise, it will fail and you’ll be provided with troubleshooting guidance. You may exit at any point or continue through to the end to check the overall calibration status of your robot.' const VIEW_TIPRACK_MEASUREMENTS = 'View measurements' const contentsBySessionType: { @@ -82,6 +89,13 @@ const contentsBySessionType: { continueButtonText: `${START} ${TIP_LENGTH_CAL_HEADER}`, noteBody: NOTE_BODY_PRE_PROTOCOL, }, + [Sessions.SESSION_TYPE_CALIBRATION_HEALTH_CHECK]: { + headerText: HEALTH_CHECK_HEADER, + bodyText: HEALTH_CHECK_BODY, + continueButtonText: `${START} ${HEALTH_CHECK_HEADER}`, + continuingToText: HEALTH_CHECK_PROCEDURE, + noteBody: NOTE_HEALTH_CHECK_OUTCOMES, + }, } export function Introduction(props: CalibrationPanelProps): React.Node { @@ -100,6 +114,8 @@ export function Introduction(props: CalibrationPanelProps): React.Node { const lookupType = isExtendedPipOffset ? Sessions.SESSION_TYPE_TIP_LENGTH_CALIBRATION : sessionType + const isHealthCheck = + sessionType === Sessions.SESSION_TYPE_CALIBRATION_HEALTH_CHECK const proceed = () => sendCommands({ command: Sessions.sharedCalCommands.LOAD_LABWARE }) @@ -149,16 +165,27 @@ export function Introduction(props: CalibrationPanelProps): React.Node { )} - - {`${NOTE_HEADER} `} - {IT_IS} - {` ${EXTREMELY} `} - {noteBody} - + {!isHealthCheck ? ( + + {`${NOTE_HEADER} `} + {IT_IS} + {` ${EXTREMELY} `} + {noteBody} + + ) : ( + + {`${NOTE_HEADER} `} + {noteBody} + + )} diff --git a/app/src/components/CalibrationPanels/SaveXYPoint.js b/app/src/components/CalibrationPanels/SaveXYPoint.js index 059bb7c28a4..8c0cfd40267 100644 --- a/app/src/components/CalibrationPanels/SaveXYPoint.js +++ b/app/src/components/CalibrationPanels/SaveXYPoint.js @@ -21,9 +21,9 @@ import * as Sessions from '../../sessions' import type { JogAxis, JogDirection, JogStep } from '../../http-api-client' import type { CalibrationPanelProps } from './types' import type { - SessionCommandString, SessionType, CalibrationSessionStep, + SessionCommandString, } from '../../sessions/types' import { JogControls } from '../JogControls' import { formatJogVector } from './utils' @@ -76,23 +76,30 @@ const assetMap = { } const SAVE_XY_POINT_HEADER = 'Calibrate the X and Y-axis in' +const CHECK_POINT_XY_HEADER = 'Check the X and Y-axis in' const SLOT = 'slot' const JOG_UNTIL = 'Jog the robot until the tip is' const PRECISELY_CENTERED = 'precisely centered' const ABOVE_THE_CROSS = 'above the cross in' const THEN = 'Then press the' const TO_SAVE = 'button to calibrate the x and y-axis in' +const TO_CHECK = + 'button to determine how this position compares to the previously-saved x and y-axis calibration coordinates.' const BASE_BUTTON_TEXT = 'save calibration' +const HEALTH_BUTTON_TEXT = 'check x and y-axis' const MOVE_TO_POINT_TWO_BUTTON_TEXT = `${BASE_BUTTON_TEXT} and move to slot 3` const MOVE_TO_POINT_THREE_BUTTON_TEXT = `${BASE_BUTTON_TEXT} and move to slot 7` +const HEALTH_POINT_TWO_BUTTON_TEXT = `${HEALTH_BUTTON_TEXT} and move to slot 3` +const HEALTH_POINT_THREE_BUTTON_TEXT = `${HEALTH_BUTTON_TEXT} and move to slot 7` const contentsBySessionTypeByCurrentStep: { [SessionType]: { [CalibrationSessionStep]: { slotNumber: string, buttonText: string, - moveCommandString: SessionCommandString | null, + moveCommand: SessionCommandString | null, + finalCommand?: SessionCommandString | null, }, }, } = { @@ -100,35 +107,61 @@ const contentsBySessionTypeByCurrentStep: { [Sessions.DECK_STEP_SAVING_POINT_ONE]: { slotNumber: '1', buttonText: MOVE_TO_POINT_TWO_BUTTON_TEXT, - moveCommandString: Sessions.deckCalCommands.MOVE_TO_POINT_TWO, + moveCommand: Sessions.deckCalCommands.MOVE_TO_POINT_TWO, }, [Sessions.DECK_STEP_SAVING_POINT_TWO]: { slotNumber: '3', buttonText: MOVE_TO_POINT_THREE_BUTTON_TEXT, - moveCommandString: Sessions.deckCalCommands.MOVE_TO_POINT_THREE, + moveCommand: Sessions.deckCalCommands.MOVE_TO_POINT_THREE, }, [Sessions.DECK_STEP_SAVING_POINT_THREE]: { slotNumber: '7', buttonText: BASE_BUTTON_TEXT, - moveCommandString: Sessions.sharedCalCommands.MOVE_TO_TIP_RACK, + moveCommand: Sessions.sharedCalCommands.MOVE_TO_TIP_RACK, }, }, [Sessions.SESSION_TYPE_PIPETTE_OFFSET_CALIBRATION]: { [Sessions.PIP_OFFSET_STEP_SAVING_POINT_ONE]: { slotNumber: '1', buttonText: BASE_BUTTON_TEXT, - moveCommandString: null, + moveCommand: null, + }, + }, + [Sessions.SESSION_TYPE_CALIBRATION_HEALTH_CHECK]: { + [Sessions.CHECK_STEP_COMPARING_POINT_ONE]: { + slotNumber: '1', + buttonText: HEALTH_POINT_TWO_BUTTON_TEXT, + moveCommand: Sessions.deckCalCommands.MOVE_TO_POINT_TWO, + finalCommand: Sessions.sharedCalCommands.MOVE_TO_TIP_RACK, + }, + [Sessions.CHECK_STEP_COMPARING_POINT_TWO]: { + slotNumber: '3', + buttonText: HEALTH_POINT_THREE_BUTTON_TEXT, + moveCommand: Sessions.deckCalCommands.MOVE_TO_POINT_THREE, + }, + [Sessions.CHECK_STEP_COMPARING_POINT_THREE]: { + slotNumber: '7', + buttonText: HEALTH_BUTTON_TEXT, + moveCommand: Sessions.sharedCalCommands.MOVE_TO_TIP_RACK, }, }, } export function SaveXYPoint(props: CalibrationPanelProps): React.Node { - const { isMulti, mount, sendCommands, currentStep, sessionType } = props + const { + isMulti, + mount, + sendCommands, + currentStep, + sessionType, + activePipette, + } = props const { slotNumber, buttonText, - moveCommandString, + moveCommand, + finalCommand, } = contentsBySessionTypeByCurrentStep[sessionType][currentStep] const demoAsset = React.useMemo( @@ -137,6 +170,8 @@ export function SaveXYPoint(props: CalibrationPanelProps): React.Node { [slotNumber, mount, isMulti] ) + const isHealthCheck = + sessionType === Sessions.SESSION_TYPE_CALIBRATION_HEALTH_CHECK const jog = (axis: JogAxis, dir: JogDirection, step: JogStep) => { sendCommands({ command: Sessions.sharedCalCommands.JOG, @@ -147,10 +182,19 @@ export function SaveXYPoint(props: CalibrationPanelProps): React.Node { } const savePoint = () => { - let commands = [{ command: Sessions.sharedCalCommands.SAVE_OFFSET }] - - if (moveCommandString) { - commands = [...commands, { command: moveCommandString }] + let commands = null + if (isHealthCheck) { + commands = [{ command: Sessions.checkCommands.COMPARE_POINT }] + } else { + commands = [{ command: Sessions.sharedCalCommands.SAVE_OFFSET }] + } + if ( + finalCommand && + activePipette?.rank === Sessions.CHECK_PIPETTE_RANK_SECOND + ) { + commands = [...commands, { command: finalCommand }] + } else if (moveCommand) { + commands = [...commands, { command: moveCommand }] } sendCommands(...commands) } @@ -167,7 +211,7 @@ export function SaveXYPoint(props: CalibrationPanelProps): React.Node { fontWeight={FONT_WEIGHT_SEMIBOLD} textTransform={TEXT_TRANSFORM_UPPERCASE} > - {SAVE_XY_POINT_HEADER} + {isHealthCheck ? CHECK_POINT_XY_HEADER : SAVE_XY_POINT_HEADER} {` ${SLOT} ${slotNumber || ''}`} {THEN} {` ${buttonText} `} - {`${TO_SAVE} ${SLOT} ${slotNumber}`}. + {`${isHealthCheck ? TO_CHECK : TO_SAVE} ${SLOT} ${slotNumber}`}. { - sendCommands( - { command: Sessions.sharedCalCommands.SAVE_OFFSET }, - { command: Sessions.sharedCalCommands.MOVE_TO_POINT_ONE } - ) + const continueCommands = () => { + if (sessionType === Sessions.SESSION_TYPE_CALIBRATION_HEALTH_CHECK) { + return () => { + sendCommands( + { command: Sessions.checkCommands.COMPARE_POINT }, + { command: Sessions.sharedCalCommands.MOVE_TO_POINT_ONE } + ) + } + } else { + return () => { + sendCommands( + { command: Sessions.sharedCalCommands.SAVE_OFFSET }, + { command: Sessions.sharedCalCommands.MOVE_TO_POINT_ONE } + ) + } + } } const [confirmLink, confirmModal] = useConfirmCrashRecovery({ @@ -148,7 +165,7 @@ export function SaveZPoint(props: CalibrationPanelProps): React.Node { > diff --git a/app/src/components/CalibrationPanels/TipConfirmation.js b/app/src/components/CalibrationPanels/TipConfirmation.js index 2c7d794cfb6..b765b14b602 100644 --- a/app/src/components/CalibrationPanels/TipConfirmation.js +++ b/app/src/components/CalibrationPanels/TipConfirmation.js @@ -37,6 +37,10 @@ const contentsBySessionType: { yesButtonText: YES_AND_MOVE_TO_MEASURE_TIP, moveCommandString: Sessions.sharedCalCommands.MOVE_TO_REFERENCE_POINT, }, + [Sessions.SESSION_TYPE_CALIBRATION_HEALTH_CHECK]: { + yesButtonText: YES_AND_MOVE_TO_DECK, + moveCommandString: Sessions.sharedCalCommands.MOVE_TO_DECK, + }, } export function TipConfirmation(props: CalibrationPanelProps): React.Node { const { sendCommands, sessionType, shouldPerformTipLength } = props diff --git a/app/src/components/CalibrationPanels/types.js b/app/src/components/CalibrationPanels/types.js index c6299d0a505..3d5aab12229 100644 --- a/app/src/components/CalibrationPanels/types.js +++ b/app/src/components/CalibrationPanels/types.js @@ -4,8 +4,14 @@ import type { SessionType, CalibrationSessionStep, CalibrationLabware, + CalibrationHealthCheckInstrument, + CalibrationHealthCheckComparisonByPipette, } from '../../sessions/types' +// TODO (lc 10-20-2020) Given there are lots of optional +// keys here now we should split these panel props out +// into different session types and combine them into +// a union object export type CalibrationPanelProps = {| sendCommands: (...Array) => void, cleanUpAndExit: () => void, @@ -16,4 +22,8 @@ export type CalibrationPanelProps = {| sessionType: SessionType, calBlock?: CalibrationLabware | null, shouldPerformTipLength?: boolean | null, + checkBothPipettes?: boolean | null, + instruments?: Array | null, + comparisonsByPipette?: CalibrationHealthCheckComparisonByPipette | null, + activePipette?: CalibrationHealthCheckInstrument, |} diff --git a/app/src/components/CheckCalibration/BadCalibration.js b/app/src/components/CheckCalibration/BadCalibration.js index a30b1c300c8..ca901a99293 100644 --- a/app/src/components/CheckCalibration/BadCalibration.js +++ b/app/src/components/CheckCalibration/BadCalibration.js @@ -1,6 +1,8 @@ // @flow import * as React from 'react' import { Icon, PrimaryButton, Link } from '@opentrons/components' +import type { CalibrationPanelProps } from '../CalibrationPanels/types' + import styles from './styles.css' const BAD_ROBOT_CALIBRATION_CHECK_HEADER = 'Unable to check robot calibration' @@ -17,11 +19,9 @@ const BAD_ROBOT_CALIBRATION_CHECK_BUTTON_TEXT = 'Drop tip in trash and exit' const DECK_CAL_ARTICLE_URL = 'https://support.opentrons.com/en/articles/4028788-checking-your-ot-2-s-calibration' -type BadCalibrationProps = {| - deleteSession: () => mixed, -|} -export function BadCalibration(props: BadCalibrationProps): React.Node { - const { deleteSession } = props + +export function BadCalibration(props: CalibrationPanelProps): React.Node { + const { cleanUpAndExit } = props return ( @@ -46,7 +46,7 @@ export function BadCalibration(props: BadCalibrationProps): React.Node { {LEARN_MORE} - + {BAD_ROBOT_CALIBRATION_CHECK_BUTTON_TEXT} diff --git a/app/src/components/CheckCalibration/CheckHeight.js b/app/src/components/CheckCalibration/CheckHeight.js deleted file mode 100644 index 5349a50ea75..00000000000 --- a/app/src/components/CheckCalibration/CheckHeight.js +++ /dev/null @@ -1,198 +0,0 @@ -// @flow -import * as React from 'react' -import { PrimaryButton, Icon, type Mount } from '@opentrons/components' -import { getPipetteModelSpecs } from '@opentrons/shared-data' - -import type { RobotCalibrationCheckComparison } from '../../sessions/types' -import type { JogAxis, JogDirection, JogStep } from '../../http-api-client' -import { JogControls } from '../JogControls' -import styles from './styles.css' -import { getBadOutcomeHeader } from './utils' -import { EndOfStepComparison } from './EndOfStepComparisons' -import { BadOutcomeBody } from './BadOutcomeBody' - -import slot5LeftMultiDemoAsset from '../../assets/videos/cal-movement/SLOT_5_LEFT_MULTI_Z.webm' -import slot5LeftSingleDemoAsset from '../../assets/videos/cal-movement/SLOT_5_LEFT_SINGLE_Z.webm' -import slot5RightMultiDemoAsset from '../../assets/videos/cal-movement/SLOT_5_RIGHT_MULTI_Z.webm' -import slot5RightSingleDemoAsset from '../../assets/videos/cal-movement/SLOT_5_RIGHT_SINGLE_Z.webm' - -const assetMap = { - left: { - multi: slot5LeftMultiDemoAsset, - single: slot5LeftSingleDemoAsset, - }, - right: { - multi: slot5RightMultiDemoAsset, - single: slot5RightSingleDemoAsset, - }, -} - -const CHECK_Z_HEADER = 'check z-axis in slot 5' - -const JOG_UNTIL = 'Jog the pipette until the tip is' -const JUST_BARELY_TOUCHING = 'barely touching (less than 0.1mm)' -const DECK_IN = 'the deck in' -const SLOT_5 = 'slot 5' -const THEN = 'Then press the' -const CHECK_AXES = 'check z-axis' -const TO_DETERMINE_MATCH = - 'button to determine how this position compares to the previously-saved z-axis calibration coordinate.' - -const EXIT_CALIBRATION_CHECK = 'exit robot calibration check' - -const GOOD_INSPECTING_HEADER = 'Good calibration' -const BAD_INSPECTING_PREAMBLE = - 'Your current pipette tip position falls outside the acceptable tolerance range for a' -const GOOD_INSPECTING_PREAMBLE = - 'Your current pipette tip position falls within the acceptable tolerance range for a ' -const INSPECTING_COMPARISON = - "pipette when compared to your robot's saved Z-axis calibration coordinates." -const CONTINUE_BLURB = 'You may also continue forward to the next check.' - -type CheckHeightProps = {| - isMulti: boolean, - mount: Mount | null, - isInspecting: boolean, - comparison: RobotCalibrationCheckComparison, - pipetteModel: string, - exit: () => mixed, - nextButtonText: string, - comparePoint: () => void, - goToNextCheck: () => void, - jog: (JogAxis, JogDirection, JogStep) => void, -|} -export function CheckHeight(props: CheckHeightProps): React.Node { - const { - isMulti, - mount, - isInspecting, - comparison, - pipetteModel, - exit, - nextButtonText, - comparePoint, - goToNextCheck, - jog, - } = props - - const demoAsset = React.useMemo( - () => mount && assetMap[mount][isMulti ? 'multi' : 'single'], - [mount, isMulti] - ) - - return ( - <> - - {CHECK_Z_HEADER} - - {isInspecting ? ( - - ) : ( - <> - - - - {JOG_UNTIL} - {JUST_BARELY_TOUCHING} - {DECK_IN} - {SLOT_5}. - - - {THEN} - {CHECK_AXES} - {TO_DETERMINE_MATCH} - - - - - - - - - - - - - - {nextButtonText} - - - > - )} - > - ) -} - -type CompareZProps = {| - comparison: RobotCalibrationCheckComparison, - goToNextCheck: () => void, - pipetteModel: string, - exit: () => mixed, - nextButtonText: string, -|} -function CompareZ(props: CompareZProps) { - const { - comparison, - pipetteModel, - goToNextCheck, - exit, - nextButtonText, - } = props - const { exceedsThreshold, transformType } = comparison - const { displayName } = getPipetteModelSpecs(pipetteModel) || {} - let header = GOOD_INSPECTING_HEADER - let preamble = GOOD_INSPECTING_PREAMBLE - let icon = - - if (exceedsThreshold) { - header = getBadOutcomeHeader(transformType) - preamble = BAD_INSPECTING_PREAMBLE - icon = - } - - return ( - - - {icon} - {header} - - - {preamble} - - {displayName} - - {INSPECTING_COMPARISON} - - - {exceedsThreshold && ( - <> - - - - {CONTINUE_BLURB} - > - )} - - {exceedsThreshold && ( - {EXIT_CALIBRATION_CHECK} - )} - {nextButtonText} - - - ) -} diff --git a/app/src/components/CheckCalibration/CheckXYPoint.js b/app/src/components/CheckCalibration/CheckXYPoint.js deleted file mode 100644 index 4b16dc381eb..00000000000 --- a/app/src/components/CheckCalibration/CheckXYPoint.js +++ /dev/null @@ -1,233 +0,0 @@ -// @flow -import * as React from 'react' -import { Icon, PrimaryButton, type Mount } from '@opentrons/components' -import { getPipetteModelSpecs } from '@opentrons/shared-data' - -import type { RobotCalibrationCheckComparison } from '../../sessions/types' -import type { JogAxis, JogDirection, JogStep } from '../../http-api-client' -import { getBadOutcomeHeader } from './utils' -import styles from './styles.css' -import { EndOfStepComparison } from './EndOfStepComparisons' -import { BadOutcomeBody } from './BadOutcomeBody' -import { JogControls } from '../JogControls' - -import slot1LeftMultiDemoAsset from '../../assets/videos/cal-movement/SLOT_1_LEFT_MULTI_X-Y.webm' -import slot1LeftSingleDemoAsset from '../../assets/videos/cal-movement/SLOT_1_LEFT_SINGLE_X-Y.webm' -import slot1RightMultiDemoAsset from '../../assets/videos/cal-movement/SLOT_1_RIGHT_MULTI_X-Y.webm' -import slot1RightSingleDemoAsset from '../../assets/videos/cal-movement/SLOT_1_RIGHT_SINGLE_X-Y.webm' -import slot3LeftMultiDemoAsset from '../../assets/videos/cal-movement/SLOT_3_LEFT_MULTI_X-Y.webm' -import slot3LeftSingleDemoAsset from '../../assets/videos/cal-movement/SLOT_3_LEFT_SINGLE_X-Y.webm' -import slot3RightMultiDemoAsset from '../../assets/videos/cal-movement/SLOT_3_RIGHT_MULTI_X-Y.webm' -import slot3RightSingleDemoAsset from '../../assets/videos/cal-movement/SLOT_3_RIGHT_SINGLE_X-Y.webm' -import slot7LeftMultiDemoAsset from '../../assets/videos/cal-movement/SLOT_7_LEFT_MULTI_X-Y.webm' -import slot7LeftSingleDemoAsset from '../../assets/videos/cal-movement/SLOT_7_LEFT_SINGLE_X-Y.webm' -import slot7RightMultiDemoAsset from '../../assets/videos/cal-movement/SLOT_7_RIGHT_MULTI_X-Y.webm' -import slot7RightSingleDemoAsset from '../../assets/videos/cal-movement/SLOT_7_RIGHT_SINGLE_X-Y.webm' - -const assetMap = { - '1': { - left: { - multi: slot1LeftMultiDemoAsset, - single: slot1LeftSingleDemoAsset, - }, - right: { - multi: slot1RightMultiDemoAsset, - single: slot1RightSingleDemoAsset, - }, - }, - '3': { - left: { - multi: slot3LeftMultiDemoAsset, - single: slot3LeftSingleDemoAsset, - }, - right: { - multi: slot3RightMultiDemoAsset, - single: slot3RightSingleDemoAsset, - }, - }, - '7': { - left: { - multi: slot7LeftMultiDemoAsset, - single: slot7LeftSingleDemoAsset, - }, - right: { - multi: slot7RightMultiDemoAsset, - single: slot7RightSingleDemoAsset, - }, - }, -} - -const CHECK_POINT_XY_HEADER = 'Check the X and Y-axis in' -const SLOT = 'slot' -const JOG_UNTIL = 'Jog the robot until the tip is' -const PRECISELY_CENTERED = 'precisely centered' -const ABOVE_THE_CROSS = 'above the cross in' -const THEN = 'Then press the' -const CHECK_AXES = 'check x and y-axis' -const TO_DETERMINE_MATCH = - 'button to determine how this position compares to the previously-saved x and y-axis calibration coordinates.' -const EXIT_CHECK = 'Exit robot calibration check' - -const GOOD_INSPECTING_HEADER = 'Good calibration' -const BAD_INSPECTING_PREAMBLE = - 'Your current pipette tip position falls outside the acceptable tolerance range for a' -const INSPECTING_COMPARISON = - "pipette when compared to your robot's saved X and Y-axis calibration coordinates." -const GOOD_INSPECTING_PREAMBLE = - 'Your current pipette tip position falls within the acceptable tolerance range for a ' -const CONTINUE_BLURB = 'You may also continue forward to the next check.' - -type CheckXYPointProps = {| - slotNumber: string | null, - isMulti: boolean, - mount: ?Mount, - isInspecting: boolean, - comparison: RobotCalibrationCheckComparison, - pipetteModel: string, - nextButtonText: string, - exit: () => mixed, - comparePoint: () => void, - goToNextCheck: () => void, - jog: (JogAxis, JogDirection, JogStep) => void, -|} -export function CheckXYPoint(props: CheckXYPointProps): React.Node { - const { - slotNumber, - isMulti, - mount, - isInspecting, - comparison, - pipetteModel, - exit, - nextButtonText, - comparePoint, - goToNextCheck, - jog, - } = props - - const demoAsset = React.useMemo( - () => - slotNumber && assetMap[slotNumber][mount][isMulti ? 'multi' : 'single'], - [slotNumber, mount, isMulti] - ) - - return ( - <> - - - {CHECK_POINT_XY_HEADER} - - {`${SLOT} ${slotNumber || ''}`} - - - {isInspecting ? ( - - ) : ( - <> - - - - {JOG_UNTIL} - {PRECISELY_CENTERED} - {ABOVE_THE_CROSS} - {`${SLOT} ${slotNumber || ''}`}. - - - {THEN} - {CHECK_AXES} - {TO_DETERMINE_MATCH} - - - - - - - - - - - - - - {nextButtonText} - - - > - )} - > - ) -} - -type CompareXYProps = {| - comparison: RobotCalibrationCheckComparison, - pipetteModel: string, - goToNextCheck: () => void, - exit: () => mixed, - nextButtonText: string, -|} -function CompareXY(props: CompareXYProps) { - const { - comparison, - pipetteModel, - goToNextCheck, - exit, - nextButtonText, - } = props - const { exceedsThreshold, transformType } = comparison - const { displayName } = getPipetteModelSpecs(pipetteModel) || {} - let header = GOOD_INSPECTING_HEADER - let preamble = GOOD_INSPECTING_PREAMBLE - let icon = - - if (exceedsThreshold) { - header = getBadOutcomeHeader(transformType) - preamble = BAD_INSPECTING_PREAMBLE - icon = - } - - return ( - - - {icon} - {header} - - - {preamble} - - {displayName} - - {INSPECTING_COMPARISON} - - - {exceedsThreshold && ( - <> - - - - {CONTINUE_BLURB} - > - )} - - {exceedsThreshold && ( - {EXIT_CHECK} - )} - {nextButtonText} - - - ) -} diff --git a/app/src/components/CheckCalibration/DeckSetup.js b/app/src/components/CheckCalibration/DeckSetup.js deleted file mode 100644 index d95d958f1c1..00000000000 --- a/app/src/components/CheckCalibration/DeckSetup.js +++ /dev/null @@ -1,102 +0,0 @@ -// @flow -import * as React from 'react' -import map from 'lodash/map' -import { - OutlineButton, - RobotWorkSpace, - LabwareRender, - LabwareNameOverlay, - RobotCoordsForeignDiv, -} from '@opentrons/components' -import { getLabwareDisplayName } from '@opentrons/shared-data' -import { getDeckDefinitions } from '@opentrons/components/src/deck/getDeckDefinitions' -import { getLatestLabwareDef } from '../../getLabware' -import styles from './styles.css' - -import type { LabwareDefinition2, DeckSlot } from '@opentrons/shared-data' -import type { RobotCalibrationCheckLabware } from '../../sessions/types' - -const DECK_SETUP_PROMPT = - 'Place full tip rack(s) on the deck, in their designated slots, as illustrated below.' -const DECK_SETUP_BUTTON_TEXT = 'Confirm tip rack placement and continue' - -type DeckSetupProps = {| - labware: Array, - proceed: () => mixed, -|} -export function DeckSetup(props: DeckSetupProps): React.Node { - const { labware, proceed } = props - const deckDef = React.useMemo(() => getDeckDefinitions()['ot2_standard'], []) - return ( - <> - - {DECK_SETUP_PROMPT} - - {DECK_SETUP_BUTTON_TEXT} - - - - - {({ deckSlotsById }) => - map( - deckSlotsById, - (slot: $Values, slotId) => { - if (!slot.matingSurfaceUnitVector) return null // if slot has no mating surface, don't render anything in it - const labwareForSlot = labware.find(l => l.slot === slotId) - const labwareDef = getLatestLabwareDef(labwareForSlot?.loadName) - return labwareDef ? ( - - ) : null - } - ) - } - - - > - ) -} - -type TiprackRenderProps = {| - labwareDef: LabwareDefinition2, - slotDef: DeckSlot, -|} -export function TiprackRender(props: TiprackRenderProps): React.Node { - const { labwareDef, slotDef } = props - const title = getLabwareDisplayName(labwareDef) - return ( - - - - {/* title is capitalized by CSS, and "µL" capitalized is "ML" */} - - - - ) -} diff --git a/app/src/components/CheckCalibration/DifferenceValue.js b/app/src/components/CheckCalibration/DifferenceValue.js index 36eb5caa053..9f0f510afd5 100644 --- a/app/src/components/CheckCalibration/DifferenceValue.js +++ b/app/src/components/CheckCalibration/DifferenceValue.js @@ -7,12 +7,11 @@ import styles from './styles.css' import type { RobotCalibrationCheckStep } from '../../sessions/types' const axisIndicesByStep: { [RobotCalibrationCheckStep]: Array, ... } = { - [Sessions.CHECK_STEP_COMPARING_FIRST_PIPETTE_HEIGHT]: [2], - [Sessions.CHECK_STEP_COMPARING_FIRST_PIPETTE_POINT_ONE]: [0, 1], - [Sessions.CHECK_STEP_COMPARING_FIRST_PIPETTE_POINT_TWO]: [0, 1], - [Sessions.CHECK_STEP_COMPARING_FIRST_PIPETTE_POINT_THREE]: [0, 1], - [Sessions.CHECK_STEP_COMPARING_SECOND_PIPETTE_HEIGHT]: [2], - [Sessions.CHECK_STEP_COMPARING_SECOND_PIPETTE_POINT_ONE]: [0, 1], + [Sessions.CHECK_STEP_COMPARING_HEIGHT]: [2], + [Sessions.CHECK_STEP_COMPARING_POINT_ONE]: [0, 1], + [Sessions.CHECK_STEP_COMPARING_POINT_TWO]: [0, 1], + [Sessions.CHECK_STEP_COMPARING_POINT_THREE]: [0, 1], + [Sessions.CHECK_STEP_COMPARING_HEIGHT]: [2], } const AXIS_NAMES = ['X', 'Y', 'Z'] diff --git a/app/src/components/CheckCalibration/EndOfStepComparisons.js b/app/src/components/CheckCalibration/EndOfStepComparisons.js index da7ec4e5deb..5222c1ced99 100644 --- a/app/src/components/CheckCalibration/EndOfStepComparisons.js +++ b/app/src/components/CheckCalibration/EndOfStepComparisons.js @@ -8,12 +8,12 @@ import { ThresholdValue } from './ThresholdValue' import { IndividualAxisDifferenceValue } from './DifferenceValue' import type { Axis } from '../../robot/types' -import type { RobotCalibrationCheckComparison } from '../../sessions/types' +import type { CalibrationHealthCheckComparison } from '../../sessions/types' import styles from './styles.css' type EndOfStepComparisonsProps = {| - comparison: RobotCalibrationCheckComparison, + comparison: CalibrationHealthCheckComparison, forAxes: Array, |} diff --git a/app/src/components/CheckCalibration/Introduction.js b/app/src/components/CheckCalibration/Introduction.js deleted file mode 100644 index e23e573ee93..00000000000 --- a/app/src/components/CheckCalibration/Introduction.js +++ /dev/null @@ -1,105 +0,0 @@ -// @flow -import * as React from 'react' -import { PrimaryButton, AlertModal } from '@opentrons/components' - -import { getLatestLabwareDef } from '../../getLabware' -import styles from './styles.css' -import { tiprackImages } from './tiprackImages' - -const LABWARE_LIBRARY_PAGE_PATH = 'https://labware.opentrons.com' - -const ROBOT_CALIBRATION_INTRO_HEADER = - 'Checking the OT-2’s calibration is a first step towards diagnosing and troubleshooting common pipette positioning problems you may be experiencing.' - -const ROBOT_CALIBRATION_INTRO_INSTRUCTION = - 'For this process you will be asked to manually jog each attached pipette to designated positions on the robot’s deck. You will then prompt the robot to check how this positional coordinate compares to your previously saved calibration coordinate. Note that this process does not overwrite your existing calibration data.' - -const ROBOT_CALIBRATION_INTRO_OUTCOMES = - 'If the difference between the two coordinates falls within the acceptable tolerance range for the given pipette, the check will pass. Otherwise, it will fail and you’ll be provided with troubleshooting guidance. You may exit at any point or continue through to the end to check the overall calibration status of your robot.' - -const TIPRACK_REQS = 'For this process you will require:' -const VIEW_TIPRACK_MEASUREMENTS = 'View measurements' -const NOTE_HEADER = 'Please note: ' -const NOTE_BODY = - "It's important you perform this test using the Opentrons tips and tip racks specified above, as the robot determines accuracy based on the measurements of these tips." -const CANCEL = 'Cancel' -const CONTINUE = 'Continue' -const CLEAR_DECK_HEADER = 'Clear the deck' -const CLEAR_DECK_BODY = - 'Before continuing to check robot calibration, please remove all labware and modules from the deck.' - -type IntroductionProps = {| - labwareLoadNames: Array, - proceed: () => mixed, - exit: () => mixed, -|} -export function Introduction(props: IntroductionProps): React.Node { - const { labwareLoadNames, exit, proceed } = props - const [clearDeckWarningOpen, setClearDeckWarningOpen] = React.useState(false) - - // TODO: BC: investigate whether we should sub out the warning modal - // below for the existing ClearDeckAlertModal - - return ( - <> - - {ROBOT_CALIBRATION_INTRO_HEADER} - - {ROBOT_CALIBRATION_INTRO_INSTRUCTION} - - - {ROBOT_CALIBRATION_INTRO_OUTCOMES} - - - {TIPRACK_REQS} - - {labwareLoadNames.map(loadName => ( - - - - - - {getLatestLabwareDef(loadName)?.metadata.displayName} - - e.stopPropagation()} - > - {VIEW_TIPRACK_MEASUREMENTS} - - - ))} - - - {NOTE_HEADER} - {NOTE_BODY} - - - setClearDeckWarningOpen(true)} - className={styles.continue_button} - > - {CONTINUE} - - - {clearDeckWarningOpen && ( - - {CLEAR_DECK_BODY} - - )} - > - ) -} diff --git a/app/src/components/CheckCalibration/PipetteComparisons.js b/app/src/components/CheckCalibration/PipetteComparisons.js index 70463d03e9b..2408a365d41 100644 --- a/app/src/components/CheckCalibration/PipetteComparisons.js +++ b/app/src/components/CheckCalibration/PipetteComparisons.js @@ -15,10 +15,10 @@ import { ThresholdValue } from './ThresholdValue' import styles from './styles.css' import type { - RobotCalibrationCheckInstrument, + CalibrationHealthCheckInstrument, + CalibrationHealthCheckComparisonsByStep, + CalibrationHealthCheckComparison, RobotCalibrationCheckStep, - RobotCalibrationCheckComparison, - RobotCalibrationCheckComparisonsByStep, } from '../../sessions/types' const PASS = 'pass' @@ -40,17 +40,15 @@ const POINT_TWO_CHECK_DISPLAY_NAME = 'Slot 3 X/Y-axis' const POINT_THREE_CHECK_DISPLAY_NAME = 'Slot 7 X/Y-axis' const stepDisplayNameMap: { [RobotCalibrationCheckStep]: string, ... } = { - [Sessions.CHECK_STEP_COMPARING_FIRST_PIPETTE_HEIGHT]: HEIGHT_CHECK_DISPLAY_NAME, - [Sessions.CHECK_STEP_COMPARING_FIRST_PIPETTE_POINT_ONE]: POINT_ONE_CHECK_DISPLAY_NAME, - [Sessions.CHECK_STEP_COMPARING_FIRST_PIPETTE_POINT_TWO]: POINT_TWO_CHECK_DISPLAY_NAME, - [Sessions.CHECK_STEP_COMPARING_FIRST_PIPETTE_POINT_THREE]: POINT_THREE_CHECK_DISPLAY_NAME, - [Sessions.CHECK_STEP_COMPARING_SECOND_PIPETTE_HEIGHT]: HEIGHT_CHECK_DISPLAY_NAME, - [Sessions.CHECK_STEP_COMPARING_SECOND_PIPETTE_POINT_ONE]: POINT_ONE_CHECK_DISPLAY_NAME, + [Sessions.CHECK_STEP_COMPARING_HEIGHT]: HEIGHT_CHECK_DISPLAY_NAME, + [Sessions.CHECK_STEP_COMPARING_POINT_ONE]: POINT_ONE_CHECK_DISPLAY_NAME, + [Sessions.CHECK_STEP_COMPARING_POINT_TWO]: POINT_TWO_CHECK_DISPLAY_NAME, + [Sessions.CHECK_STEP_COMPARING_POINT_THREE]: POINT_THREE_CHECK_DISPLAY_NAME, } type PipetteComparisonsProps = {| - pipette: RobotCalibrationCheckInstrument, - comparisonsByStep: RobotCalibrationCheckComparisonsByStep, + pipette: CalibrationHealthCheckInstrument, + comparisonsByStep: CalibrationHealthCheckComparisonsByStep, allSteps: Array, |} @@ -119,7 +117,7 @@ export function PipetteComparisons(props: PipetteComparisonsProps): React.Node { } type StepStatusProps = {| - comparison: RobotCalibrationCheckComparison | null, + comparison: CalibrationHealthCheckComparison | null, |} const StepStatus = (props: StepStatusProps): React.Node => { diff --git a/app/src/components/CheckCalibration/ResultsSummary.js b/app/src/components/CheckCalibration/ResultsSummary.js index 224fff37b41..1e82fe6756d 100644 --- a/app/src/components/CheckCalibration/ResultsSummary.js +++ b/app/src/components/CheckCalibration/ResultsSummary.js @@ -1,56 +1,27 @@ // @flow import * as React from 'react' -import { useSelector } from 'react-redux' import { PrimaryButton, OutlineButton } from '@opentrons/components' import find from 'lodash/find' -import pick from 'lodash/pick' -import partition from 'lodash/partition' -import type { State } from '../../types' import * as Sessions from '../../sessions' -import * as Calibration from '../../calibration' import styles from './styles.css' import { PipetteComparisons } from './PipetteComparisons' -import { BadOutcomeBody } from './BadOutcomeBody' import { saveAs } from 'file-saver' -import { getBadOutcomeHeader } from './utils' -import type { CalibrationStatus } from '../../calibration/types' -import type { - RobotCalibrationCheckComparisonsByStep, - RobotCalibrationCheckComparison, - RobotCalibrationCheckInstrument, -} from '../../sessions/types' +import type { CalibrationPanelProps } from '../CalibrationPanels/types' +import type { CalibrationHealthCheckInstrument } from '../../sessions/types' const ROBOT_CALIBRATION_CHECK_SUMMARY_HEADER = 'Calibration check summary:' -const DROP_TIP_AND_EXIT = 'Drop tip in trash and exit' +const HOME_AND_EXIT = 'Home robot and exit' const DOWNLOAD_SUMMARY = 'Download JSON summary' -const STILL_HAVING_PROBLEMS = - 'If you are still experiencing issues, please download the JSON summary and share it with our support team who will then follow up with you.' -type ResultsSummaryProps = {| - robotName: string, - deleteSession: () => mixed, - comparisonsByStep: RobotCalibrationCheckComparisonsByStep, - instrumentsByMount: { [mount: string]: RobotCalibrationCheckInstrument, ... }, -|} -export function ResultsSummary(props: ResultsSummaryProps): React.Node { - const { - robotName, - deleteSession, - comparisonsByStep, - instrumentsByMount, - } = props - - const calibrationStatus = useSelector( - state => Calibration.getCalibrationStatus(state, robotName) - ) +export function ResultsSummary(props: CalibrationPanelProps): React.Node { + const { comparisonsByPipette, instruments, cleanUpAndExit } = props const handleDownloadButtonClick = () => { const now = new Date() const report = { - comparisonsByStep, - instrumentsByMount, - calibrationStatus, + comparisonsByPipette, + instruments, savedAt: now.toISOString(), } const data = new Blob([JSON.stringify(report)], { @@ -60,31 +31,25 @@ export function ResultsSummary(props: ResultsSummaryProps): React.Node { } const firstPipette = find( - instrumentsByMount, - (p: RobotCalibrationCheckInstrument) => + instruments, + (p: CalibrationHealthCheckInstrument) => p.rank === Sessions.CHECK_PIPETTE_RANK_FIRST ) const secondPipette = find( - instrumentsByMount, - (p: RobotCalibrationCheckInstrument) => + instruments, + (p: CalibrationHealthCheckInstrument) => p.rank === Sessions.CHECK_PIPETTE_RANK_SECOND ) - const [firstComparisonsByStep, secondComparisonsByStep] = partition( - Object.keys(comparisonsByStep), - compStep => Sessions.FIRST_PIPETTE_COMPARISON_STEPS.includes(compStep) - ).map(stepNames => pick(comparisonsByStep, stepNames)) + const firstComparisonsByStep = firstPipette + ? comparisonsByPipette?.first + : null + const secondComparisonsByStep = secondPipette + ? comparisonsByPipette?.second + : null - const lastFailedComparison = [ - ...Sessions.FIRST_PIPETTE_COMPARISON_STEPS, - ...Sessions.SECOND_PIPETTE_COMPARISON_STEPS, - ].reduce((acc, step): RobotCalibrationCheckComparison | null => { - const comparison = comparisonsByStep[step] - if (comparison && comparison.exceedsThreshold) { - return comparison - } else { - return acc - } - }, null) + // TODO (lc 10-20-2020): Rather than having the app decide + // what the last failed comparison was, the robot should + // just send a final report over to the app to decipher. return ( <> @@ -94,13 +59,15 @@ export function ResultsSummary(props: ResultsSummaryProps): React.Node { - + {firstPipette && firstComparisonsByStep && ( + + )} - {secondPipette && ( + {secondPipette && secondComparisonsByStep && ( {DOWNLOAD_SUMMARY} - {lastFailedComparison && ( - - )} - {DROP_TIP_AND_EXIT} + {HOME_AND_EXIT} > ) } - -type TroubleshootingInstructionsProps = { - comparison: RobotCalibrationCheckComparison, -} -function TroubleshootingInstructions( - props: TroubleshootingInstructionsProps -): React.Node { - const { comparison } = props - return ( - - - {getBadOutcomeHeader(comparison.transformType)} - - - - - {STILL_HAVING_PROBLEMS} - - ) -} diff --git a/app/src/components/CheckCalibration/ReturnTip.js b/app/src/components/CheckCalibration/ReturnTip.js new file mode 100644 index 00000000000..46715a8ff04 --- /dev/null +++ b/app/src/components/CheckCalibration/ReturnTip.js @@ -0,0 +1,61 @@ +// @flow +import * as React from 'react' +import { + Flex, + PrimaryBtn, + Text, + ALIGN_CENTER, + DIRECTION_COLUMN, + JUSTIFY_CENTER, + SPACING_3, +} from '@opentrons/components' + +import * as Sessions from '../../sessions' +import type { CalibrationPanelProps } from '../CalibrationPanels/types' + +const CONFIRM_RETURN_BODY = 'Return tip and ' +const CONTINUE_TO_NEXT = 'continue to next pipette' +const EXIT_PROGRAM = 'see calibration health check results' +const CONTINUE = 'continue to the next tiprack' +const EXIT = 'continue to the result summary' + +export function ReturnTip(props: CalibrationPanelProps): React.Node { + const { sendCommands, checkBothPipettes, activePipette } = props + const onFinalPipette = + !checkBothPipettes || + activePipette?.rank === Sessions.CHECK_PIPETTE_RANK_SECOND + const commandsList = onFinalPipette + ? [ + { command: Sessions.checkCommands.RETURN_TIP }, + { command: Sessions.checkCommands.TRANSITION }, + ] + : [ + { command: Sessions.checkCommands.RETURN_TIP }, + { command: Sessions.checkCommands.CHECK_SWITCH_PIPETTE }, + ] + + const confirmReturnTip = () => { + sendCommands(...commandsList) + } + return ( + + + {`${CONFIRM_RETURN_BODY} + ${onFinalPipette ? EXIT_PROGRAM : CONTINUE_TO_NEXT}`} + + + {onFinalPipette ? EXIT : CONTINUE} + + + ) +} diff --git a/app/src/components/CheckCalibration/TipPickUp.js b/app/src/components/CheckCalibration/TipPickUp.js deleted file mode 100644 index db93c1b84f4..00000000000 --- a/app/src/components/CheckCalibration/TipPickUp.js +++ /dev/null @@ -1,147 +0,0 @@ -// @flow -import * as React from 'react' -import { PrimaryButton } from '@opentrons/components' -import { getLabwareDisplayName } from '@opentrons/shared-data' - -import { getLatestLabwareDef } from '../../getLabware' -import { JogControls } from '../JogControls' -import styles from './styles.css' -import multiA1DemoAsset from '../../assets/videos/tip-pick-up/A1_Multi_Channel_REV1.webm' -import singleA1DemoAsset from '../../assets/videos/tip-pick-up/A1_Single_Channel_REV1.webm' -import multiB1DemoAsset from '../../assets/videos/tip-pick-up/B1_Multi_Channel_REV1.webm' -import singleB1DemoAsset from '../../assets/videos/tip-pick-up/B1_Single_Channel_REV1.webm' - -import type { RobotCalibrationCheckLabware } from '../../sessions/types' -import type { JogAxis, JogDirection, JogStep } from '../../http-api-client' - -const TIP_PICK_UP_HEADER = 'Position pipette over ' -const TIP_PICK_UP_BUTTON_TEXT = 'Pick up tip' - -const CONFIRM_TIP_BODY = 'Did pipette pick up tips successfully?' -const CONFIRM_TIP_YES_BUTTON_TEXT = 'Yes, continue' -const CONFIRM_TIP_NO_BUTTON_TEXT = 'No, try again' -const SINGLE_JOG_UNTIL_AT = 'Jog pipette until nozzle is centered above the' -const MULTI_JOG_UNTIL_AT = 'Jog pipette until the channel nozzle' -const CLOSEST = 'closest' -const TO_YOU_IS_CENTERED = 'to you is centered above the' -const POSITION = 'position' -const AND = 'and' -const FLUSH = 'flush' -const WITH_TOP_OF_TIP = 'with the top of the tip.' - -const ASSET_MAP = { - A1: { - multi: multiA1DemoAsset, - single: singleA1DemoAsset, - }, - B1: { - multi: multiB1DemoAsset, - single: singleB1DemoAsset, - }, -} -type TipPickUpProps = {| - isMulti: boolean, - tiprack: RobotCalibrationCheckLabware, - isInspecting: boolean, - tipRackWellName: string, - pickUpTip: () => void, - confirmTip: () => void, - invalidateTip: () => void, - jog: (JogAxis, JogDirection, JogStep) => void, -|} -export function TipPickUp(props: TipPickUpProps): React.Node { - const { - tiprack, - isMulti, - isInspecting, - tipRackWellName, - pickUpTip, - confirmTip, - invalidateTip, - jog, - } = props - const tiprackDef = React.useMemo( - () => getLatestLabwareDef(tiprack?.loadName), - [tiprack] - ) - - const demoAsset = - tipRackWellName && ASSET_MAP[tipRackWellName][isMulti ? 'multi' : 'single'] - - const jogUntilAbove = isMulti ? ( - <> - {MULTI_JOG_UNTIL_AT} - {CLOSEST} - {TO_YOU_IS_CENTERED} - > - ) : ( - SINGLE_JOG_UNTIL_AT - ) - - return ( - <> - - - {TIP_PICK_UP_HEADER} - {tiprackDef - ? getLabwareDisplayName(tiprackDef).replace('µL', 'uL') - : null} - - - - {isInspecting ? ( - - - {CONFIRM_TIP_BODY} - - - {CONFIRM_TIP_NO_BUTTON_TEXT} - - - {CONFIRM_TIP_YES_BUTTON_TEXT} - - - ) : ( - <> - - - {jogUntilAbove} - {tipRackWellName} - {`${POSITION} ${AND}`} - {FLUSH} - {WITH_TOP_OF_TIP} - - - - - - - - - - - - - {TIP_PICK_UP_BUTTON_TEXT} - - - > - )} - > - ) -} diff --git a/app/src/components/CheckCalibration/__tests__/BadCalibration.test.js b/app/src/components/CheckCalibration/__tests__/BadCalibration.test.js index 847c77566b9..c8e756f934e 100644 --- a/app/src/components/CheckCalibration/__tests__/BadCalibration.test.js +++ b/app/src/components/CheckCalibration/__tests__/BadCalibration.test.js @@ -3,6 +3,9 @@ import * as React from 'react' import { mount } from 'enzyme' import { act } from 'react-dom/test-utils' +import * as Sessions from '../../../sessions' +import { mockDeckCalTipRack } from '../../../sessions/__fixtures__' + import { BadCalibration } from '../BadCalibration' describe('BadCalibration', () => { @@ -12,13 +15,39 @@ describe('BadCalibration', () => { wrapper .find('PrimaryButton[children="Drop tip in trash and exit"]') .find('button') + const render = ( + props: $Shape> = {} + ) => { + const { + pipMount = 'left', + isMulti = false, + tipRack = mockDeckCalTipRack, + calBlock = null, + sendCommands = jest.fn(), + cleanUpAndExit = mockDeleteSession, + currentStep = Sessions.CHECK_STEP_BAD_ROBOT_CALIBRATION, + sessionType = Sessions.SESSION_TYPE_CALIBRATION_HEALTH_CHECK, + } = props + return mount( + + ) + } afterEach(() => { jest.resetAllMocks() }) it('clicking button exits calibration check', () => { - const wrapper = mount() + const wrapper = render() act(() => getExitButton(wrapper).invoke('onClick')()) wrapper.update() expect(mockDeleteSession).toHaveBeenCalled() diff --git a/app/src/components/CheckCalibration/__tests__/CheckCalibration.test.js b/app/src/components/CheckCalibration/__tests__/CheckCalibration.test.js index 7516a7fda68..cdac6059965 100644 --- a/app/src/components/CheckCalibration/__tests__/CheckCalibration.test.js +++ b/app/src/components/CheckCalibration/__tests__/CheckCalibration.test.js @@ -4,32 +4,27 @@ import { Provider } from 'react-redux' import { mount } from 'enzyme' import { act } from 'react-dom/test-utils' +import * as Sessions from '../../../sessions' import { getDeckDefinitions } from '@opentrons/components/src/deck/getDeckDefinitions' -import { SpinnerModalPage } from '@opentrons/components' -import * as Calibration from '../../../calibration' -import * as Sessions from '../../../sessions' -import * as RobotApi from '../../../robot-api' - -import { CheckCalibration } from '../index' -import { Introduction } from '../Introduction' -import { DeckSetup } from '../DeckSetup' -import { TipPickUp } from '../TipPickUp' -import { CheckXYPoint } from '../CheckXYPoint' -import { CheckHeight } from '../CheckHeight' +import { CheckHealthCalibration } from '../index' +import { BadCalibration } from '../BadCalibration' import { ResultsSummary } from '../ResultsSummary' -import { ConfirmExitModal } from '../ConfirmExitModal' +import { ReturnTip } from '../ReturnTip' +import { + Introduction, + DeckSetup, + TipPickUp, + TipConfirmation, + SaveXYPoint, + SaveZPoint, +} from '../../CalibrationPanels' import { mockCalibrationCheckSessionAttributes } from '../../../sessions/__fixtures__' -import { mockCalibrationStatus } from '../../../calibration/__fixtures__' -import type { State } from '../../../types' -import type { RequestState } from '../../../robot-api/types' import type { RobotCalibrationCheckStep } from '../../../sessions/types' jest.mock('@opentrons/components/src/deck/getDeckDefinitions') -jest.mock('../../../sessions/selectors') -jest.mock('../../../robot-api/selectors') jest.mock('../../../calibration/selectors') type CheckCalibrationSpec = { @@ -39,31 +34,11 @@ type CheckCalibrationSpec = { ... } -const getRobotSessionOfType: JestMockFn< - [State, string, Sessions.SessionType], - $Call< - typeof Sessions.getRobotSessionOfType, - State, - string, - Sessions.SessionType - > -> = Sessions.getRobotSessionOfType - -const getRequestById: JestMockFn< - [State, string], - $Call -> = RobotApi.getRequestById - const mockGetDeckDefinitions: JestMockFn< [], $Call > = getDeckDefinitions -const mockGetCalibrationStatus: JestMockFn< - [State, string], - $Call -> = Calibration.getCalibrationStatus - describe('CheckCalibration', () => { let mockStore let render @@ -72,57 +47,31 @@ describe('CheckCalibration', () => { id: 'fake_check_session_id', ...mockCalibrationCheckSessionAttributes, } - let mockRequestState: RequestState = { - status: 'success', - response: { - path: '/fake/api/path', - method: 'POST', - status: 200, - ok: true, - }, - } - - const mockCloseCalibrationCheck = jest.fn() - - const getBackButton = wrapper => + const getExitButton = wrapper => wrapper.find({ title: 'exit' }).find('button') - const getConfirmExitButton = wrapper => - wrapper - .find(ConfirmExitModal) - .find({ children: 'continue' }) - .find('button') - const POSSIBLE_CHILDREN = [ Introduction, DeckSetup, TipPickUp, - CheckXYPoint, - CheckHeight, + TipConfirmation, + SaveZPoint, + SaveXYPoint, ResultsSummary, + BadCalibration, ] const SPECS: Array = [ { component: Introduction, currentStep: 'sessionStarted' }, { component: DeckSetup, currentStep: 'labwareLoaded' }, - { component: TipPickUp, currentStep: 'preparingFirstPipette' }, - { component: TipPickUp, currentStep: 'inspectingFirstTip' }, - { component: TipPickUp, currentStep: 'preparingSecondPipette' }, - { component: TipPickUp, currentStep: 'inspectingSecondTip' }, - { component: CheckXYPoint, currentStep: 'joggingFirstPipetteToPointOne' }, - { component: CheckXYPoint, currentStep: 'comparingFirstPipettePointOne' }, - { component: CheckXYPoint, currentStep: 'joggingFirstPipetteToPointTwo' }, - { component: CheckXYPoint, currentStep: 'comparingFirstPipettePointTwo' }, - { component: CheckXYPoint, currentStep: 'joggingFirstPipetteToPointThree' }, - { component: CheckXYPoint, currentStep: 'comparingFirstPipettePointThree' }, - { component: CheckXYPoint, currentStep: 'joggingSecondPipetteToPointOne' }, - { component: CheckXYPoint, currentStep: 'comparingSecondPipettePointOne' }, - { component: CheckHeight, currentStep: 'joggingFirstPipetteToHeight' }, - { component: CheckHeight, currentStep: 'comparingFirstPipetteHeight' }, - { component: CheckHeight, currentStep: 'joggingSecondPipetteToHeight' }, - { component: CheckHeight, currentStep: 'comparingSecondPipetteHeight' }, - { component: ResultsSummary, currentStep: 'sessionExited' }, - { component: ResultsSummary, currentStep: 'checkComplete' }, + { component: TipPickUp, currentStep: 'preparingPipette' }, + { component: TipConfirmation, currentStep: 'inspectingTip' }, + { component: SaveZPoint, currentStep: 'comparingHeight' }, + { component: SaveXYPoint, currentStep: 'comparingPointOne' }, + { component: SaveXYPoint, currentStep: 'comparingPointTwo' }, + { component: SaveXYPoint, currentStep: 'comparingPointThree' }, + { component: ReturnTip, currentStep: 'returningTip' }, + { component: ResultsSummary, currentStep: 'resultsSummary' }, ] beforeEach(() => { @@ -135,18 +84,21 @@ describe('CheckCalibration', () => { dispatch, } mockGetDeckDefinitions.mockReturnValue({}) - mockGetCalibrationStatus.mockReturnValue(mockCalibrationStatus) mockCalibrationCheckSession = { id: 'fake_check_session_id', ...mockCalibrationCheckSessionAttributes, } - render = () => { + render = (props = {}) => { + const { showSpinner = false } = props return mount( - , { wrappingComponent: Provider, @@ -169,7 +121,6 @@ describe('CheckCalibration', () => { currentStep: spec.currentStep, }, } - getRobotSessionOfType.mockReturnValue(mockCalibrationCheckSession) const wrapper = render() POSSIBLE_CHILDREN.forEach(child => { if (child === spec.component) { @@ -179,66 +130,24 @@ describe('CheckCalibration', () => { } }) }) - - it(`renders a spinner when a request is pending in ${spec.currentStep}`, () => { - mockCalibrationCheckSession = { - ...mockCalibrationCheckSession, - details: { - ...mockCalibrationCheckSession.details, - currentStep: spec.currentStep, - }, - } - - getRobotSessionOfType.mockReturnValue(mockCalibrationCheckSession) - - mockRequestState = { - status: 'pending', - } - getRequestById.mockReturnValue(mockRequestState) - - const wrapper = render() - expect(wrapper.exists(SpinnerModalPage)).toBe(true) - }) }) - it('pops a confirm exit modal on exit click', () => { - getRobotSessionOfType.mockReturnValue(mockCalibrationCheckSession) + it('renders confirm exit modal on exit click', () => { const wrapper = render() - act(() => { - getBackButton(wrapper).invoke('onClick')() - }) + expect(wrapper.find('ConfirmExitModal').exists()).toBe(false) + act(() => getExitButton(wrapper).invoke('onClick')()) wrapper.update() - expect(wrapper.exists(ConfirmExitModal)).toBe(true) - expect(mockCloseCalibrationCheck).not.toHaveBeenCalled() + expect(wrapper.find('ConfirmExitModal').exists()).toBe(true) }) - it('calls deleteRobotCalibrationCheckSession when exit is confirmed', () => { - getRobotSessionOfType.mockReturnValue(mockCalibrationCheckSession) - const wrapper = render() - - act(() => { - getBackButton(wrapper).invoke('onClick')() - }) - wrapper.update() - - act(() => { - getConfirmExitButton(wrapper).invoke('onClick')() - }) - wrapper.update() + it('does not render spinner when showSpinner is false', () => { + const wrapper = render({ showSpinner: false }) + expect(wrapper.find('SpinnerModalPage').exists()).toBe(false) + }) - expect(mockStore.dispatch).toHaveBeenCalledWith( - expect.objectContaining({ - ...Sessions.createSessionCommand( - 'robot-name', - 'fake_check_session_id', - { - command: Sessions.checkCommands.EXIT, - data: {}, - } - ), - meta: { requestId: expect.any(String) }, - }) - ) + it('renders spinner when showSpinner is true', () => { + const wrapper = render({ showSpinner: true }) + expect(wrapper.find('SpinnerModalPage').exists()).toBe(true) }) }) diff --git a/app/src/components/CheckCalibration/__tests__/CheckHeight.test.js b/app/src/components/CheckCalibration/__tests__/CheckHeight.test.js deleted file mode 100644 index 96f623115a1..00000000000 --- a/app/src/components/CheckCalibration/__tests__/CheckHeight.test.js +++ /dev/null @@ -1,204 +0,0 @@ -// @flow -import * as React from 'react' -import { mount } from 'enzyme' -import { act } from 'react-dom/test-utils' - -import { - CHECK_TRANSFORM_TYPE_UNKNOWN, - CHECK_TRANSFORM_TYPE_INSTRUMENT_OFFSET, - CHECK_TRANSFORM_TYPE_DECK, -} from '../../../sessions' -import { CheckHeight } from '../CheckHeight' - -describe('CheckHeight', () => { - let render - - const mockComparePoint = jest.fn() - const mockGoToNextCheck = jest.fn() - const mockJog = jest.fn() - const mockExit = jest.fn() - - const getContinueButton = wrapper => - wrapper.find('PrimaryButton[children="Go To Next Check"]').find('button') - - const getJogButton = (wrapper, direction) => - wrapper.find(`JogButton[name="${direction}"]`).find('button') - - const getExitButton = wrapper => - wrapper - .find('PrimaryButton[children="exit robot calibration check"]') - .find('button') - - const getVideo = wrapper => wrapper.find(`source`) - - const getBadOutcomeBody = wrapper => wrapper.find('BadOutcomeBody') - const getOutcomeHeader = wrapper => - wrapper - .find('h3') - .at(1) - .text() - - beforeEach(() => { - render = (props = {}) => { - const { - isMulti = false, - isInspecting = false, - mountProp = 'left', - comparison = { - differenceVector: [0, 0, 0], - thresholdVector: [1, 1, 1], - exceedsThreshold: false, - transformType: CHECK_TRANSFORM_TYPE_UNKNOWN, - }, - pipetteModel = 'p300_single_v2.1', - } = props - return mount( - - ) - } - }) - afterEach(() => { - jest.resetAllMocks() - }) - - it('displays proper asset', () => { - const assetMap = { - left: { - multi: 'SLOT_5_LEFT_MULTI_Z.webm', - single: 'SLOT_5_LEFT_SINGLE_Z.webm', - }, - right: { - multi: 'SLOT_5_RIGHT_MULTI_Z.webm', - single: 'SLOT_5_RIGHT_SINGLE_Z.webm', - }, - } - - Object.keys(assetMap).forEach(mountString => { - Object.keys(assetMap[mountString]).forEach(channelString => { - const wrapper = render({ - mountProp: mountString, - isMulti: channelString === 'multi', - }) - expect(getVideo(wrapper).prop('src')).toEqual( - assetMap[mountString][channelString] - ) - }) - }) - }) - - it('allows jogging in z axis', () => { - const wrapper = render() - - const jogDirections = ['up', 'down'] - const jogParamsByDirection = { - up: ['z', 1, 0.1], - down: ['z', -1, 0.1], - } - jogDirections.forEach(direction => { - act(() => getJogButton(wrapper, direction).invoke('onClick')()) - wrapper.update() - - expect(mockJog).toHaveBeenCalledWith(...jogParamsByDirection[direction]) - }) - - const unavailableJogDirections = ['left', 'right', 'back', 'forward'] - unavailableJogDirections.forEach(direction => { - expect(getJogButton(wrapper, direction)).toEqual({}) - }) - }) - - it('compares check step when primary button is clicked', () => { - const wrapper = render() - - act(() => getContinueButton(wrapper).invoke('onClick')()) - wrapper.update() - - expect(mockComparePoint).toHaveBeenCalled() - }) - - it('confirms check step when isInspecting and primary button is clicked', () => { - const wrapper = render({ isInspecting: true }) - - act(() => getContinueButton(wrapper).invoke('onClick')()) - wrapper.update() - - expect(mockGoToNextCheck).toHaveBeenCalled() - }) - - it('no exit button when isInspecting not exceeded threshold', () => { - const wrapper = render({ isInspecting: true }) - - expect(getExitButton(wrapper).exists()).toBe(false) - }) - - it('confirms check step when isInspecting and primary button is clicked, and deck transform issue', () => { - const comparison = { - differenceVector: [0, 0, 0], - thresholdVector: [1, 1, 1], - exceedsThreshold: true, - transformType: CHECK_TRANSFORM_TYPE_DECK, - } - const wrapper = render({ isInspecting: true, comparison }) - - act(() => getContinueButton(wrapper).invoke('onClick')()) - wrapper.update() - - expect(mockGoToNextCheck).toHaveBeenCalled() - }) - - it('renders deck calibration when exceeds threshold and transform type is deck calibration', () => { - const comparison = { - differenceVector: [0, 0, 0], - thresholdVector: [1, 1, 1], - exceedsThreshold: true, - transformType: CHECK_TRANSFORM_TYPE_DECK, - } - const wrapper = render({ isInspecting: true, comparison }) - expect(getBadOutcomeBody(wrapper).exists()).toBe(true) - expect(getOutcomeHeader(wrapper)).toEqual( - expect.stringMatching(/Bad deck calibration data detected/) - ) - }) - - it('renders instrument offset blurb when exceeds threshold and transform type is instrument offset', () => { - const comparison = { - differenceVector: [0, 0, 0], - thresholdVector: [1, 1, 1], - exceedsThreshold: true, - transformType: CHECK_TRANSFORM_TYPE_INSTRUMENT_OFFSET, - } - const wrapper = render({ isInspecting: true, comparison }) - - expect(getBadOutcomeBody(wrapper).exists()).toBe(true) - expect(getOutcomeHeader(wrapper)).toEqual( - expect.stringMatching(/Bad pipette offset calibration data detected/) - ) - }) - - it('renders unknown blurb when exceeds threshold and transform type is unknown', () => { - const comparison = { - differenceVector: [0, 0, 0], - thresholdVector: [1, 1, 1], - exceedsThreshold: true, - transformType: CHECK_TRANSFORM_TYPE_UNKNOWN, - } - const wrapper = render({ isInspecting: true, comparison }) - expect(getBadOutcomeBody(wrapper).exists()).toBe(true) - expect(getOutcomeHeader(wrapper)).toEqual( - expect.stringMatching( - /Bad deck calibration data or pipette offset calibration data detected/ - ) - ) - }) -}) diff --git a/app/src/components/CheckCalibration/__tests__/CheckXYPoint.test.js b/app/src/components/CheckCalibration/__tests__/CheckXYPoint.test.js deleted file mode 100644 index ee6f35eed85..00000000000 --- a/app/src/components/CheckCalibration/__tests__/CheckXYPoint.test.js +++ /dev/null @@ -1,227 +0,0 @@ -// @flow -import * as React from 'react' -import { mount } from 'enzyme' -import { act } from 'react-dom/test-utils' -import type { Mount } from '@opentrons/components' - -import { - CHECK_TRANSFORM_TYPE_UNKNOWN, - CHECK_TRANSFORM_TYPE_INSTRUMENT_OFFSET, - CHECK_TRANSFORM_TYPE_DECK, -} from '../../../sessions' -import { CheckXYPoint } from '../CheckXYPoint' - -describe('CheckXYPoint', () => { - let render - - const mockComparePoint = jest.fn() - const mockGoToNextCheck = jest.fn() - const mockJog = jest.fn() - const mockExit = jest.fn() - - const getContinueButton = wrapper => - wrapper.find('PrimaryButton[children="continue"]').find('button') - - const getJogButton = (wrapper, direction) => - wrapper.find(`JogButton[name="${direction}"]`).find('button') - - const getBadOutcomeBody = wrapper => wrapper.find('BadOutcomeBody') - const getOutcomeHeader = wrapper => - wrapper - .find('h3') - .at(1) - .text() - - const getVideo = wrapper => wrapper.find(`source`) - - beforeEach(() => { - render = (props: $Shape> = {}) => { - const { - slotNumber = '1', - isMulti = false, - mount: mountProp = 'left', - isInspecting = false, - comparison = { - differenceVector: [0, 0, 0], - thresholdVector: [1, 1, 1], - exceedsThreshold: false, - transformType: CHECK_TRANSFORM_TYPE_UNKNOWN, - }, - pipetteModel = 'p300_single_v2.1', - nextButtonText = 'continue', - } = props - return mount( - - ) - } - }) - afterEach(() => { - jest.resetAllMocks() - }) - - it('displays proper asset', () => { - const slot1LeftMultiSrc = 'SLOT_1_LEFT_MULTI_X-Y.webm' - const slot1LeftSingleSrc = 'SLOT_1_LEFT_SINGLE_X-Y.webm' - const slot1RightMultiSrc = 'SLOT_1_RIGHT_MULTI_X-Y.webm' - const slot1RightSingleSrc = 'SLOT_1_RIGHT_SINGLE_X-Y.webm' - const slot3LeftMultiSrc = 'SLOT_3_LEFT_MULTI_X-Y.webm' - const slot3LeftSingleSrc = 'SLOT_3_LEFT_SINGLE_X-Y.webm' - const slot3RightMultiSrc = 'SLOT_3_RIGHT_MULTI_X-Y.webm' - const slot3RightSingleSrc = 'SLOT_3_RIGHT_SINGLE_X-Y.webm' - const slot7LeftMultiSrc = 'SLOT_7_LEFT_MULTI_X-Y.webm' - const slot7LeftSingleSrc = 'SLOT_7_LEFT_SINGLE_X-Y.webm' - const slot7RightMultiSrc = 'SLOT_7_RIGHT_MULTI_X-Y.webm' - const slot7RightSingleSrc = 'SLOT_7_RIGHT_SINGLE_X-Y.webm' - const assetMap: { [string]: { [Mount]: { ... }, ... }, ... } = { - '1': { - left: { - multi: slot1LeftMultiSrc, - single: slot1LeftSingleSrc, - }, - right: { - multi: slot1RightMultiSrc, - single: slot1RightSingleSrc, - }, - }, - '3': { - left: { - multi: slot3LeftMultiSrc, - single: slot3LeftSingleSrc, - }, - right: { - multi: slot3RightMultiSrc, - single: slot3RightSingleSrc, - }, - }, - '7': { - left: { - multi: slot7LeftMultiSrc, - single: slot7LeftSingleSrc, - }, - right: { - multi: slot7RightMultiSrc, - single: slot7RightSingleSrc, - }, - }, - } - Object.keys(assetMap).forEach(slotNumber => { - const xyStep = assetMap[slotNumber] - Object.keys(xyStep).forEach(mountString => { - Object.keys(xyStep[mountString]).forEach(channelString => { - const wrapper = render({ - mount: mountString, - isMulti: channelString === 'multi', - slotNumber: slotNumber, - }) - expect(getVideo(wrapper).prop('src')).toEqual( - xyStep[mountString][channelString] - ) - }) - }) - }) - }) - - it('allows jogging in x and y axis', () => { - const wrapper = render() - - const jogDirections = ['left', 'right', 'back', 'forward'] - const jogParamsByDirection = { - left: ['x', -1, 0.1], - right: ['x', 1, 0.1], - back: ['y', 1, 0.1], - forward: ['y', -1, 0.1], - } - jogDirections.forEach(direction => { - act(() => getJogButton(wrapper, direction).invoke('onClick')()) - wrapper.update() - - expect(mockJog).toHaveBeenCalledWith(...jogParamsByDirection[direction]) - }) - - const unavailableJogDirections = ['up', 'down'] - unavailableJogDirections.forEach(direction => { - expect(getJogButton(wrapper, direction)).toEqual({}) - }) - }) - - it('compares check step when primary button is clicked', () => { - const wrapper = render() - - act(() => getContinueButton(wrapper).invoke('onClick')()) - wrapper.update() - - expect(mockComparePoint).toHaveBeenCalled() - }) - - it('confirms check step when isInspecting and primary button is clicked, and deck transform issue', () => { - const comparison = { - differenceVector: [0, 0, 0], - thresholdVector: [1, 1, 1], - exceedsThreshold: true, - transformType: CHECK_TRANSFORM_TYPE_DECK, - } - const wrapper = render({ isInspecting: true, comparison }) - - act(() => getContinueButton(wrapper).invoke('onClick')()) - wrapper.update() - - expect(mockGoToNextCheck).toHaveBeenCalled() - }) - - it('renders deck calibration when exceeds threshold and transform type is deck calibration', () => { - const comparison = { - differenceVector: [0, 0, 0], - thresholdVector: [1, 1, 1], - exceedsThreshold: true, - transformType: CHECK_TRANSFORM_TYPE_DECK, - } - const wrapper = render({ isInspecting: true, comparison }) - expect(getBadOutcomeBody(wrapper).exists()).toBe(true) - expect(getOutcomeHeader(wrapper)).toEqual( - expect.stringMatching(/Bad deck calibration data detected/) - ) - }) - - it('renders instrument offset blurb when exceeds threshold and transform type is instrument offset', () => { - const comparison = { - differenceVector: [0, 0, 0], - thresholdVector: [1, 1, 1], - exceedsThreshold: true, - transformType: CHECK_TRANSFORM_TYPE_INSTRUMENT_OFFSET, - } - const wrapper = render({ isInspecting: true, comparison }) - - expect(getBadOutcomeBody(wrapper).exists()).toBe(true) - expect(getOutcomeHeader(wrapper)).toEqual( - expect.stringMatching(/Bad pipette offset calibration data detected/) - ) - }) - - it('renders unknown blurb when exceeds threshold and transform type is unknown', () => { - const comparison = { - differenceVector: [0, 0, 0], - thresholdVector: [1, 1, 1], - exceedsThreshold: true, - transformType: CHECK_TRANSFORM_TYPE_UNKNOWN, - } - const wrapper = render({ isInspecting: true, comparison }) - expect(getBadOutcomeBody(wrapper).exists()).toBe(true) - expect(getOutcomeHeader(wrapper)).toEqual( - expect.stringMatching( - /Bad deck calibration data or pipette offset calibration data detected/ - ) - ) - }) -}) diff --git a/app/src/components/CheckCalibration/__tests__/DeckSetup.test.js b/app/src/components/CheckCalibration/__tests__/DeckSetup.test.js deleted file mode 100644 index e25aa01ffe7..00000000000 --- a/app/src/components/CheckCalibration/__tests__/DeckSetup.test.js +++ /dev/null @@ -1,43 +0,0 @@ -// @flow -import * as React from 'react' -import { mount } from 'enzyme' -import { act } from 'react-dom/test-utils' -import { mockRobotCalibrationCheckSessionDetails } from '../../../sessions/__fixtures__' - -import { DeckSetup } from '../DeckSetup' - -jest.mock('../../../getLabware') - -jest.mock('@opentrons/components/src/deck/RobotWorkSpace', () => ({ - RobotWorkSpace: () => <>>, -})) - -describe('DeckSetup', () => { - let render - - const mockProceed = jest.fn() - - beforeEach(() => { - render = () => { - return mount( - - ) - } - }) - - afterEach(() => { - jest.resetAllMocks() - }) - - it('clicking continue proceeds to next step', () => { - const wrapper = render() - - act(() => wrapper.find('button').invoke('onClick')()) - wrapper.update() - - expect(mockProceed).toHaveBeenCalled() - }) -}) diff --git a/app/src/components/CheckCalibration/__tests__/DifferenceValue.test.js b/app/src/components/CheckCalibration/__tests__/DifferenceValue.test.js index 1a18b35cfc2..0cb7792f363 100644 --- a/app/src/components/CheckCalibration/__tests__/DifferenceValue.test.js +++ b/app/src/components/CheckCalibration/__tests__/DifferenceValue.test.js @@ -19,7 +19,7 @@ describe('DifferenceValue', () => { beforeEach(() => { render = ({ - stepName = Sessions.CHECK_STEP_COMPARING_FIRST_PIPETTE_HEIGHT, + stepName = Sessions.CHECK_STEP_COMPARING_HEIGHT, differenceVector = goodZComparison.differenceVector, }: { stepName?: RobotCalibrationCheckStep, @@ -72,7 +72,7 @@ describe('DifferenceValue', () => { it('renders good X/Y comparison', () => { const wrapper = render({ - stepName: Sessions.CHECK_STEP_COMPARING_FIRST_PIPETTE_POINT_ONE, + stepName: Sessions.CHECK_STEP_COMPARING_POINT_ONE, differenceVector: goodXYComparison.differenceVector, }) @@ -104,7 +104,7 @@ describe('DifferenceValue', () => { it('renders bad X/Y comparison', () => { const wrapper = render({ - stepName: Sessions.CHECK_STEP_COMPARING_FIRST_PIPETTE_POINT_ONE, + stepName: Sessions.CHECK_STEP_COMPARING_POINT_ONE, differenceVector: badXYComparison.differenceVector, }) @@ -136,7 +136,7 @@ describe('DifferenceValue', () => { it('renders no sign with zero value', () => { const wrapper = render({ - stepName: Sessions.CHECK_STEP_COMPARING_FIRST_PIPETTE_HEIGHT, + stepName: Sessions.CHECK_STEP_COMPARING_HEIGHT, differenceVector: [0, 0, 0], }) @@ -150,7 +150,7 @@ describe('DifferenceValue', () => { it('renders plus sign with positive value', () => { const wrapper = render({ - stepName: Sessions.CHECK_STEP_COMPARING_FIRST_PIPETTE_HEIGHT, + stepName: Sessions.CHECK_STEP_COMPARING_HEIGHT, differenceVector: [0, 0, 5.2], }) @@ -164,7 +164,7 @@ describe('DifferenceValue', () => { it('renders minus sign with negative value', () => { const wrapper = render({ - stepName: Sessions.CHECK_STEP_COMPARING_FIRST_PIPETTE_HEIGHT, + stepName: Sessions.CHECK_STEP_COMPARING_HEIGHT, differenceVector: [0, 0, -3.1], }) diff --git a/app/src/components/CheckCalibration/__tests__/Introduction.test.js b/app/src/components/CheckCalibration/__tests__/Introduction.test.js deleted file mode 100644 index 36797f063ef..00000000000 --- a/app/src/components/CheckCalibration/__tests__/Introduction.test.js +++ /dev/null @@ -1,80 +0,0 @@ -// @flow -import * as React from 'react' -import { mount } from 'enzyme' -import { act } from 'react-dom/test-utils' -import { AlertModal } from '@opentrons/components' - -import { Introduction } from '../Introduction' - -describe('Introduction', () => { - let render - - const mockProceed = jest.fn() - const mockExit = jest.fn() - - const getContinueButton = wrapper => - wrapper.find('PrimaryButton[children="Continue"]').find('button') - - const getClearDeckContinueButton = wrapper => - wrapper - .find(AlertModal) - .find('OutlineButton[children="Continue"]') - .find('button') - - const getClearDeckCancelButton = wrapper => - wrapper - .find(AlertModal) - .find('OutlineButton[children="Cancel"]') - .find('button') - - const tiprackLoadnames = [ - 'opentrons_96_tiprack_20ul', - 'opentrons_96_tiprack_300ul', - ] - - beforeEach(() => { - render = (labwareLoadNames = tiprackLoadnames) => { - return mount( - - ) - } - }) - - afterEach(() => { - jest.resetAllMocks() - }) - - it('Clear deck warning is not visible on mount', () => { - const wrapper = render() - - expect(wrapper.exists(Introduction)).toBe(true) - expect(wrapper.exists('AlertModal[heading="Clear the deck"]')).toBe(false) - }) - - it('clicking continue opens clear deck warning', () => { - const wrapper = render() - - act(() => getContinueButton(wrapper).invoke('onClick')()) - wrapper.update() - - expect(wrapper.exists('AlertModal[heading="Clear the deck"]')).toBe(true) - }) - - it('clicking continue in clear deck warning proceeds to next step and cancel exits', () => { - const wrapper = render() - act(() => getContinueButton(wrapper).invoke('onClick')()) - wrapper.update() - - act(() => getClearDeckContinueButton(wrapper).invoke('onClick')()) - wrapper.update() - expect(mockProceed).toHaveBeenCalled() - - act(() => getClearDeckCancelButton(wrapper).invoke('onClick')()) - wrapper.update() - expect(mockExit).toHaveBeenCalled() - }) -}) diff --git a/app/src/components/CheckCalibration/__tests__/PipetteComparisons.test.js b/app/src/components/CheckCalibration/__tests__/PipetteComparisons.test.js index 847707a5a64..457593d22f0 100644 --- a/app/src/components/CheckCalibration/__tests__/PipetteComparisons.test.js +++ b/app/src/components/CheckCalibration/__tests__/PipetteComparisons.test.js @@ -4,8 +4,8 @@ import { mount } from 'enzyme' import * as Sessions from '../../../sessions' import * as Fixtures from '../../../sessions/__fixtures__' import type { - RobotCalibrationCheckComparisonsByStep, - RobotCalibrationCheckInstrument, + CalibrationHealthCheckComparisonsByStep, + CalibrationHealthCheckInstrument, } from '../../../sessions/types' import { PipetteComparisons } from '../PipetteComparisons' @@ -22,11 +22,11 @@ describe('PipetteComparisons', () => { beforeEach(() => { render = ({ - pipette = mockSessionDetails.instruments.right, - comparisonsByStep = mockSessionDetails.comparisonsByStep, + pipette = mockSessionDetails.activePipette, + comparisonsByStep = mockSessionDetails.comparisonsByPipette.first, }: { - pipette?: RobotCalibrationCheckInstrument, - comparisonsByStep?: RobotCalibrationCheckComparisonsByStep, + pipette?: CalibrationHealthCheckInstrument, + comparisonsByStep?: CalibrationHealthCheckComparisonsByStep, } = {}) => { return mount( { let render let mockStore let dispatch - - const mockDeleteSession = jest.fn() + let mockDeleteSession const getExitButton = wrapper => - wrapper - .find('PrimaryButton[children="Drop tip in trash and exit"]') - .find('button') + wrapper.find('PrimaryButton[children="Home robot and exit"]').find('button') const getSaveButton = wrapper => wrapper @@ -50,6 +41,9 @@ describe('ResultsSummary', () => { .find('button') beforeEach(() => { + mockDeleteSession = jest.fn() + const mockSendCommands = jest.fn() + mockGetCalibrationStatus.mockReturnValue(mockCalibrationStatus) dispatch = jest.fn() mockStore = { subscribe: () => {}, @@ -58,20 +52,33 @@ describe('ResultsSummary', () => { }), dispatch, } - mockGetCalibrationStatus.mockReturnValue(mockCalibrationStatus) - render = ({ - instrumentsByMount = mockSessionDetails.instruments, - comparisonsByStep = mockSessionDetails.comparisonsByStep, - }: { - instrumentsByMount?: RobotCalibrationCheckInstrumentsByMount, - comparisonsByStep?: RobotCalibrationCheckComparisonsByStep, - } = {}) => { + render = ( + props: $Shape> = {} + ) => { + const { + pipMount = 'left', + isMulti = false, + tipRack = Fixtures.mockDeckCalTipRack, + calBlock = null, + sendCommands = mockSendCommands, + cleanUpAndExit = mockDeleteSession, + currentStep = Sessions.CHECK_STEP_RESULTS_SUMMARY, + sessionType = Sessions.SESSION_TYPE_CALIBRATION_HEALTH_CHECK, + comparisonsByPipette = mockSessionDetails.comparisonsByPipette, + instruments = mockSessionDetails.instruments, + } = props return mount( , { wrappingComponent: Provider, @@ -94,22 +101,23 @@ describe('ResultsSummary', () => { .at(0) .find('h5') .text() - ).toEqual(expect.stringContaining('right')) + ).toEqual(expect.stringContaining('left')) expect( wrapper .find('PipetteComparisons') .at(1) .find('h5') .text() - ).toEqual(expect.stringContaining('left')) + ).toEqual(expect.stringContaining('right')) }) it('summarizes both pipettes if no comparisons have been made', () => { + const emptyComparison = { + first: {}, + second: {}, + } const wrapper = render({ - comparisonsByStep: omit( - mockSessionDetails.comparisonsByStep, - Sessions.SECOND_PIPETTE_COMPARISON_STEPS - ), + comparisonsByPipette: emptyComparison, }) expect( @@ -118,28 +126,27 @@ describe('ResultsSummary', () => { .at(0) .find('h5') .text() - ).toEqual(expect.stringContaining('right')) + ).toEqual(expect.stringContaining('left')) expect( wrapper .find('PipetteComparisons') .at(1) .find('h5') .text() - ).toEqual(expect.stringContaining('left')) + ).toEqual(expect.stringContaining('right')) }) it('does not summarize second pipette if none present', () => { const wrapper = render({ - instrumentsByMount: omit(mockSessionDetails.instruments, 'left'), + instruments: [mockSessionDetails.instruments[0]], }) - expect( wrapper .find('PipetteComparisons') .at(0) .find('h5') .text() - ).toEqual(expect.stringContaining('right')) + ).toEqual(expect.stringContaining('left')) expect( wrapper .find('PipetteComparisons') @@ -148,27 +155,8 @@ describe('ResultsSummary', () => { ).toBe(false) }) - it('does not show troubleshooting intstructions if no failures', () => { - const wrapper = render() - - expect(wrapper.find('TroubleshootingInstructions').exists()).toBe(false) - }) - - it('does show troubleshooting intstructions at least one failed check', () => { - const wrapper = render({ - comparisonsByStep: { - ...mockSessionDetails.comparisonsByStep, - [Sessions.CHECK_STEP_COMPARING_SECOND_PIPETTE_POINT_ONE]: - Fixtures.badXYComparison, - }, - }) - - expect(wrapper.find('TroubleshootingInstructions').exists()).toBe(true) - }) - it('exits when button is clicked', () => { const wrapper = render() - act(() => getExitButton(wrapper).invoke('onClick')()) wrapper.update() diff --git a/app/src/components/CheckCalibration/__tests__/TipPickup.test.js b/app/src/components/CheckCalibration/__tests__/TipPickup.test.js deleted file mode 100644 index 1aa50f19efb..00000000000 --- a/app/src/components/CheckCalibration/__tests__/TipPickup.test.js +++ /dev/null @@ -1,92 +0,0 @@ -// @flow -import * as React from 'react' -import { mount } from 'enzyme' -import { act } from 'react-dom/test-utils' -import { mockRobotCalibrationCheckSessionDetails } from '../../../sessions/__fixtures__' -import { TipPickUp } from '../TipPickUp' - -describe('TipPickUp', () => { - let render - - const mockPickUpTip = jest.fn() - const mockConfirmTip = jest.fn() - const mockInvalidateTip = jest.fn() - const mockJog = jest.fn() - - const getContinueButton = wrapper => - wrapper.find('PrimaryButton[children="Pick up tip"]').find('button') - - const getJogButton = (wrapper, direction) => - wrapper.find(`JogButton[name="${direction}"]`).find('button') - - const getConfirmButton = wrapper => - wrapper.find('PrimaryButton[children="Yes, continue"]').find('button') - - const getRejectButton = (wrapper, direction) => - wrapper.find('PrimaryButton[children="No, try again"]').find('button') - - beforeEach(() => { - render = (props = {}) => { - const { - isMulti = false, - tiprack = mockRobotCalibrationCheckSessionDetails.labware[0], - isInspecting = false, - tipRackWellName = 'A1', - } = props - return mount( - - ) - } - }) - afterEach(() => { - jest.resetAllMocks() - }) - - it('picks up tip after jogging, when not inspecting tip', () => { - const wrapper = render() - - const jogDirections = ['left', 'right', 'back', 'forward', 'up', 'down'] - const jogParamsByDirection = { - left: ['x', -1, 0.1], - right: ['x', 1, 0.1], - back: ['y', 1, 0.1], - forward: ['y', -1, 0.1], - up: ['z', 1, 0.1], - down: ['z', -1, 0.1], - } - jogDirections.forEach(direction => { - act(() => getJogButton(wrapper, direction).invoke('onClick')()) - wrapper.update() - - expect(mockJog).toHaveBeenCalledWith(...jogParamsByDirection[direction]) - }) - - act(() => getContinueButton(wrapper).invoke('onClick')()) - wrapper.update() - - expect(mockPickUpTip).toHaveBeenCalled() - }) - - it('gives option to continue or invalidate tip if inspecting', () => { - const wrapper = render({ isInspecting: true }) - - act(() => getConfirmButton(wrapper).invoke('onClick')()) - wrapper.update() - - expect(mockConfirmTip).toHaveBeenCalled() - - act(() => getRejectButton(wrapper).invoke('onClick')()) - wrapper.update() - - expect(mockInvalidateTip).toHaveBeenCalled() - }) -}) diff --git a/app/src/components/CheckCalibration/index.js b/app/src/components/CheckCalibration/index.js index fda9645a3dd..14a3a99d5ad 100644 --- a/app/src/components/CheckCalibration/index.js +++ b/app/src/components/CheckCalibration/index.js @@ -1,451 +1,188 @@ // @flow import * as React from 'react' -import { useSelector, useDispatch } from 'react-redux' -import last from 'lodash/last' + +import { getPipetteModelSpecs } from '@opentrons/shared-data' import { ModalPage, SpinnerModalPage, - LEFT, - RIGHT, - type Mount, useConditionalConfirm, + DISPLAY_FLEX, + DIRECTION_COLUMN, + ALIGN_CENTER, + JUSTIFY_CENTER, + SPACING_3, + C_TRANSPARENT, + ALIGN_FLEX_START, + C_WHITE, } from '@opentrons/components' -import { getPipetteModelSpecs } from '@opentrons/shared-data' -import { useDispatchApiRequest, getRequestById, PENDING } from '../../robot-api' -import * as Sessions from '../../sessions' - -import type { TitleBarProps } from '@opentrons/components' -import type { State, Dispatch } from '../../types' -import type { RequestState } from '../../robot-api/types' -import type { JogAxis, JogDirection, JogStep } from '../../http-api-client' -import * as SessionTypes from '../../sessions/types' -// TODO: BC 2020-09-02 use shared components from CalibrationPanels for child -// components, and delete duplicate files in this directory -import { Introduction } from './Introduction' -import { DeckSetup } from './DeckSetup' -import { TipPickUp } from './TipPickUp' +import * as Sessions from '../../sessions' +import { + Introduction, + DeckSetup, + TipPickUp, + TipConfirmation, + SaveZPoint, + SaveXYPoint, + ConfirmExitModal, +} from '../CalibrationPanels' +import { ReturnTip } from './ReturnTip' import { ResultsSummary } from './ResultsSummary' -import { CheckXYPoint } from './CheckXYPoint' -import { CheckHeight } from './CheckHeight' import { BadCalibration } from './BadCalibration' -import { ConfirmExitModal } from './ConfirmExitModal' -import { formatJogVector } from './utils' -import styles from './styles.css' - -const ROBOT_CALIBRATION_CHECK_SUBTITLE = 'Robot calibration check' -const MOVE_TO_NEXT = 'move to next check' -const CONTINUE = 'continue' -const EXIT = 'exit' -const DROP_TIP_AND_DO_SECOND_PIPETTE = - 'drop tip in trash and continue to 2nd pipette' -const CHECK_X_Y_AXES = 'check x and y-axis' -const CHECK_Z_AXIS = 'check z-axis' - -type CheckCalibrationProps = {| - robotName: string, - closeCalibrationCheck: () => mixed, -|} -export function CheckCalibration(props: CheckCalibrationProps): React.Node { - const { robotName, closeCalibrationCheck } = props - const dispatch = useDispatch() - const [dispatchRequest, requestIds] = useDispatchApiRequest() - - const requestStatus = useSelector(state => - getRequestById(state, last(requestIds)) - )?.status - const robotCalCheckSession = useSelector((state: State) => { - const session: Sessions.Session | null = Sessions.getRobotSessionOfType( - state, - robotName, - Sessions.SESSION_TYPE_CALIBRATION_CHECK - ) - if ( - session && - session.sessionType === Sessions.SESSION_TYPE_CALIBRATION_CHECK - ) { - return session - } - return {} - }) - const { currentStep, labware, instruments, comparisonsByStep } = - robotCalCheckSession.details || {} +import type { StyleProps } from '@opentrons/components' +import type { SessionCommandParams } from '../../sessions/types' - const hasTwoPipettes = React.useMemo( - () => instruments && Object.keys(instruments).length === 2, - [instruments] - ) +import type { CalibrationPanelProps } from '../CalibrationPanels/types' +import type { CalibrationHealthCheckParentProps } from './types' - const activeInstrument = React.useMemo(() => { - const rank = getPipetteRankForStep(currentStep) - const activeInstrId = - instruments && - Object.keys(instruments).find(mount => - mount ? instruments[mount]?.rank === rank : null - ) - return activeInstrId && instruments[activeInstrId] - }, [currentStep, instruments]) +const ROBOT_CALIBRATION_CHECK_SUBTITLE = 'Calibration health check' +const EXIT = 'exit' - const activeMount: Mount | null = React.useMemo(() => { - const rawMount = activeInstrument && activeInstrument.mount.toLowerCase() - return [LEFT, RIGHT].find(m => m === rawMount) || null - }, [activeInstrument]) +const darkContentsStyleProps = { + display: DISPLAY_FLEX, + flexDirection: DIRECTION_COLUMN, + alignItems: ALIGN_CENTER, + padding: SPACING_3, + backgroundColor: C_TRANSPARENT, + height: '100%', +} +const contentsStyleProps = { + display: DISPLAY_FLEX, + backgroundColor: C_WHITE, + flexDirection: DIRECTION_COLUMN, + justifyContent: JUSTIFY_CENTER, + alignItems: ALIGN_FLEX_START, + padding: SPACING_3, + maxWidth: '48rem', + minHeight: '14rem', +} - const activeLabware = React.useMemo( - () => - labware && - activeInstrument && - labware.find(l => l.id === activeInstrument.tiprack_id), - [labware, activeInstrument] - ) - const isActiveInstrumentMultiChannel = React.useMemo(() => { - const spec = - instruments && - activeInstrument && - getPipetteModelSpecs(activeInstrument?.model) - return spec ? spec.channels > 1 : false - }, [activeInstrument, instruments]) +const terminalContentsStyleProps = { + ...contentsStyleProps, + paddingX: '1.5rem', +} - const activeInstrumentModel = React.useMemo(() => { - const spec = instruments && activeInstrument && activeInstrument.model - return spec || '' - }, [activeInstrument, instruments]) +const PANEL_BY_STEP: { + [string]: React.ComponentType, +} = { + [Sessions.CHECK_STEP_SESSION_STARTED]: Introduction, + [Sessions.CHECK_STEP_LABWARE_LOADED]: DeckSetup, + [Sessions.CHECK_STEP_PREPARING_PIPETTE]: TipPickUp, + [Sessions.CHECK_STEP_INSPECTING_TIP]: TipConfirmation, + [Sessions.CHECK_STEP_COMPARING_HEIGHT]: SaveZPoint, + [Sessions.CHECK_STEP_COMPARING_POINT_ONE]: SaveXYPoint, + [Sessions.CHECK_STEP_COMPARING_POINT_TWO]: SaveXYPoint, + [Sessions.CHECK_STEP_COMPARING_POINT_THREE]: SaveXYPoint, + [Sessions.CHECK_STEP_RETURNING_TIP]: ReturnTip, + [Sessions.CHECK_STEP_RESULTS_SUMMARY]: ResultsSummary, + [Sessions.CHECK_STEP_BAD_ROBOT_CALIBRATION]: BadCalibration, +} - const tipRackWellName: string = React.useMemo(() => { - const instr_ids = instruments ? Object.keys(instruments) : [] - if (!activeInstrument) { - return '' - } else if ( - hasTwoPipettes && - instruments[instr_ids[0]]?.tiprack_id === - instruments[instr_ids[1]]?.tiprack_id && - activeInstrument.mount.toLowerCase() === LEFT - ) { - return 'B1' - } else if (instr_ids.length > 0) { - return 'A1' - } else { - return '' - } - }, [instruments, activeInstrument, hasTwoPipettes]) +const PANEL_STYLE_PROPS_BY_STEP: { + [string]: StyleProps, +} = { + [Sessions.CHECK_STEP_SESSION_STARTED]: terminalContentsStyleProps, + [Sessions.CHECK_STEP_LABWARE_LOADED]: darkContentsStyleProps, + [Sessions.CHECK_STEP_PREPARING_PIPETTE]: contentsStyleProps, + [Sessions.CHECK_STEP_COMPARING_HEIGHT]: contentsStyleProps, + [Sessions.CHECK_STEP_COMPARING_POINT_ONE]: contentsStyleProps, + [Sessions.CHECK_STEP_COMPARING_POINT_TWO]: contentsStyleProps, + [Sessions.CHECK_STEP_COMPARING_POINT_THREE]: terminalContentsStyleProps, +} - function deleteSession() { - robotCalCheckSession.id && - dispatchRequest( - Sessions.deleteSession(robotName, robotCalCheckSession.id) - ) - closeCalibrationCheck() - } +export function CheckHealthCalibration( + props: CalibrationHealthCheckParentProps +): React.Node { + const { session, robotName, dispatchRequests, showSpinner } = props + const { + currentStep, + activePipette, + activeTipRack, + instruments, + comparisonsByPipette, + } = session?.details || {} const { showConfirmation: showConfirmExit, confirm: confirmExit, cancel: cancelExit, - } = useConditionalConfirm( - () => sendCommand(Sessions.checkCommands.EXIT), - true - ) + } = useConditionalConfirm(() => { + cleanUpAndExit() + }, true) - function sendCommand( - command: SessionTypes.SessionCommandString, - data: SessionTypes.SessionCommandData = {} - ) { - robotCalCheckSession.id && - dispatchRequest( - Sessions.createSessionCommand(robotName, robotCalCheckSession.id, { - command, - data, - }) - ) - } - function jog(axis: JogAxis, direction: JogDirection, step: JogStep) { - robotCalCheckSession.id && - dispatch( - Sessions.createSessionCommand(robotName, robotCalCheckSession.id, { - command: Sessions.checkCommands.JOG, - data: { - vector: formatJogVector(axis, direction, step), - }, + const isMulti = React.useMemo(() => { + const spec = activePipette && getPipetteModelSpecs(activePipette.model) + return spec ? spec.channels > 1 : false + }, [activePipette]) + + function sendCommands(...commands: Array) { + if (session?.id) { + const sessionCommandActions = commands.map(c => + Sessions.createSessionCommand(robotName, session.id, { + command: c.command, + data: c.data || {}, }) ) + dispatchRequests(...sessionCommandActions) + } } - if (requestStatus === PENDING) { - return ( - - ) + function cleanUpAndExit() { + if (session?.id) { + dispatchRequests( + Sessions.createSessionCommand(robotName, session.id, { + command: Sessions.sharedCalCommands.EXIT, + data: {}, + }), + Sessions.deleteSession(robotName, session.id) + ) + } } - let stepContents - let modalContentsClassName = styles.modal_contents - let shouldDisplayTitleBarExit = true + const checkBothPipettes = instruments?.length === 2 - switch (currentStep) { - case Sessions.CHECK_STEP_SESSION_STARTED: { - stepContents = ( - sendCommand(Sessions.checkCommands.LOAD_LABWARE)} - labwareLoadNames={labware.map(l => l.loadName)} - /> - ) - break - } - case Sessions.CHECK_STEP_LABWARE_LOADED: { - stepContents = ( - sendCommand(Sessions.checkCommands.PREPARE_PIPETTE)} - labware={labware} - /> - ) - modalContentsClassName = styles.page_content_dark - break - } - case Sessions.CHECK_STEP_INSPECTING_FIRST_TIP: - case Sessions.CHECK_STEP_PREPARING_FIRST_PIPETTE: - case Sessions.CHECK_STEP_INSPECTING_SECOND_TIP: - case Sessions.CHECK_STEP_PREPARING_SECOND_PIPETTE: { - const isInspecting = [ - Sessions.CHECK_STEP_INSPECTING_FIRST_TIP, - Sessions.CHECK_STEP_INSPECTING_SECOND_TIP, - ].includes(currentStep) + if (!session || !activeTipRack) { + return null + } - stepContents = activeLabware ? ( - sendCommand(Sessions.checkCommands.PICK_UP_TIP)} - confirmTip={() => sendCommand(Sessions.checkCommands.CONFIRM_TIP)} - invalidateTip={() => - sendCommand(Sessions.checkCommands.INVALIDATE_TIP) - } - jog={jog} - /> - ) : null - break - } - case Sessions.CHECK_STEP_JOGGING_FIRST_PIPETTE_POINT_ONE: - case Sessions.CHECK_STEP_COMPARING_FIRST_PIPETTE_POINT_ONE: - case Sessions.CHECK_STEP_JOGGING_FIRST_PIPETTE_POINT_TWO: - case Sessions.CHECK_STEP_COMPARING_FIRST_PIPETTE_POINT_TWO: - case Sessions.CHECK_STEP_JOGGING_FIRST_PIPETTE_POINT_THREE: - case Sessions.CHECK_STEP_COMPARING_FIRST_PIPETTE_POINT_THREE: - case Sessions.CHECK_STEP_JOGGING_SECOND_PIPETTE_POINT_ONE: - case Sessions.CHECK_STEP_COMPARING_SECOND_PIPETTE_POINT_ONE: { - const slotNumber = getSlotNumberFromStep(currentStep) + const titleBarProps = { + title: ROBOT_CALIBRATION_CHECK_SUBTITLE, + back: { onClick: confirmExit, title: EXIT, children: EXIT }, + } - const isInspecting = [ - Sessions.CHECK_STEP_COMPARING_FIRST_PIPETTE_POINT_ONE, - Sessions.CHECK_STEP_COMPARING_FIRST_PIPETTE_POINT_TWO, - Sessions.CHECK_STEP_COMPARING_FIRST_PIPETTE_POINT_THREE, - Sessions.CHECK_STEP_COMPARING_SECOND_PIPETTE_POINT_ONE, - ].includes(currentStep) - const nextButtonText = getNextButtonTextForStep( - currentStep, - hasTwoPipettes - ) - const comparison = comparisonsByStep[currentStep] - if (comparison?.exceedsThreshold) { - shouldDisplayTitleBarExit = false - } - stepContents = ( - sendCommand(Sessions.checkCommands.COMPARE_POINT)} - goToNextCheck={() => - sendCommand(Sessions.checkCommands.GO_TO_NEXT_CHECK) - } - jog={jog} - /> - ) - break - } - case Sessions.CHECK_STEP_JOGGING_FIRST_PIPETTE_HEIGHT: - case Sessions.CHECK_STEP_COMPARING_FIRST_PIPETTE_HEIGHT: - case Sessions.CHECK_STEP_JOGGING_SECOND_PIPETTE_HEIGHT: - case Sessions.CHECK_STEP_COMPARING_SECOND_PIPETTE_HEIGHT: { - const isInspecting = [ - Sessions.CHECK_STEP_COMPARING_FIRST_PIPETTE_HEIGHT, - Sessions.CHECK_STEP_COMPARING_SECOND_PIPETTE_HEIGHT, - ].includes(currentStep) - const nextButtonText = getNextButtonTextForStep( - currentStep, - hasTwoPipettes - ) - const comparison = comparisonsByStep[currentStep] - if (comparison?.exceedsThreshold) { - shouldDisplayTitleBarExit = false - } - stepContents = ( - sendCommand(Sessions.checkCommands.COMPARE_POINT)} - goToNextCheck={() => - sendCommand(Sessions.checkCommands.GO_TO_NEXT_CHECK) - } - jog={jog} - /> - ) - break - } - case Sessions.CHECK_STEP_BAD_ROBOT_CALIBRATION: { - shouldDisplayTitleBarExit = false - stepContents = - break - } - case Sessions.CHECK_STEP_SESSION_EXITED: - case Sessions.CHECK_STEP_CHECK_COMPLETE: - case Sessions.CHECK_STEP_NO_PIPETTES_ATTACHED: { - stepContents = ( - - ) - modalContentsClassName = styles.terminal_modal_contents - shouldDisplayTitleBarExit = false - break - } - default: { - } + if (showSpinner) { + return } - return ( + const Panel = PANEL_BY_STEP[currentStep] + return Panel ? ( <> - {stepContents} + {showConfirmExit && ( - + )} > - ) -} - -// helpers - -const getNextButtonTextForStep = ( - step: SessionTypes.RobotCalibrationCheckStep, - hasTwoPipettes: boolean -): string => { - switch (step) { - case Sessions.CHECK_STEP_JOGGING_FIRST_PIPETTE_POINT_ONE: - case Sessions.CHECK_STEP_JOGGING_SECOND_PIPETTE_POINT_ONE: - case Sessions.CHECK_STEP_JOGGING_FIRST_PIPETTE_POINT_TWO: - case Sessions.CHECK_STEP_JOGGING_FIRST_PIPETTE_POINT_THREE: { - return CHECK_X_Y_AXES - } - case Sessions.CHECK_STEP_JOGGING_FIRST_PIPETTE_HEIGHT: - case Sessions.CHECK_STEP_JOGGING_SECOND_PIPETTE_HEIGHT: { - return CHECK_Z_AXIS - } - case Sessions.CHECK_STEP_COMPARING_FIRST_PIPETTE_POINT_ONE: - case Sessions.CHECK_STEP_COMPARING_FIRST_PIPETTE_POINT_TWO: - case Sessions.CHECK_STEP_COMPARING_FIRST_PIPETTE_HEIGHT: - case Sessions.CHECK_STEP_COMPARING_SECOND_PIPETTE_HEIGHT: { - return MOVE_TO_NEXT - } - case Sessions.CHECK_STEP_COMPARING_FIRST_PIPETTE_POINT_THREE: { - return hasTwoPipettes ? DROP_TIP_AND_DO_SECOND_PIPETTE : CONTINUE - } - case Sessions.CHECK_STEP_COMPARING_SECOND_PIPETTE_POINT_ONE: { - return CONTINUE - } - default: { - // should never reach this case, func only called when currentStep listed above - return '' - } - } -} - -const getSlotNumberFromStep = ( - step: SessionTypes.RobotCalibrationCheckStep -): string => { - switch (step) { - case Sessions.CHECK_STEP_JOGGING_FIRST_PIPETTE_POINT_ONE: - case Sessions.CHECK_STEP_COMPARING_FIRST_PIPETTE_POINT_ONE: - case Sessions.CHECK_STEP_JOGGING_SECOND_PIPETTE_POINT_ONE: - case Sessions.CHECK_STEP_COMPARING_SECOND_PIPETTE_POINT_ONE: { - return '1' - } - case Sessions.CHECK_STEP_JOGGING_FIRST_PIPETTE_POINT_TWO: - case Sessions.CHECK_STEP_COMPARING_FIRST_PIPETTE_POINT_TWO: { - return '3' - } - case Sessions.CHECK_STEP_JOGGING_FIRST_PIPETTE_POINT_THREE: - case Sessions.CHECK_STEP_COMPARING_FIRST_PIPETTE_POINT_THREE: { - return '7' - } - default: - // should never reach this case, func only called when currentStep listed above - return '' - } -} - -const getPipetteRankForStep = ( - step: SessionTypes.RobotCalibrationCheckStep -): SessionTypes.RobotCalibrationCheckPipetteRank | null => { - switch (step) { - case Sessions.CHECK_STEP_INSPECTING_FIRST_TIP: - case Sessions.CHECK_STEP_PREPARING_FIRST_PIPETTE: - case Sessions.CHECK_STEP_JOGGING_FIRST_PIPETTE_HEIGHT: - case Sessions.CHECK_STEP_COMPARING_FIRST_PIPETTE_HEIGHT: - case Sessions.CHECK_STEP_JOGGING_FIRST_PIPETTE_POINT_ONE: - case Sessions.CHECK_STEP_COMPARING_FIRST_PIPETTE_POINT_ONE: - case Sessions.CHECK_STEP_JOGGING_FIRST_PIPETTE_POINT_TWO: - case Sessions.CHECK_STEP_COMPARING_FIRST_PIPETTE_POINT_TWO: - case Sessions.CHECK_STEP_JOGGING_FIRST_PIPETTE_POINT_THREE: - case Sessions.CHECK_STEP_COMPARING_FIRST_PIPETTE_POINT_THREE: { - return Sessions.CHECK_PIPETTE_RANK_FIRST - } - case Sessions.CHECK_STEP_INSPECTING_SECOND_TIP: - case Sessions.CHECK_STEP_PREPARING_SECOND_PIPETTE: - case Sessions.CHECK_STEP_JOGGING_SECOND_PIPETTE_HEIGHT: - case Sessions.CHECK_STEP_COMPARING_SECOND_PIPETTE_HEIGHT: - case Sessions.CHECK_STEP_JOGGING_SECOND_PIPETTE_POINT_ONE: - case Sessions.CHECK_STEP_COMPARING_SECOND_PIPETTE_POINT_ONE: { - return Sessions.CHECK_PIPETTE_RANK_SECOND - } - default: - // should never reach this case, func only called when currentStep listed above - return null - } -} - -const buildTitleBarProps = ( - shouldDisplayTitleBarExit: boolean, - confirmExit: () => mixed -): TitleBarProps => { - return { - title: ROBOT_CALIBRATION_CHECK_SUBTITLE, - back: { - onClick: confirmExit, - title: EXIT, - children: EXIT, - className: !shouldDisplayTitleBarExit - ? styles.suppress_exit_button - : undefined, - }, - } + ) : null } diff --git a/app/src/components/CheckCalibration/tiprackImages.js b/app/src/components/CheckCalibration/tiprackImages.js deleted file mode 100644 index cdd035c4520..00000000000 --- a/app/src/components/CheckCalibration/tiprackImages.js +++ /dev/null @@ -1,20 +0,0 @@ -// @flow -// images by labware load name - -// TODO: BC 2020-04-01): this mapping should live in shared-data, -// it is now following the existing pattern in labware-library - -export const tiprackImages = { - opentrons_96_tiprack_1000ul: [ - require('../../assets/images/labware/opentrons_96_tiprack_1000ul_side_view.jpg'), - ], - opentrons_96_tiprack_10ul: [ - require('../../assets/images/labware/opentrons_96_tiprack_10ul_side_view.jpg'), - ], - opentrons_96_tiprack_20ul: [ - require('../../assets/images/labware/opentrons_96_tiprack_10ul_side_view.jpg'), - ], - opentrons_96_tiprack_300ul: [ - require('../../assets/images/labware/opentrons_96_tiprack_300ul_side_view.jpg'), - ], -} diff --git a/app/src/components/CheckCalibration/types.js b/app/src/components/CheckCalibration/types.js new file mode 100644 index 00000000000..120e9d4ffb7 --- /dev/null +++ b/app/src/components/CheckCalibration/types.js @@ -0,0 +1,27 @@ +// @flow +import type { Action } from '../../types' +import type { + SessionCommandParams, + CalibrationCheckSession, + CalibrationLabware, + RobotCalibrationCheckStep, +} from '../../sessions/types' + +export type CalibrationHealthCheckParentProps = {| + robotName: string, + session: CalibrationCheckSession | null, + dispatchRequests: ( + ...Array<{ ...Action, meta: { requestId: string } }> + ) => void, + showSpinner: boolean, + hasBlock?: boolean, +|} + +export type CalibrateHealthCheckChildProps = {| + sendSessionCommands: (...Array) => void, + deleteSession: () => void, + tipRackList: Array, + isMulti: boolean, + mount: string, + currentStep: RobotCalibrationCheckStep, +|} diff --git a/app/src/components/RobotSettings/CheckCalibrationControl.js b/app/src/components/RobotSettings/CheckCalibrationControl.js index 11b1796b1b5..53ef558bf00 100644 --- a/app/src/components/RobotSettings/CheckCalibrationControl.js +++ b/app/src/components/RobotSettings/CheckCalibrationControl.js @@ -1,33 +1,24 @@ // @flow import * as React from 'react' import { useSelector } from 'react-redux' -import last from 'lodash/last' import * as RobotApi from '../../robot-api' import * as Sessions from '../../sessions' import { Icon, - Flex, - Box, - Text, SecondaryBtn, - ALIGN_CENTER, - SIZE_2, - SPACING_1, - SPACING_2, - SPACING_3, BORDER_SOLID_LIGHT, - COLOR_WARNING, - FONT_SIZE_BODY_1, - FONT_WEIGHT_SEMIBOLD, Tooltip, useHoverTooltip, } from '@opentrons/components' import { Portal } from '../portal' -import { CheckCalibration } from '../CheckCalibration' +import { CheckHealthCalibration } from '../CheckCalibration' import { TitledControl } from '../TitledControl' +import type { SessionCommandString } from '../../sessions/types' +import type { RequestState } from '../../robot-api/types' + import type { State } from '../../types' export type CheckCalibrationControlProps = {| @@ -39,45 +30,106 @@ const CAL_HEALTH_CHECK = 'Calibration Health Check' const CHECK_HEALTH = 'check health' const CAL_HEALTH_CHECK_DESCRIPTION = 'Check the calibration settings for your robot.' -const COULD_NOT_START = 'Could not start Robot Calibration Check' -const PLEASE_TRY_AGAIN = - 'Please try again or contact support if you continue to experience issues' +// pipette calibration commands for which the full page spinner should not appear +const spinnerCommandBlockList: Array = [ + Sessions.sharedCalCommands.JOG, +] export function CheckCalibrationControl({ robotName, disabledReason, }: CheckCalibrationControlProps): React.Node { const [showWizard, setShowWizard] = React.useState(false) - const [dispatch, requestIds] = RobotApi.useDispatchApiRequest() const [targetProps, tooltipProps] = useHoverTooltip() - const requestState = useSelector((state: State) => { - const reqId = last(requestIds) ?? null - return RobotApi.getRequestById(state, reqId) - }) - const requestStatus = requestState?.status ?? null + const trackedRequestId = React.useRef(null) + const deleteRequestId = React.useRef(null) + const createRequestId = React.useRef(null) - const ensureSession = () => { - dispatch( - Sessions.ensureSession(robotName, Sessions.SESSION_TYPE_CALIBRATION_CHECK) - ) - } + const [dispatchRequests] = RobotApi.useDispatchApiRequests( + dispatchedAction => { + if (dispatchedAction.type === Sessions.ENSURE_SESSION) { + createRequestId.current = dispatchedAction.meta.requestId + } else if ( + dispatchedAction.type === Sessions.DELETE_SESSION && + checkHealthSession?.id === dispatchedAction.payload.sessionId + ) { + deleteRequestId.current = dispatchedAction.meta.requestId + } else if ( + dispatchedAction.type !== Sessions.CREATE_SESSION_COMMAND || + !spinnerCommandBlockList.includes( + dispatchedAction.payload.command.command + ) + ) { + trackedRequestId.current = dispatchedAction.meta.requestId + } + } + ) - const buttonDisabled = - Boolean(disabledReason) || requestStatus === RobotApi.PENDING + const showSpinner = + useSelector(state => + trackedRequestId.current + ? RobotApi.getRequestById(state, trackedRequestId.current) + : null + )?.status === RobotApi.PENDING + + const shouldClose = + useSelector(state => + deleteRequestId.current + ? RobotApi.getRequestById(state, deleteRequestId.current) + : null + )?.status === RobotApi.SUCCESS + + const shouldOpen = + useSelector((state: State) => + createRequestId.current + ? RobotApi.getRequestById(state, createRequestId.current) + : null + )?.status === RobotApi.SUCCESS React.useEffect(() => { - if (requestStatus === RobotApi.SUCCESS) setShowWizard(true) - }, [requestStatus]) - - const buttonChildren = - requestStatus !== RobotApi.PENDING ? ( - {CHECK_HEALTH} - ) : ( - + if (shouldOpen) { + setShowWizard(true) + createRequestId.current = null + } + if (shouldClose) { + setShowWizard(false) + deleteRequestId.current = null + } + }, [shouldOpen, shouldClose]) + + const handleStart = () => { + dispatchRequests( + Sessions.ensureSession( + robotName, + Sessions.SESSION_TYPE_CALIBRATION_HEALTH_CHECK + ) ) + } + + const checkHealthSession = useSelector((state: State) => { + const session: Sessions.Session | null = Sessions.getRobotSessionOfType( + state, + robotName, + Sessions.SESSION_TYPE_CALIBRATION_HEALTH_CHECK + ) + if ( + session && + session.sessionType === Sessions.SESSION_TYPE_CALIBRATION_HEALTH_CHECK + ) { + return session + } + return null + }) + + const buttonDisabled = Boolean(disabledReason) || showSpinner + + const buttonChildren = showSpinner ? ( + + ) : ( + CHECK_HEALTH + ) - // TODO(mc, 2020-06-17): extract alert presentational stuff return ( <> {buttonChildren} @@ -98,28 +150,14 @@ export function CheckCalibrationControl({ {disabledReason !== null && ( {disabledReason} )} - {requestState && requestState.status === RobotApi.FAILURE && ( - - - - - {COULD_NOT_START}:{' '} - {RobotApi.getErrorResponseMessage(requestState.error)} - - {PLEASE_TRY_AGAIN} - - - )} {showWizard && ( - setShowWizard(false)} + dispatchRequests={dispatchRequests} + showSpinner={showSpinner} /> )} diff --git a/app/src/components/RobotSettings/__tests__/CheckCalibrationControl.test.js b/app/src/components/RobotSettings/__tests__/CheckCalibrationControl.test.js index 3cb44a5fbc8..3162f3f44a1 100644 --- a/app/src/components/RobotSettings/__tests__/CheckCalibrationControl.test.js +++ b/app/src/components/RobotSettings/__tests__/CheckCalibrationControl.test.js @@ -1,53 +1,68 @@ // @flow import * as React from 'react' -import { Provider } from 'react-redux' -import { mount } from 'enzyme' -import { noop } from 'lodash' import * as Sessions from '../../../sessions' -import * as RobotApi from '../../../robot-api' -import { mockRobot } from '../../../robot-api/__fixtures__' -import { Icon, Tooltip, SecondaryBtn } from '@opentrons/components' -import { Portal } from '../../portal' +import { Tooltip, SecondaryBtn } from '@opentrons/components' +import { mountWithStore } from '@opentrons/components/__utils__' import { TitledControl } from '../../TitledControl' -import { CheckCalibration } from '../../CheckCalibration' import { CheckCalibrationControl } from '../CheckCalibrationControl' import type { State } from '../../../types' -import type { RequestState } from '../../../robot-api/types' + +import { mockCalibrationCheckSessionAttributes } from '../../../sessions/__fixtures__' jest.mock('../../../robot-api/selectors') +jest.mock('../../../sessions/selectors') jest.mock('../../CheckCalibration', () => ({ CheckCalibration: () => <>>, })) -const { name: robotName } = mockRobot -const MOCK_STATE: $Shape = {} +const getRobotSessionOfType: JestMockFn< + [State, string, Sessions.SessionType], + $Call< + typeof Sessions.getRobotSessionOfType, + State, + string, + Sessions.SessionType + > +> = Sessions.getRobotSessionOfType -const getRequestById: JestMockFn<[State, string], RequestState | null> = - RobotApi.getRequestById +const MOCK_STATE: State = ({ mockState: true }: any) describe('CheckCalibrationControl', () => { - const dispatch = jest.fn() - const render = props => { - return mount( - , - { - wrappingComponent: Provider, - wrappingComponentProps: { - store: { getState: () => MOCK_STATE, subscribe: noop, dispatch }, - }, - } + const getCalCheckButton = wrapper => + wrapper + .find('TitledControl[title="Calibration Health Check"]') + .find('button') + + const render = ( + props: $Shape> = {} + ) => { + const { robotName = 'robot-name', disabledReason = null } = props + return mountWithStore( + , + { initialState: MOCK_STATE } ) } + beforeEach(() => { + const mockCalibrationCheckSession: Sessions.CalibrationCheckSession = { + id: 'fake_check_session_id', + ...mockCalibrationCheckSessionAttributes, + } + getRobotSessionOfType.mockReturnValue(mockCalibrationCheckSession) + }) + afterEach(() => { jest.resetAllMocks() }) it('should render a TitledControl', () => { - const wrapper = render({ disabledReason: null }) + const { wrapper } = render({ disabledReason: null }) const titledButton = wrapper.find(TitledControl) const button = titledButton.find(SecondaryBtn) @@ -60,7 +75,7 @@ describe('CheckCalibrationControl', () => { }) it('should be able to disable the button', () => { - const wrapper = render({ disabledReason: 'oh no!' }) + const { wrapper } = render({ disabledReason: 'oh no!' }) const button = wrapper.find('button') const tooltip = wrapper.find(Tooltip) @@ -68,73 +83,17 @@ describe('CheckCalibrationControl', () => { expect(tooltip.prop('children')).toBe('oh no!') }) - it('should ensure a calibration check session exists on click', () => { - const wrapper = render({ disabledReason: null }) - - wrapper.find('button').invoke('onClick')() + it('button launches new check calibration health after confirm', () => { + const { wrapper, store } = render() + getCalCheckButton(wrapper).invoke('onClick')() + wrapper.update() - expect(dispatch).toHaveBeenCalledWith({ + expect(store.dispatch).toHaveBeenCalledWith({ ...Sessions.ensureSession( - robotName, - Sessions.SESSION_TYPE_CALIBRATION_CHECK + 'robot-name', + Sessions.SESSION_TYPE_CALIBRATION_HEALTH_CHECK ), - meta: { requestId: expect.any(String) }, - }) - }) - - it('should show a spinner in the button while request is pending', () => { - const wrapper = render({ disabledReason: null }) - wrapper.find('button').invoke('onClick')() - - const action = dispatch.mock.calls[0][0] - const requestId = action.meta.requestId - - getRequestById.mockImplementation((state, reqId) => { - expect(state).toBe(MOCK_STATE) - expect(reqId).toBe(requestId) - return { status: RobotApi.PENDING } + meta: expect.objectContaining({ requestId: expect.any(String) }), }) - - wrapper.setProps({}) - - const button = wrapper.find('button') - const spinner = button.find(Icon) - - expect(button.prop('disabled')).toBe(true) - expect(spinner.prop('name')).toBe('ot-spinner') - expect(spinner.prop('spin')).toBe(true) - }) - - it('should show a CheckCalbration wizard in a Portal when request succeeds', () => { - const wrapper = render({ disabledReason: null }) - - wrapper.find('button').invoke('onClick')() - getRequestById.mockReturnValue(({ status: RobotApi.SUCCESS }: any)) - wrapper.setProps({}) - wrapper.update() - - const wizard = wrapper.find(Portal).find(CheckCalibration) - expect(wizard.prop('robotName')).toBe(robotName) - - wrapper.find(CheckCalibration).invoke('closeCalibrationCheck')() - expect(wrapper.exists(CheckCalibration)).toBe(false) - }) - - it('should show a warning message if the request fails', () => { - const wrapper = render({ disabledReason: null }) - - wrapper.find('button').invoke('onClick')() - getRequestById.mockReturnValue({ - status: RobotApi.FAILURE, - response: { ok: false, method: 'GET', path: '/sessions', status: 500 }, - error: { errors: [{ detail: 'oh no!' }] }, - }) - wrapper.setProps({}) - wrapper.update() - - expect(wrapper.exists(CheckCalibration)).toBe(false) - expect(wrapper.exists('Icon[name="alert-circle"]')).toBe(true) - expect(wrapper.html()).toMatch(/could not start robot calibration check/i) - expect(wrapper.html()).toContain('oh no!') }) }) diff --git a/app/src/sessions/__fixtures__/calibration-check.js b/app/src/sessions/__fixtures__/calibration-check.js index 4671fb926b0..a0468ffda15 100644 --- a/app/src/sessions/__fixtures__/calibration-check.js +++ b/app/src/sessions/__fixtures__/calibration-check.js @@ -1,95 +1,96 @@ // @flow import { - CHECK_STEP_COMPARING_FIRST_PIPETTE_HEIGHT, - CHECK_STEP_COMPARING_FIRST_PIPETTE_POINT_ONE, - CHECK_STEP_COMPARING_FIRST_PIPETTE_POINT_TWO, - CHECK_STEP_COMPARING_FIRST_PIPETTE_POINT_THREE, - CHECK_STEP_COMPARING_SECOND_PIPETTE_HEIGHT, - CHECK_STEP_COMPARING_SECOND_PIPETTE_POINT_ONE, + CHECK_STEP_COMPARING_HEIGHT, + CHECK_STEP_COMPARING_POINT_ONE, + CHECK_STEP_COMPARING_POINT_TWO, + CHECK_STEP_COMPARING_POINT_THREE, CHECK_TRANSFORM_TYPE_UNKNOWN, } from '../constants' import type { - RobotCalibrationCheckSessionDetails, - RobotCalibrationCheckComparison, + CheckCalibrationHealthSessionDetails, + CalibrationHealthCheckComparison, + CalibrationLabware, } from '../types' -export const badZComparison: RobotCalibrationCheckComparison = { +import tipRackFixture from '@opentrons/shared-data/labware/fixtures/2/fixture_tiprack_300_ul' + +export const mockCalibrationCheckLabware: CalibrationLabware = { + slot: '8', + loadName: 'opentrons_96_tiprack_300ul', + namespace: 'opentrons', + version: 1, + isTiprack: true, + definition: tipRackFixture, +} + +export const badZComparison: CalibrationHealthCheckComparison = { differenceVector: [0, 0, 4], thresholdVector: [0, 0, 1], exceedsThreshold: true, transformType: CHECK_TRANSFORM_TYPE_UNKNOWN, } -export const goodZComparison: RobotCalibrationCheckComparison = { +export const goodZComparison: CalibrationHealthCheckComparison = { differenceVector: [0, 0, 0.1], thresholdVector: [0, 0, 1], exceedsThreshold: false, transformType: CHECK_TRANSFORM_TYPE_UNKNOWN, } -export const badXYComparison: RobotCalibrationCheckComparison = { +export const badXYComparison: CalibrationHealthCheckComparison = { differenceVector: [4, 4, 0], thresholdVector: [1, 1, 0], exceedsThreshold: true, transformType: CHECK_TRANSFORM_TYPE_UNKNOWN, } -export const goodXYComparison: RobotCalibrationCheckComparison = { +export const goodXYComparison: CalibrationHealthCheckComparison = { differenceVector: [0.1, 0.1, 0], thresholdVector: [1, 1, 0], exceedsThreshold: false, transformType: CHECK_TRANSFORM_TYPE_UNKNOWN, } -export const mockRobotCalibrationCheckSessionDetails: RobotCalibrationCheckSessionDetails = { - instruments: { - left: { +export const mockRobotCalibrationCheckSessionDetails: CheckCalibrationHealthSessionDetails = { + instruments: [ + { model: 'fake_pipette_model', name: 'fake_pipette_name', tip_length: 42, mount: 'left', - tiprack_id: 'abc123_labware_uuid', - rank: 'second', + rank: 'first', serial: 'fake pipette serial 1', }, - right: { + { model: 'fake_pipette_model', name: 'fake_pipette_name', tip_length: 42, mount: 'right', - tiprack_id: 'def456_labware_uuid', - rank: 'first', + rank: 'second', serial: 'fake pipette serial 2', }, - }, + ], currentStep: 'sessionStarted', - nextSteps: { - links: { labwareLoaded: '/fake/route' }, - }, - comparisonsByStep: { - [CHECK_STEP_COMPARING_FIRST_PIPETTE_HEIGHT]: goodZComparison, - [CHECK_STEP_COMPARING_FIRST_PIPETTE_POINT_ONE]: goodXYComparison, - [CHECK_STEP_COMPARING_FIRST_PIPETTE_POINT_TWO]: goodXYComparison, - [CHECK_STEP_COMPARING_FIRST_PIPETTE_POINT_THREE]: goodXYComparison, - [CHECK_STEP_COMPARING_SECOND_PIPETTE_HEIGHT]: goodZComparison, - [CHECK_STEP_COMPARING_SECOND_PIPETTE_POINT_ONE]: goodXYComparison, - }, - labware: [ - { - alternatives: ['fake_tiprack_load_name'], - slot: '8', - id: 'abc123_labware_uuid', - forMounts: ['left'], - loadName: 'opentrons_96_tiprack_300ul', - namespace: 'opentrons', - version: 1, + comparisonsByPipette: { + first: { + [CHECK_STEP_COMPARING_HEIGHT]: goodZComparison, + [CHECK_STEP_COMPARING_POINT_ONE]: goodXYComparison, + [CHECK_STEP_COMPARING_POINT_TWO]: goodXYComparison, + [CHECK_STEP_COMPARING_POINT_THREE]: goodXYComparison, }, - { - alternatives: ['fake_other_tiprack_load_name'], - slot: '6', - id: 'def456_labware_uuid', - forMounts: ['right'], - loadName: 'opentrons_96_tiprack_20ul', - namespace: 'opentrons', - version: 1, + second: { + [CHECK_STEP_COMPARING_HEIGHT]: goodZComparison, + [CHECK_STEP_COMPARING_POINT_ONE]: goodXYComparison, + [CHECK_STEP_COMPARING_POINT_TWO]: goodXYComparison, + [CHECK_STEP_COMPARING_POINT_THREE]: goodXYComparison, }, - ], + }, + labware: [mockCalibrationCheckLabware], + activePipette: { + model: 'fake_pipette_model', + name: 'fake_pipette_name', + tip_length: 42, + mount: 'left', + rank: 'first', + serial: 'fake pipette serial 1', + }, + activeTipRack: mockCalibrationCheckLabware, } diff --git a/app/src/sessions/__fixtures__/index.js b/app/src/sessions/__fixtures__/index.js index 1f0ed62dd2c..b29a490fa3b 100644 --- a/app/src/sessions/__fixtures__/index.js +++ b/app/src/sessions/__fixtures__/index.js @@ -30,7 +30,7 @@ export const mockSessionId: string = 'fake_session_id' export const mockOtherSessionId: string = 'other_fake_session_id' export const mockCalibrationCheckSessionAttributes: Types.CalibrationCheckSessionResponseAttributes = { - sessionType: Constants.SESSION_TYPE_CALIBRATION_CHECK, + sessionType: Constants.SESSION_TYPE_CALIBRATION_HEALTH_CHECK, createParams: {}, details: mockRobotCalibrationCheckSessionDetails, } @@ -64,7 +64,7 @@ export const mockSessionCommand: Types.SessionCommandAttributes = { } export const mockSessionCommandAttributes: Types.SessionCommandAttributes = { - command: 'calibration.check.preparePipette', + command: 'calibration.check.comparePoint', status: 'accepted', data: {}, } @@ -259,128 +259,3 @@ export const { failureStatus: 500, failureBody: mockV2ErrorResponse, }) - -export const mockCalibrationCheckSessionAnalyticsProps = { - sessionType: Constants.SESSION_TYPE_CALIBRATION_CHECK, - leftPipetteModel: - mockRobotCalibrationCheckSessionDetails.instruments.left.model, - rightPipetteModel: - mockRobotCalibrationCheckSessionDetails.instruments.right.model, - comparingFirstPipetteHeightDifferenceVector: - mockRobotCalibrationCheckSessionDetails.comparisonsByStep - .comparingFirstPipetteHeight.differenceVector, - comparingFirstPipetteHeightThresholdVector: - mockRobotCalibrationCheckSessionDetails.comparisonsByStep - .comparingFirstPipetteHeight.thresholdVector, - comparingFirstPipetteHeightExceedsThreshold: - mockRobotCalibrationCheckSessionDetails.comparisonsByStep - .comparingFirstPipetteHeight.exceedsThreshold, - comparingFirstPipetteHeightErrorSource: - mockRobotCalibrationCheckSessionDetails.comparisonsByStep - .comparingFirstPipetteHeight.transformType, - comparingFirstPipettePointOneDifferenceVector: - mockRobotCalibrationCheckSessionDetails.comparisonsByStep - .comparingFirstPipettePointOne.differenceVector, - comparingFirstPipettePointOneThresholdVector: - mockRobotCalibrationCheckSessionDetails.comparisonsByStep - .comparingFirstPipettePointOne.thresholdVector, - comparingFirstPipettePointOneExceedsThreshold: - mockRobotCalibrationCheckSessionDetails.comparisonsByStep - .comparingFirstPipettePointOne.exceedsThreshold, - comparingFirstPipettePointOneErrorSource: - mockRobotCalibrationCheckSessionDetails.comparisonsByStep - .comparingFirstPipettePointOne.transformType, - comparingFirstPipettePointTwoDifferenceVector: - mockRobotCalibrationCheckSessionDetails.comparisonsByStep - .comparingFirstPipettePointTwo.differenceVector, - comparingFirstPipettePointTwoThresholdVector: - mockRobotCalibrationCheckSessionDetails.comparisonsByStep - .comparingFirstPipettePointTwo.thresholdVector, - comparingFirstPipettePointTwoExceedsThreshold: - mockRobotCalibrationCheckSessionDetails.comparisonsByStep - .comparingFirstPipettePointTwo.exceedsThreshold, - comparingFirstPipettePointTwoErrorSource: - mockRobotCalibrationCheckSessionDetails.comparisonsByStep - .comparingFirstPipettePointTwo.transformType, - comparingFirstPipettePointThreeDifferenceVector: - mockRobotCalibrationCheckSessionDetails.comparisonsByStep - .comparingFirstPipettePointThree.differenceVector, - comparingFirstPipettePointThreeThresholdVector: - mockRobotCalibrationCheckSessionDetails.comparisonsByStep - .comparingFirstPipettePointThree.thresholdVector, - comparingFirstPipettePointThreeExceedsThreshold: - mockRobotCalibrationCheckSessionDetails.comparisonsByStep - .comparingFirstPipettePointThree.exceedsThreshold, - comparingFirstPipettePointThreeErrorSource: - mockRobotCalibrationCheckSessionDetails.comparisonsByStep - .comparingFirstPipettePointThree.transformType, - comparingSecondPipetteHeightDifferenceVector: - mockRobotCalibrationCheckSessionDetails.comparisonsByStep - .comparingSecondPipetteHeight.differenceVector, - comparingSecondPipetteHeightThresholdVector: - mockRobotCalibrationCheckSessionDetails.comparisonsByStep - .comparingSecondPipetteHeight.thresholdVector, - comparingSecondPipetteHeightExceedsThreshold: - mockRobotCalibrationCheckSessionDetails.comparisonsByStep - .comparingSecondPipetteHeight.exceedsThreshold, - comparingSecondPipetteHeightErrorSource: - mockRobotCalibrationCheckSessionDetails.comparisonsByStep - .comparingSecondPipetteHeight.transformType, - comparingSecondPipettePointOneDifferenceVector: - mockRobotCalibrationCheckSessionDetails.comparisonsByStep - .comparingSecondPipettePointOne.differenceVector, - comparingSecondPipettePointOneThresholdVector: - mockRobotCalibrationCheckSessionDetails.comparisonsByStep - .comparingSecondPipettePointOne.thresholdVector, - comparingSecondPipettePointOneExceedsThreshold: - mockRobotCalibrationCheckSessionDetails.comparisonsByStep - .comparingSecondPipettePointOne.exceedsThreshold, - comparingSecondPipettePointOneErrorSource: - mockRobotCalibrationCheckSessionDetails.comparisonsByStep - .comparingSecondPipettePointOne.transformType, -} - -export const mockCalibrationCheckSessionIntercomProps = { - sessionType: Constants.SESSION_TYPE_CALIBRATION_CHECK, - leftPipetteModel: - mockRobotCalibrationCheckSessionDetails.instruments.left.model, - rightPipetteModel: - mockRobotCalibrationCheckSessionDetails.instruments.right.model, - succeeded: true, - comparingFirstPipetteHeightExceedsThreshold: - mockRobotCalibrationCheckSessionDetails.comparisonsByStep - .comparingFirstPipetteHeight.exceedsThreshold, - comparingFirstPipetteHeightErrorSource: - mockRobotCalibrationCheckSessionDetails.comparisonsByStep - .comparingFirstPipetteHeight.transformType, - comparingFirstPipettePointOneExceedsThreshold: - mockRobotCalibrationCheckSessionDetails.comparisonsByStep - .comparingFirstPipettePointOne.exceedsThreshold, - comparingFirstPipettePointOneErrorSource: - mockRobotCalibrationCheckSessionDetails.comparisonsByStep - .comparingFirstPipettePointOne.transformType, - comparingFirstPipettePointTwoExceedsThreshold: - mockRobotCalibrationCheckSessionDetails.comparisonsByStep - .comparingFirstPipettePointTwo.exceedsThreshold, - comparingFirstPipettePointTwoErrorSource: - mockRobotCalibrationCheckSessionDetails.comparisonsByStep - .comparingFirstPipettePointTwo.transformType, - comparingFirstPipettePointThreeExceedsThreshold: - mockRobotCalibrationCheckSessionDetails.comparisonsByStep - .comparingFirstPipettePointThree.exceedsThreshold, - comparingFirstPipettePointThreeErrorSource: - mockRobotCalibrationCheckSessionDetails.comparisonsByStep - .comparingFirstPipettePointThree.transformType, - comparingSecondPipetteHeightExceedsThreshold: - mockRobotCalibrationCheckSessionDetails.comparisonsByStep - .comparingSecondPipetteHeight.exceedsThreshold, - comparingSecondPipetteHeightErrorSource: - mockRobotCalibrationCheckSessionDetails.comparisonsByStep - .comparingSecondPipetteHeight.transformType, - comparingSecondPipettePointOneExceedsThreshold: - mockRobotCalibrationCheckSessionDetails.comparisonsByStep - .comparingSecondPipettePointOne.exceedsThreshold, - comparingSecondPipettePointOneErrorSource: - mockRobotCalibrationCheckSessionDetails.comparisonsByStep - .comparingSecondPipettePointOne.transformType, -} diff --git a/app/src/sessions/__tests__/selectors.test.js b/app/src/sessions/__tests__/selectors.test.js deleted file mode 100644 index 1ea4884b2b3..00000000000 --- a/app/src/sessions/__tests__/selectors.test.js +++ /dev/null @@ -1,199 +0,0 @@ -// @flow -import noop from 'lodash/noop' -import * as Fixtures from '../__fixtures__' -import * as Selectors from '../selectors' - -import type { State } from '../../types' - -jest.mock('../../robot/selectors') - -type SelectorSpec = {| - name: string, - selector: (State, ...Array) => mixed, - state: $Shape, - args?: Array, - before?: () => mixed, - expected: mixed, -|} - -const SPECS: Array = [ - { - name: 'getRobotSessions returns null if no sessions', - selector: Selectors.getRobotSessions, - state: { - sessions: {}, - }, - args: ['germanium-cobweb'], - expected: null, - }, - { - name: 'getRobotSessions returns session map', - selector: Selectors.getRobotSessions, - state: { - sessions: { - 'germanium-cobweb': { - robotSessions: { - [Fixtures.mockSessionId]: - Fixtures.mockCalibrationCheckSessionAttributes, - }, - }, - }, - }, - args: ['germanium-cobweb'], - expected: { - [Fixtures.mockSessionId]: Fixtures.mockCalibrationCheckSessionAttributes, - }, - }, - { - name: 'getRobotSessionById returns null if not found', - selector: Selectors.getRobotSessionById, - state: { - sessions: { - 'germanium-cobweb': { - robotSessions: { - [Fixtures.mockSessionId]: - Fixtures.mockCalibrationCheckSessionAttributes, - }, - }, - }, - }, - args: ['germanium-cobweb', 'non_existent_session_id'], - expected: null, - }, - { - name: 'getRobotSessionById returns found session', - selector: Selectors.getRobotSessionById, - state: { - sessions: { - 'germanium-cobweb': { - robotSessions: { - [Fixtures.mockSessionId]: - Fixtures.mockCalibrationCheckSessionAttributes, - }, - }, - }, - }, - args: ['germanium-cobweb', Fixtures.mockSessionId], - expected: Fixtures.mockCalibrationCheckSessionAttributes, - }, - { - name: - 'getAnalyticsPropsForRobotSessionById returns props for check cal session', - selector: Selectors.getAnalyticsPropsForRobotSessionById, - state: { - sessions: { - 'germanium-cobweb': { - robotSessions: { - [Fixtures.mockSessionId]: - Fixtures.mockCalibrationCheckSessionAttributes, - }, - }, - }, - }, - args: ['germanium-cobweb', Fixtures.mockSessionId], - expected: Fixtures.mockCalibrationCheckSessionAnalyticsProps, - }, - { - name: - 'getAnalyticsPropsForRobotSessionById returns null for untracked session type', - selector: Selectors.getAnalyticsPropsForRobotSessionById, - state: { - sessions: { - 'germanium-cobweb': { - robotSessions: { - [Fixtures.mockSessionId]: { - ...Fixtures.mockCalibrationCheckSessionAttributes, - sessionType: 'FakeUntrackedSessionType', - }, - }, - }, - }, - }, - args: ['germanium-cobweb', Fixtures.mockSessionId], - expected: null, - }, - { - name: - 'getAnalyticsPropsForRobotSessionById returns null if session not found', - selector: Selectors.getAnalyticsPropsForRobotSessionById, - state: { - sessions: { - 'germanium-cobweb': { - robotSessions: { - [Fixtures.mockSessionId]: { - ...Fixtures.mockCalibrationCheckSessionAttributes, - sessionType: 'FakeUntrackedSessionType', - }, - }, - }, - }, - }, - args: ['germanium-cobweb', 'fake_nonexistent_session_id'], - expected: null, - }, - { - name: - 'getIntercomEventPropsForRobotSessionById returns props for check cal session', - selector: Selectors.getIntercomEventPropsForRobotSessionById, - state: { - sessions: { - 'germanium-cobweb': { - robotSessions: { - [Fixtures.mockSessionId]: - Fixtures.mockCalibrationCheckSessionAttributes, - }, - }, - }, - }, - args: ['germanium-cobweb', Fixtures.mockSessionId], - expected: Fixtures.mockCalibrationCheckSessionIntercomProps, - }, - { - name: - 'getIntercomEventPropsForRobotSessionById returns null for untracked session type', - selector: Selectors.getIntercomEventPropsForRobotSessionById, - state: { - sessions: { - 'germanium-cobweb': { - robotSessions: { - [Fixtures.mockSessionId]: { - ...Fixtures.mockCalibrationCheckSessionAttributes, - sessionType: 'FakeUntrackedSessionType', - }, - }, - }, - }, - }, - args: ['germanium-cobweb', Fixtures.mockSessionId], - expected: null, - }, - { - name: - 'getIntercomEventPropsForRobotSessionById returns null if session not found', - selector: Selectors.getIntercomEventPropsForRobotSessionById, - state: { - sessions: { - 'germanium-cobweb': { - robotSessions: { - [Fixtures.mockSessionId]: { - ...Fixtures.mockCalibrationCheckSessionAttributes, - sessionType: 'FakeUntrackedSessionType', - }, - }, - }, - }, - }, - args: ['germanium-cobweb', 'fake_nonexistent_session_id'], - expected: null, - }, -] - -describe('sessions selectors', () => { - SPECS.forEach(spec => { - const { name, selector, state, args = [], before = noop, expected } = spec - it(name, () => { - before() - expect(selector(state, ...args)).toEqual(expected) - }) - }) -}) diff --git a/app/src/sessions/calibration-check/constants.js b/app/src/sessions/calibration-check/constants.js index 025c63d7191..af8dc70cab6 100644 --- a/app/src/sessions/calibration-check/constants.js +++ b/app/src/sessions/calibration-check/constants.js @@ -5,58 +5,41 @@ import { sharedCalCommands } from '../common-calibration/constants' export const CHECK_STEP_SESSION_STARTED: 'sessionStarted' = 'sessionStarted' export const CHECK_STEP_LABWARE_LOADED: 'labwareLoaded' = 'labwareLoaded' -export const CHECK_STEP_PREPARING_FIRST_PIPETTE: 'preparingFirstPipette' = - 'preparingFirstPipette' -export const CHECK_STEP_INSPECTING_FIRST_TIP: 'inspectingFirstTip' = - 'inspectingFirstTip' -export const CHECK_STEP_JOGGING_FIRST_PIPETTE_HEIGHT: 'joggingFirstPipetteToHeight' = - 'joggingFirstPipetteToHeight' -export const CHECK_STEP_COMPARING_FIRST_PIPETTE_HEIGHT: 'comparingFirstPipetteHeight' = - 'comparingFirstPipetteHeight' -export const CHECK_STEP_JOGGING_FIRST_PIPETTE_POINT_ONE: 'joggingFirstPipetteToPointOne' = - 'joggingFirstPipetteToPointOne' -export const CHECK_STEP_COMPARING_FIRST_PIPETTE_POINT_ONE: 'comparingFirstPipettePointOne' = - 'comparingFirstPipettePointOne' -export const CHECK_STEP_JOGGING_FIRST_PIPETTE_POINT_TWO: 'joggingFirstPipetteToPointTwo' = - 'joggingFirstPipetteToPointTwo' -export const CHECK_STEP_COMPARING_FIRST_PIPETTE_POINT_TWO: 'comparingFirstPipettePointTwo' = - 'comparingFirstPipettePointTwo' -export const CHECK_STEP_JOGGING_FIRST_PIPETTE_POINT_THREE: 'joggingFirstPipetteToPointThree' = - 'joggingFirstPipetteToPointThree' -export const CHECK_STEP_COMPARING_FIRST_PIPETTE_POINT_THREE: 'comparingFirstPipettePointThree' = - 'comparingFirstPipettePointThree' -export const CHECK_STEP_PREPARING_SECOND_PIPETTE: 'preparingSecondPipette' = - 'preparingSecondPipette' -export const CHECK_STEP_INSPECTING_SECOND_TIP: 'inspectingSecondTip' = - 'inspectingSecondTip' -export const CHECK_STEP_JOGGING_SECOND_PIPETTE_HEIGHT: 'joggingSecondPipetteToHeight' = - 'joggingSecondPipetteToHeight' -export const CHECK_STEP_COMPARING_SECOND_PIPETTE_HEIGHT: 'comparingSecondPipetteHeight' = - 'comparingSecondPipetteHeight' -export const CHECK_STEP_JOGGING_SECOND_PIPETTE_POINT_ONE: 'joggingSecondPipetteToPointOne' = - 'joggingSecondPipetteToPointOne' -export const CHECK_STEP_COMPARING_SECOND_PIPETTE_POINT_ONE: 'comparingSecondPipettePointOne' = - 'comparingSecondPipettePointOne' -export const CHECK_STEP_CHECK_COMPLETE: 'checkComplete' = 'checkComplete' +export const CHECK_STEP_PREPARING_PIPETTE: 'preparingPipette' = + 'preparingPipette' +export const CHECK_STEP_INSPECTING_TIP: 'inspectingTip' = 'inspectingTip' +export const CHECK_STEP_COMPARING_HEIGHT: 'comparingHeight' = 'comparingHeight' +export const CHECK_STEP_COMPARING_POINT_ONE: 'comparingPointOne' = + 'comparingPointOne' +export const CHECK_STEP_COMPARING_POINT_TWO: 'comparingPointTwo' = + 'comparingPointTwo' +export const CHECK_STEP_COMPARING_POINT_THREE: 'comparingPointThree' = + 'comparingPointThree' +export const CHECK_STEP_CHECK_COMPLETE: 'calibrationComplete' = + 'calibrationComplete' export const CHECK_STEP_SESSION_EXITED: 'sessionExited' = 'sessionExited' export const CHECK_STEP_BAD_ROBOT_CALIBRATION: 'badCalibrationData' = 'badCalibrationData' +export const CHECK_STEP_RETURNING_TIP: 'returningTip' = 'returningTip' +export const CHECK_STEP_RESULTS_SUMMARY: 'resultsSummary' = 'resultsSummary' export const CHECK_STEP_NO_PIPETTES_ATTACHED: 'noPipettesAttached' = 'noPipettesAttached' -const PREPARE_PIPETTE: 'calibration.check.preparePipette' = - 'calibration.check.preparePipette' +const CHECK_SWITCH_PIPETTE: 'calibration.check.switchPipette' = + 'calibration.check.switchPipette' const COMPARE_POINT: 'calibration.check.comparePoint' = 'calibration.check.comparePoint' -const GO_TO_NEXT_CHECK: 'calibration.check.goToNextCheck' = - 'calibration.check.goToNextCheck' +const RETURN_TIP: 'calibration.check.returnTip' = 'calibration.check.returnTip' +const TRANSITION: 'calibration.check.transition' = + 'calibration.check.transition' const EXIT: 'calibration.exitSession' = 'calibration.exitSession' export const checkCommands = { ...sharedCalCommands, - PREPARE_PIPETTE, + CHECK_SWITCH_PIPETTE, COMPARE_POINT, - GO_TO_NEXT_CHECK, + TRANSITION, + RETURN_TIP, EXIT, } @@ -68,13 +51,13 @@ export const CHECK_PIPETTE_RANK_FIRST: 'first' = 'first' export const CHECK_PIPETTE_RANK_SECOND: 'second' = 'second' export const FIRST_PIPETTE_COMPARISON_STEPS = [ - CHECK_STEP_COMPARING_FIRST_PIPETTE_HEIGHT, - CHECK_STEP_COMPARING_FIRST_PIPETTE_POINT_ONE, - CHECK_STEP_COMPARING_FIRST_PIPETTE_POINT_TWO, - CHECK_STEP_COMPARING_FIRST_PIPETTE_POINT_THREE, + CHECK_STEP_COMPARING_HEIGHT, + CHECK_STEP_COMPARING_POINT_ONE, + CHECK_STEP_COMPARING_POINT_TWO, + CHECK_STEP_COMPARING_POINT_THREE, ] export const SECOND_PIPETTE_COMPARISON_STEPS = [ - CHECK_STEP_COMPARING_SECOND_PIPETTE_HEIGHT, - CHECK_STEP_COMPARING_SECOND_PIPETTE_POINT_ONE, + CHECK_STEP_COMPARING_HEIGHT, + CHECK_STEP_COMPARING_POINT_ONE, ] diff --git a/app/src/sessions/calibration-check/selectors.js b/app/src/sessions/calibration-check/selectors.js index 76581a9201a..f4f59a87b15 100644 --- a/app/src/sessions/calibration-check/selectors.js +++ b/app/src/sessions/calibration-check/selectors.js @@ -1,6 +1,6 @@ // @flow import type { State } from '../../types' -import { SESSION_TYPE_CALIBRATION_CHECK } from '../constants' +import { SESSION_TYPE_CALIBRATION_HEALTH_CHECK } from '../constants' import type { Session, CalibrationCheckSession } from '../types' import { getRobotSessionOfType } from '../selectors' @@ -11,11 +11,11 @@ export const getCalibrationCheckSession: ( const calCheckSession: Session | null = getRobotSessionOfType( state, robotName, - SESSION_TYPE_CALIBRATION_CHECK + SESSION_TYPE_CALIBRATION_HEALTH_CHECK ) if ( calCheckSession && - calCheckSession.sessionType === SESSION_TYPE_CALIBRATION_CHECK + calCheckSession.sessionType === SESSION_TYPE_CALIBRATION_HEALTH_CHECK ) { return calCheckSession } diff --git a/app/src/sessions/calibration-check/types.js b/app/src/sessions/calibration-check/types.js index 3176d15f07e..1dcca91cd95 100644 --- a/app/src/sessions/calibration-check/types.js +++ b/app/src/sessions/calibration-check/types.js @@ -1,29 +1,22 @@ // @flow +import type { CalibrationLabware } from '../types' + // calibration check session types import typeof { CHECK_STEP_SESSION_STARTED, CHECK_STEP_LABWARE_LOADED, - CHECK_STEP_PREPARING_FIRST_PIPETTE, - CHECK_STEP_INSPECTING_FIRST_TIP, - CHECK_STEP_JOGGING_FIRST_PIPETTE_HEIGHT, - CHECK_STEP_COMPARING_FIRST_PIPETTE_HEIGHT, - CHECK_STEP_JOGGING_FIRST_PIPETTE_POINT_ONE, - CHECK_STEP_COMPARING_FIRST_PIPETTE_POINT_ONE, - CHECK_STEP_JOGGING_FIRST_PIPETTE_POINT_TWO, - CHECK_STEP_COMPARING_FIRST_PIPETTE_POINT_TWO, - CHECK_STEP_JOGGING_FIRST_PIPETTE_POINT_THREE, - CHECK_STEP_COMPARING_FIRST_PIPETTE_POINT_THREE, - CHECK_STEP_PREPARING_SECOND_PIPETTE, - CHECK_STEP_INSPECTING_SECOND_TIP, - CHECK_STEP_JOGGING_SECOND_PIPETTE_HEIGHT, - CHECK_STEP_COMPARING_SECOND_PIPETTE_HEIGHT, - CHECK_STEP_JOGGING_SECOND_PIPETTE_POINT_ONE, - CHECK_STEP_COMPARING_SECOND_PIPETTE_POINT_ONE, + CHECK_STEP_PREPARING_PIPETTE, + CHECK_STEP_INSPECTING_TIP, + CHECK_STEP_COMPARING_HEIGHT, + CHECK_STEP_COMPARING_POINT_ONE, + CHECK_STEP_COMPARING_POINT_TWO, + CHECK_STEP_COMPARING_POINT_THREE, + CHECK_STEP_RETURNING_TIP, + CHECK_STEP_RESULTS_SUMMARY, CHECK_STEP_SESSION_EXITED, CHECK_STEP_CHECK_COMPLETE, CHECK_STEP_BAD_ROBOT_CALIBRATION, - CHECK_STEP_NO_PIPETTES_ATTACHED, CHECK_TRANSFORM_TYPE_INSTRUMENT_OFFSET, CHECK_TRANSFORM_TYPE_UNKNOWN, CHECK_TRANSFORM_TYPE_DECK, @@ -36,79 +29,58 @@ import typeof { export type RobotCalibrationCheckStep = | CHECK_STEP_SESSION_STARTED | CHECK_STEP_LABWARE_LOADED - | CHECK_STEP_PREPARING_FIRST_PIPETTE - | CHECK_STEP_INSPECTING_FIRST_TIP - | CHECK_STEP_JOGGING_FIRST_PIPETTE_HEIGHT - | CHECK_STEP_COMPARING_FIRST_PIPETTE_HEIGHT - | CHECK_STEP_JOGGING_FIRST_PIPETTE_POINT_ONE - | CHECK_STEP_COMPARING_FIRST_PIPETTE_POINT_ONE - | CHECK_STEP_JOGGING_FIRST_PIPETTE_POINT_TWO - | CHECK_STEP_COMPARING_FIRST_PIPETTE_POINT_TWO - | CHECK_STEP_JOGGING_FIRST_PIPETTE_POINT_THREE - | CHECK_STEP_COMPARING_FIRST_PIPETTE_POINT_THREE - | CHECK_STEP_PREPARING_SECOND_PIPETTE - | CHECK_STEP_INSPECTING_SECOND_TIP - | CHECK_STEP_JOGGING_SECOND_PIPETTE_HEIGHT - | CHECK_STEP_COMPARING_SECOND_PIPETTE_HEIGHT - | CHECK_STEP_JOGGING_SECOND_PIPETTE_POINT_ONE - | CHECK_STEP_COMPARING_SECOND_PIPETTE_POINT_ONE + | CHECK_STEP_PREPARING_PIPETTE + | CHECK_STEP_INSPECTING_TIP + | CHECK_STEP_COMPARING_HEIGHT + | CHECK_STEP_COMPARING_POINT_ONE + | CHECK_STEP_COMPARING_POINT_TWO + | CHECK_STEP_COMPARING_POINT_THREE + | CHECK_STEP_RETURNING_TIP + | CHECK_STEP_RESULTS_SUMMARY | CHECK_STEP_SESSION_EXITED | CHECK_STEP_CHECK_COMPLETE | CHECK_STEP_BAD_ROBOT_CALIBRATION - | CHECK_STEP_NO_PIPETTES_ATTACHED export type RobotCalibrationCheckPipetteRank = | CHECK_PIPETTE_RANK_FIRST | CHECK_PIPETTE_RANK_SECOND -export type RobotCalibrationCheckInstrument = {| +export type CalibrationHealthCheckInstrument = {| model: string, name: string, tip_length: number, mount: string, - tiprack_id: string, rank: RobotCalibrationCheckPipetteRank, serial: string, |} -export type RobotCalibrationCheckLabware = {| - alternatives: Array, - slot: string, - id: string, - forMounts: Array, - loadName: string, - namespace: string, - version: number, -|} - export type CheckTransformType = | CHECK_TRANSFORM_TYPE_INSTRUMENT_OFFSET | CHECK_TRANSFORM_TYPE_UNKNOWN | CHECK_TRANSFORM_TYPE_DECK -export type RobotCalibrationCheckComparison = {| +export type CalibrationHealthCheckComparison = {| differenceVector: [number, number, number], thresholdVector: [number, number, number], exceedsThreshold: boolean, transformType: CheckTransformType, |} -export type RobotCalibrationCheckInstrumentsByMount = { - [mount: string]: RobotCalibrationCheckInstrument, +export type CalibrationHealthCheckComparisonsByStep = { + [RobotCalibrationCheckStep]: CalibrationHealthCheckComparison, ..., } -export type RobotCalibrationCheckComparisonsByStep = { - [RobotCalibrationCheckStep]: RobotCalibrationCheckComparison, - ..., +export type CalibrationHealthCheckComparisonByPipette = { + first: CalibrationHealthCheckComparisonsByStep, + second: CalibrationHealthCheckComparisonsByStep, } -export type RobotCalibrationCheckSessionDetails = {| - instruments: RobotCalibrationCheckInstrumentsByMount, +export type CheckCalibrationHealthSessionDetails = {| + instruments: Array, currentStep: RobotCalibrationCheckStep, - nextSteps: {| - links: { [RobotCalibrationCheckStep]: string, ... }, - |}, - comparisonsByStep: RobotCalibrationCheckComparisonsByStep, - labware: Array, + comparisonsByPipette: CalibrationHealthCheckComparisonByPipette, + labware: Array, + activePipette: CalibrationHealthCheckInstrument, + activeTipRack: CalibrationLabware, |} diff --git a/app/src/sessions/constants.js b/app/src/sessions/constants.js index 75a1ed3d962..103578db22c 100644 --- a/app/src/sessions/constants.js +++ b/app/src/sessions/constants.js @@ -60,7 +60,7 @@ export const CREATE_SESSION_COMMAND_SUCCESS: 'sessions:CREATE_SESSION_COMMAND_SU export const CREATE_SESSION_COMMAND_FAILURE: 'sessions:CREATE_SESSION_COMMAND_FAILURE' = 'sessions:CREATE_SESSION_COMMAND_FAILURE' -export const SESSION_TYPE_CALIBRATION_CHECK: 'calibrationCheck' = +export const SESSION_TYPE_CALIBRATION_HEALTH_CHECK: 'calibrationCheck' = 'calibrationCheck' export const SESSION_TYPE_TIP_LENGTH_CALIBRATION: 'tipLengthCalibration' = 'tipLengthCalibration' diff --git a/app/src/sessions/pipette-offset-calibration/types.js b/app/src/sessions/pipette-offset-calibration/types.js index 189595323ee..f152e0b5157 100644 --- a/app/src/sessions/pipette-offset-calibration/types.js +++ b/app/src/sessions/pipette-offset-calibration/types.js @@ -37,7 +37,7 @@ export type PipetteOffsetCalibrationSessionParams = {| mount: string, shouldRecalibrateTipLength: boolean, hasCalibrationBlock: boolean, - tipRackDefinition: ?LabwareDefinition2, + tipRackDefinition: LabwareDefinition2 | null, |} export type PipetteOffsetCalibrationSessionDetails = {| diff --git a/app/src/sessions/selectors.js b/app/src/sessions/selectors.js index c1328a9ff1b..fe33b0cded0 100644 --- a/app/src/sessions/selectors.js +++ b/app/src/sessions/selectors.js @@ -31,35 +31,68 @@ export function getRobotSessionOfType( return foundSessionId ? sessionsById[foundSessionId] : null } +// TODO (lc 10-20-2020) move these selectors into a +// a cal check specific file. const getMountEventPropsFromCalibrationCheck: ( session: Types.CalibrationCheckSession ) => Types.AnalyticsModelsByMount = session => { const { instruments } = session.details const initialModelsByMount: $Shape = {} - const modelsByMount: Types.AnalyticsModelsByMount = Object.keys( - instruments - ).reduce( - (acc: Types.AnalyticsModelsByMount, mount: string) => ({ + const modelsByMount: Types.AnalyticsModelsByMount = instruments.reduce( + ( + acc: Types.AnalyticsModelsByMount, + instrument: Types.CalibrationHealthCheckInstrument + ) => ({ ...acc, - [`${mount.toLowerCase()}PipetteModel`]: instruments[mount].model, + [`${instrument.mount.toLowerCase()}PipetteModel`]: instrument.model, }), initialModelsByMount ) return modelsByMount } +// TODO (lc 10-20-2020) move these selectors into a +// a cal check specific file. const getSharedAnalyticsPropsFromCalibrationCheck: ( session: Types.CalibrationCheckSession ) => Types.SharedAnalyticsProps = session => ({ sessionType: session.sessionType, }) +// TODO (lc 10-20-2020) move these selectors into a +// a cal check specific file. const getAnalyticsPropsFromCalibrationCheck: ( session: Types.CalibrationCheckSession ) => Types.CalibrationCheckSessionAnalyticsProps = session => { - const { comparisonsByStep } = session.details + const { comparisonsByPipette, activePipette } = session.details + const rank = activePipette.rank const initialStepData: $Shape = {} - const normalizedStepData = Object.keys(comparisonsByStep).reduce( + const normalizedStepDataFirstPip = Object.keys( + comparisonsByPipette.first + ).reduce( + ( + acc: Types.CalibrationCheckAnalyticsData, + stepName: Types.RobotCalibrationCheckStep + ) => { + const { + differenceVector, + thresholdVector, + exceedsThreshold, + transformType, + } = comparisonsByPipette[rank][stepName] + return { + ...acc, + [`${stepName}DifferenceVector`]: differenceVector, + [`${stepName}ThresholdVector`]: thresholdVector, + [`${stepName}ExceedsThreshold`]: exceedsThreshold, + [`${stepName}ErrorSource`]: transformType, + } + }, + initialStepData + ) + const normalizedStepDataSecondPip = Object.keys( + comparisonsByPipette.second + ).reduce( ( acc: Types.CalibrationCheckAnalyticsData, stepName: Types.RobotCalibrationCheckStep @@ -69,7 +102,7 @@ const getAnalyticsPropsFromCalibrationCheck: ( thresholdVector, exceedsThreshold, transformType, - } = comparisonsByStep[stepName] + } = comparisonsByPipette[rank][stepName] return { ...acc, [`${stepName}DifferenceVector`]: differenceVector, @@ -83,21 +116,28 @@ const getAnalyticsPropsFromCalibrationCheck: ( return { ...getSharedAnalyticsPropsFromCalibrationCheck(session), ...getMountEventPropsFromCalibrationCheck(session), - ...normalizedStepData, + ...normalizedStepDataFirstPip, + ...normalizedStepDataSecondPip, } } +// TODO (lc 10-20-2020) move these selectors into a +// a cal check specific file. const getIntercomPropsFromCalibrationCheck: ( session: Types.CalibrationCheckSession ) => Types.CalibrationCheckSessionIntercomProps = session => { - const { comparisonsByStep } = session.details + const { comparisonsByPipette, activePipette } = session.details + const rank = activePipette.rank + const comparisons = comparisonsByPipette[rank] const initialStepData: $Shape = {} - const normalizedStepData = Object.keys(comparisonsByStep).reduce( + const normalizedStepData = Object.keys(comparisons).reduce( ( acc: Types.CalibrationCheckIntercomData, stepName: Types.RobotCalibrationCheckStep ) => { - const { exceedsThreshold, transformType } = comparisonsByStep[stepName] + const { exceedsThreshold, transformType } = comparisonsByPipette[rank][ + stepName + ] return { ...acc, [`${stepName}ExceedsThreshold`]: exceedsThreshold, @@ -108,9 +148,7 @@ const getIntercomPropsFromCalibrationCheck: ( ) const succeeded = !some( - Object.keys(comparisonsByStep).map(k => - Boolean(comparisonsByStep[k].exceedsThreshold) - ) + Object.keys(comparisons).map(k => Boolean(comparisons[k].exceedsThreshold)) ) return { ...getSharedAnalyticsPropsFromCalibrationCheck(session), @@ -128,7 +166,7 @@ export const getAnalyticsPropsForRobotSessionById: ( const session = getRobotSessionById(state, robotName, sessionId) if (!session) return null - if (session.sessionType === Constants.SESSION_TYPE_CALIBRATION_CHECK) { + if (session.sessionType === Constants.SESSION_TYPE_CALIBRATION_HEALTH_CHECK) { return getAnalyticsPropsFromCalibrationCheck(session) } else { // the exited session type doesn't report to analytics @@ -143,7 +181,7 @@ export const getIntercomEventPropsForRobotSessionById: ( ) => Types.SessionIntercomProps | null = (state, robotName, sessionId) => { const session = getRobotSessionById(state, robotName, sessionId) if (!session) return null - if (session.sessionType === Constants.SESSION_TYPE_CALIBRATION_CHECK) { + if (session.sessionType === Constants.SESSION_TYPE_CALIBRATION_HEALTH_CHECK) { return getIntercomPropsFromCalibrationCheck(session) } else { // the exited session type doesn't report to analytics diff --git a/app/src/sessions/types.js b/app/src/sessions/types.js index 00a6a7d467e..32844f7762b 100644 --- a/app/src/sessions/types.js +++ b/app/src/sessions/types.js @@ -19,7 +19,7 @@ import typeof { CREATE_SESSION_COMMAND, CREATE_SESSION_COMMAND_SUCCESS, CREATE_SESSION_COMMAND_FAILURE, - SESSION_TYPE_CALIBRATION_CHECK, + SESSION_TYPE_CALIBRATION_HEALTH_CHECK, SESSION_TYPE_TIP_LENGTH_CALIBRATION, SESSION_TYPE_DECK_CALIBRATION, SESSION_TYPE_PIPETTE_OFFSET_CALIBRATION, @@ -48,7 +48,7 @@ export type * from './pipette-offset-calibration/types' // The available session types export type SessionType = - | SESSION_TYPE_CALIBRATION_CHECK + | SESSION_TYPE_CALIBRATION_HEALTH_CHECK | SESSION_TYPE_TIP_LENGTH_CALIBRATION | SESSION_TYPE_DECK_CALIBRATION | SESSION_TYPE_PIPETTE_OFFSET_CALIBRATION @@ -83,8 +83,8 @@ export type SessionCommandParams = { } export type CalibrationCheckSessionResponseAttributes = {| - sessionType: SESSION_TYPE_CALIBRATION_CHECK, - details: CalCheckTypes.RobotCalibrationCheckSessionDetails, + sessionType: SESSION_TYPE_CALIBRATION_HEALTH_CHECK, + details: CalCheckTypes.CheckCalibrationHealthSessionDetails, createParams: {}, |} @@ -333,18 +333,14 @@ export type AnalyticsModelsByMount = {| |} export type CalibrationCheckCommonEventData = {| - comparingFirstPipetteHeightExceedsThreshold?: boolean, - comparingFirstPipetteHeightErrorSource?: string, - comparingFirstPipettePointOneExceedsThreshold?: boolean, - comparingFirstPipettePointOneErrorSource?: string, - comparingFirstPipettePointTwoExceedsThreshold?: boolean, - comparingFirstPipettePointTwoErrorSource?: string, - comparingFirstPipettePointThreeExceedsThreshold?: boolean, - comparingFirstPipettePointThreeErrorSource?: string, - comparingSecondPipetteHeightExceedsThreshold?: boolean, - comparingSecondPipetteHeightErrorSource?: string, - comparingSecondPipettePointOneExceedsThreshold?: boolean, - comparingSecondPipettePointOneErrorSource?: string, + comparingHeightExceedsThreshold?: boolean, + comparingHeightErrorSource?: string, + comparingPointOneExceedsThreshold?: boolean, + comparingPointOneErrorSource?: string, + comparingPointTwoExceedsThreshold?: boolean, + comparingPointTwoErrorSource?: string, + comparingPointThreeExceedsThreshold?: boolean, + comparingPointThreeErrorSource?: string, |} export type CalibrationCheckIntercomData = {| @@ -354,18 +350,14 @@ export type CalibrationCheckIntercomData = {| export type CalibrationCheckAnalyticsData = {| ...CalibrationCheckCommonEventData, - comparingFirstPipetteHeightDifferenceVector?: VectorTuple, - comparingFirstPipetteHeightThresholdVector?: VectorTuple, - comparingFirstPipettePointOneDifferenceVector?: VectorTuple, - comparingFirstPipettePointOneThresholdVector?: VectorTuple, - comparingFirstPipettePointTwoDifferenceVector?: VectorTuple, - comparingFirstPipettePointTwoThresholdVector?: VectorTuple, - comparingFirstPipettePointThreeDifferenceVector?: VectorTuple, - comparingFirstPipettePointThreeThresholdVector?: VectorTuple, - comparingSecondPipetteHeightDifferenceVector?: VectorTuple, - comparingSecondPipetteHeightThresholdVector?: VectorTuple, - comparingSecondPipettePointOneDifferenceVector?: VectorTuple, - comparingSecondPipettePointOneThresholdVector?: VectorTuple, + comparingHeightDifferenceVector?: VectorTuple, + comparingHeightThresholdVector?: VectorTuple, + comparingPointOneDifferenceVector?: VectorTuple, + comparingPointOneThresholdVector?: VectorTuple, + comparingPointTwoDifferenceVector?: VectorTuple, + comparingPointTwoThresholdVector?: VectorTuple, + comparingPointThreeDifferenceVector?: VectorTuple, + comparingPointThreeThresholdVector?: VectorTuple, |} export type SharedAnalyticsProps = {| diff --git a/app/src/support/__tests__/epic.test.js b/app/src/support/__tests__/epic.test.js deleted file mode 100644 index da0e66bb687..00000000000 --- a/app/src/support/__tests__/epic.test.js +++ /dev/null @@ -1,160 +0,0 @@ -// @flow -// support profile epic test -import { TestScheduler } from 'rxjs/testing' -import { configInitialized } from '../../config' -import * as Profile from '../profile' -import * as Event from '../intercom-event' -import { supportEpic } from '../epic' - -import type { Action, State } from '../../types' -import type { Config } from '../../config/types' -import type { - SupportConfig, - SupportProfileUpdate, - IntercomEvent, -} from '../types' - -jest.mock('../profile') -jest.mock('../intercom-event') - -const makeProfileUpdate: JestMockFn< - [Action, State], - SupportProfileUpdate | null -> = Profile.makeProfileUpdate - -const makeIntercomEvent: JestMockFn<[Action, State], IntercomEvent | null> = - Event.makeIntercomEvent - -const sendEvent: JestMockFn<[IntercomEvent], void> = Event.sendEvent - -const initializeProfile: JestMockFn<[SupportConfig], void> = - Profile.initializeProfile - -const updateProfile: JestMockFn<[SupportProfileUpdate], void> = - Profile.updateProfile - -const MOCK_ACTION: Action = ({ type: 'MOCK_ACTION' }: any) -const MOCK_PROFILE_STATE: $Shape<{| ...State, config: $Shape |}> = { - config: { - support: { userId: 'foo', createdAt: 42, name: 'bar', email: null }, - }, -} - -const MOCK_EVENT_STATE: $Shape<{| ...State |}> = {} - -describe('support profile epic', () => { - let testScheduler - - beforeEach(() => { - makeProfileUpdate.mockReturnValue(null) - - testScheduler = new TestScheduler((actual, expected) => { - expect(actual).toEqual(expected) - }) - }) - - afterEach(() => { - jest.resetAllMocks() - }) - - it('should initialize support profile on config:INITIALIZED', () => { - testScheduler.run(({ hot, expectObservable, flush }) => { - const action$ = hot('-a', { - a: configInitialized(MOCK_PROFILE_STATE.config), - }) - const state$ = hot('--') - const result$ = supportEpic(action$, state$) - - expectObservable(result$, '--') - flush() - - expect(initializeProfile).toHaveBeenCalledWith( - MOCK_PROFILE_STATE.config.support - ) - }) - }) - - it('should do nothing with actions that do not map to a profile update', () => { - testScheduler.run(({ hot, expectObservable, flush }) => { - const action$ = hot('-a', { a: MOCK_ACTION }) - const state$ = hot('s-', { s: MOCK_PROFILE_STATE }) - const result$ = supportEpic(action$, state$) - - expectObservable(result$, '--') - flush() - - expect(makeProfileUpdate).toHaveBeenCalledWith( - MOCK_ACTION, - MOCK_PROFILE_STATE - ) - }) - }) - - it('should call a profile update ', () => { - const profileUpdate = { someProp: 'value' } - makeProfileUpdate.mockReturnValueOnce(profileUpdate) - - testScheduler.run(({ hot, expectObservable, flush }) => { - const action$ = hot('-a', { a: MOCK_ACTION }) - const state$ = hot('s-', { s: MOCK_PROFILE_STATE }) - const result$ = supportEpic(action$, state$) - - expectObservable(result$) - flush() - - expect(updateProfile).toHaveBeenCalledWith(profileUpdate) - }) - }) -}) - -describe('support event epic', () => { - let testScheduler - - beforeEach(() => { - makeIntercomEvent.mockReturnValue(null) - - testScheduler = new TestScheduler((actual, expected) => { - expect(actual).toEqual(expected) - }) - }) - - afterEach(() => { - jest.resetAllMocks() - }) - - it('should do nothing with actions that do not map to an event', () => { - testScheduler.run(({ hot, expectObservable, flush }) => { - const action$ = hot('-a', { a: MOCK_ACTION }) - const state$ = hot('s-', { s: MOCK_EVENT_STATE }) - const result$ = supportEpic(action$, state$) - - expectObservable(result$, '--') - flush() - - expect(makeIntercomEvent).toHaveBeenCalledWith( - MOCK_ACTION, - MOCK_EVENT_STATE - ) - expect(sendEvent).not.toHaveBeenCalled() - }) - }) - - it('should send an event', () => { - const eventPayload = { - eventName: 'completed-robot-calibration-check', - metadata: { someProp: 'value' }, - } - makeIntercomEvent.mockReturnValueOnce(eventPayload) - - testScheduler.run(({ hot, expectObservable, flush }) => { - const action$ = hot('-a', { a: MOCK_ACTION }) - const state$ = hot('s-', { s: MOCK_PROFILE_STATE }) - const result$ = supportEpic(action$, state$) - - expectObservable(result$) - flush() - - expect(sendEvent).toHaveBeenCalledWith(eventPayload) - }) - }) -}) diff --git a/app/src/support/__tests__/intercom-event.test.js b/app/src/support/__tests__/intercom-event.test.js deleted file mode 100644 index 656d934bad9..00000000000 --- a/app/src/support/__tests__/intercom-event.test.js +++ /dev/null @@ -1,82 +0,0 @@ -// @flow - -import type { IntercomPayload } from '../types' -import type { State } from '../../types' -import * as Binding from '../intercom-binding' -import * as Sessions from '../../sessions' -import * as SessionTypes from '../../sessions/types' -import { makeIntercomEvent, sendEvent } from '../intercom-event' -import * as Constants from '../constants' - -jest.mock('../intercom-binding') -jest.mock('../../sessions/selectors') - -const sendIntercomEvent: JestMockFn<[string, IntercomPayload], void> = - Binding.sendIntercomEvent -const getIntercomEventPropsForRobotSessionById: JestMockFn< - [State, string, string], - SessionTypes.SessionIntercomProps | null -> = Sessions.getIntercomEventPropsForRobotSessionById - -const MOCK_STATE: $Shape<{| ...State |}> = {} - -describe('support event tests', () => { - afterEach(() => { - jest.resetAllMocks() - }) - - it('makeIntercomEvent should ignore unhandled events', () => { - const built = makeIntercomEvent( - Sessions.createSession( - 'whocares', - Sessions.SESSION_TYPE_CALIBRATION_CHECK, - {} - ), - MOCK_STATE - ) - expect(built).toBeNull() - }) - - it('makeIntercomEvent should send an event for calibration check complete', () => { - const sessionState = { - sessionType: 'calibrationCheck', - succeeded: false, - leftPipetteModel: 'p300_single_v2.0', - comparingFirstPipetteHeightExceedsThreshold: true, - comparingFirstPipetteHeightErrorSource: 'unknown', - } - getIntercomEventPropsForRobotSessionById.mockReturnValue(sessionState) - const built = makeIntercomEvent( - Sessions.deleteSession('silly-robot', 'dummySessionID'), - MOCK_STATE - ) - expect(built).toEqual({ - eventName: Constants.INTERCOM_EVENT_CALCHECK_COMPLETE, - metadata: sessionState, - }) - }) - - it('makeIntercomEvent should ignore events for which no data can be retrieved', () => { - getIntercomEventPropsForRobotSessionById.mockReturnValue(null) - const built = makeIntercomEvent( - Sessions.deleteSession('silly-robot', 'dummySessionID'), - MOCK_STATE - ) - expect(built).toBeNull() - }) - - it('sendEvent should pass on its arguments', () => { - const props = { - eventName: Constants.INTERCOM_EVENT_CALCHECK_COMPLETE, - metadata: { - someKey: true, - someOtherKey: 'hi', - }, - } - sendEvent(props) - expect(sendIntercomEvent).toHaveBeenCalledWith( - props.eventName, - props.metadata - ) - }) -}) diff --git a/app/src/support/epic.js b/app/src/support/epic.js index 5462c894e68..3328e166bff 100644 --- a/app/src/support/epic.js +++ b/app/src/support/epic.js @@ -6,8 +6,6 @@ import { tap, filter, withLatestFrom, ignoreElements } from 'rxjs/operators' import * as Cfg from '../config' import { initializeProfile, makeProfileUpdate, updateProfile } from './profile' -import { makeIntercomEvent, sendEvent } from './intercom-event' - import type { Epic } from '../types' import type { ConfigInitializedAction } from '../config/types' @@ -30,17 +28,7 @@ const updateProfileEpic: Epic = (action$, state$) => { ) } -const sendEventEpic: Epic = (action$, state$) => { - return action$.pipe( - withLatestFrom(state$, makeIntercomEvent), - filter(maybeSend => maybeSend !== null), - tap(sendEvent), - ignoreElements() - ) -} - export const supportEpic: Epic = combineEpics( initializeSupportEpic, - updateProfileEpic, - sendEventEpic + updateProfileEpic ) diff --git a/app/src/support/intercom-event.js b/app/src/support/intercom-event.js deleted file mode 100644 index 1c09beb2249..00000000000 --- a/app/src/support/intercom-event.js +++ /dev/null @@ -1,36 +0,0 @@ -// @flow -// functions for sending events to intercom, both for enriching user profiles -// and for triggering contextual support conversations -import type { Action, State } from '../types' -import { sendIntercomEvent } from './intercom-binding' -import type { IntercomEvent } from './types' -import { INTERCOM_EVENT_CALCHECK_COMPLETE } from './constants' -import * as Sessions from '../sessions' - -export function makeIntercomEvent( - action: Action, - state: State -): IntercomEvent | null { - switch (action.type) { - case Sessions.DELETE_SESSION: { - const { robotName, sessionId } = action.payload - const eventProps = Sessions.getIntercomEventPropsForRobotSessionById( - state, - robotName, - sessionId - ) - if (eventProps === null) { - return null - } - return { - eventName: INTERCOM_EVENT_CALCHECK_COMPLETE, - metadata: eventProps, - } - } - } - return null -} - -export function sendEvent(event: IntercomEvent): void { - sendIntercomEvent(event.eventName, event?.metadata ?? {}) -} diff --git a/robot-server/robot_server/robot/calibration/check/constants.py b/robot-server/robot_server/robot/calibration/check/constants.py index 39d442c4996..15e57794ab5 100644 --- a/robot-server/robot_server/robot/calibration/check/constants.py +++ b/robot-server/robot_server/robot/calibration/check/constants.py @@ -1,7 +1,29 @@ +from typing import Dict, Union +from typing_extensions import Literal + +from enum import Enum from opentrons.types import Point +from robot_server.robot.calibration.constants import ( + STATE_WILDCARD, POINT_ONE_ID, POINT_TWO_ID, POINT_THREE_ID) + + +class CalibrationCheckState(str, Enum): + sessionStarted = "sessionStarted" + labwareLoaded = "labwareLoaded" + preparingPipette = "preparingPipette" + inspectingTip = "inspectingTip" + comparingHeight = "comparingHeight" + comparingPointOne = "comparingPointOne" + comparingPointTwo = "comparingPointTwo" + comparingPointThree = "comparingPointThree" + returningTip = "returningTip" + resultsSummary = "resultsSummary" + sessionExited = "sessionExited" + badCalibrationData = "badCalibrationData" + calibrationComplete = "calibrationComplete" + WILDCARD = STATE_WILDCARD -MOVE_TO_TIP_RACK_SAFETY_BUFFER = Point(0, 0, 10) # Add in a 2mm buffer to tiprack thresholds on top of # the max acceptable range for a given pipette based @@ -22,3 +44,14 @@ 'p20_crosses': Point(1.4, 1.4, 0.0), 'other_height': Point(0.0, 0.0, 0.8) } + +TIPRACK_SLOT = '8' + +StatePointMap = Dict[ + CalibrationCheckState, Union[Literal['1BLC', '3BRC', '7TLC']]] + +MOVE_POINT_STATE_MAP: StatePointMap = { + CalibrationCheckState.comparingHeight: POINT_ONE_ID, + CalibrationCheckState.comparingPointOne: POINT_TWO_ID, + CalibrationCheckState.comparingPointTwo: POINT_THREE_ID +} diff --git a/robot-server/robot_server/robot/calibration/check/models.py b/robot-server/robot_server/robot/calibration/check/models.py index 8eab92cc248..443def96903 100644 --- a/robot-server/robot_server/robot/calibration/check/models.py +++ b/robot-server/robot_server/robot/calibration/check/models.py @@ -1,9 +1,8 @@ -from uuid import UUID -from typing import Dict, Optional, List, Tuple +from typing import Optional, List, Tuple from functools import partial from pydantic import BaseModel, Field -from ..helper_classes import NextSteps +from ..helper_classes import RequiredLabware, AttachedPipette OffsetVector = Tuple[float, float, float] @@ -12,46 +11,6 @@ "coordinates (x, y, z)") -# TODO: BC: the mount and rank fields here are typed as strings -# because of serialization problems, though they are actually -# backed by enums. This shouldn't be the case, and we should -# be able to handle the de/serialization of these fields from -# the middle ware before they are returned to the client -class AttachedPipette(BaseModel): - """Pipette (if any) attached to the mount""" - model: Optional[str] =\ - Field(None, - description="The model of the attached pipette. These are snake " - "case as in the Protocol API. This includes the full" - " version string") - name: Optional[str] =\ - Field(None, description="Short name of pipette model without" - "generation version") - tip_length: Optional[float] =\ - Field(None, description="The default tip length for this pipette") - mount: Optional[str] =\ - Field(None, description="The mount this pipette attached to") - has_tip: Optional[bool] =\ - Field(None, description="Whether a tip is attached.") - tiprack_id: Optional[UUID] =\ - Field(None, description="Id of tiprack associated with this pipette.") - rank: Optional[str] =\ - Field(None, description="Rank in the order of pipettes used for flow") - serial: Optional[str] =\ - Field(None, description="The serial number of the attached pipette") - - -class LabwareStatus(BaseModel): - """A model describing all tipracks required, based on pipettes attached.""" - alternatives: List[str] - slot: Optional[str] - id: UUID - forMounts: List[str] - loadName: str - namespace: str - version: str - - class ComparisonStatus(BaseModel): """ A model describing the comparison of a checked point to calibrated value @@ -62,49 +21,63 @@ class ComparisonStatus(BaseModel): transformType: str -class CalibrationSessionStatus(BaseModel): +class ComparisonMap(BaseModel): + comparingHeight: Optional[ComparisonStatus] =\ + Field(None, description="height validation step") + comparingPointOne: Optional[ComparisonStatus] =\ + Field(None, description="point 1 validation step") + comparingPointTwo: Optional[ComparisonStatus] =\ + Field(None, description="point 2 validation step") + comparingPointThree: Optional[ComparisonStatus] =\ + Field(None, description="point 3 validation step") + + +class ComparisonStatePerPipette(BaseModel): + first: ComparisonMap + second: ComparisonMap + + +class CheckAttachedPipette(AttachedPipette): + rank: str + + +class CalibrationCheckSessionStatus(BaseModel): """The current status of a given session.""" - instruments: Dict[str, AttachedPipette] + instruments: List[CheckAttachedPipette] + activePipette: CheckAttachedPipette currentStep: str = Field(..., description="Current step of session") - comparisonsByStep: Dict[str, ComparisonStatus] - nextSteps: Optional[NextSteps] =\ - Field(None, description="Next Available Steps in Session") - labware: List[LabwareStatus] + comparisonsByPipette: ComparisonStatePerPipette + labware: List[RequiredLabware] + activeTipRack: RequiredLabware class Config: arbitrary_types_allowed = True schema_extra = { "examples": [ { - "instruments": { - "fakeUUID": { + "instruments": [ + { "model": "p300_single_v1.5", "name": "p300_single", "tip_length": 51.7, "mount": "left", "id": "P3HS12123041" }, - "fakeUUID2": { + { "model": None, "name": None, "tip_length": None, "mount": "right", "id": None } - }, + ], "currentStep": "sessionStarted", - "comparisonsByStep": { + "comparisonsByPipette": { "comparingFirstPipetteHeight": { "differenceVector": [1, 0, 0], "exceedsThreshold": False } - }, - "nextSteps": { - "links": { - "loadLabware": {"url": "", "params": {}} - } } - } ] } diff --git a/robot-server/robot_server/robot/calibration/check/session.py b/robot-server/robot_server/robot/calibration/check/session.py deleted file mode 100644 index 4bd7d232fe5..00000000000 --- a/robot-server/robot_server/robot/calibration/check/session.py +++ /dev/null @@ -1,768 +0,0 @@ -import typing -import logging -from uuid import uuid4 -from enum import Enum -from dataclasses import dataclass - -from opentrons.protocols.implementations.labware import LabwareImplementation - -from robot_server.robot.calibration.session import CalibrationSession, \ - HEIGHT_SAFETY_BUFFER -from opentrons.types import Mount, Point, Location - -from robot_server.robot.calibration.check.util import StateMachine, WILDCARD -from robot_server.robot.calibration.check.models import ComparisonStatus -from robot_server.robot.calibration.helper_classes import ( - CheckMove, DeckCalibrationError, PipetteRank, PipetteInfo, PipetteStatus -) -from robot_server.service.errors import RobotServerError -from opentrons.hardware_control import ThreadManager -from opentrons.protocol_api import labware - -from robot_server.service.session.models.command import \ - CalibrationCommand, CalibrationCheckCommand -from robot_server.service.session.models.common import OffsetVector -from .constants import (PIPETTE_TOLERANCES, - P1000_OK_TIP_PICK_UP_VECTOR, - DEFAULT_OK_TIP_PICK_UP_VECTOR, - MOVE_TO_TIP_RACK_SAFETY_BUFFER) -from ..errors import CalibrationError - -MODULE_LOG = logging.getLogger(__name__) - -""" -A set of endpoints that can be used to create a session for any robot -calibration tasks such as checking your calibration data, performing mount -offset or a robot deck transform. -""" - - -class CalibrationCheckState(str, Enum): - sessionStarted = "sessionStarted" - labwareLoaded = "labwareLoaded" - preparingFirstPipette = "preparingFirstPipette" - inspectingFirstTip = "inspectingFirstTip" - joggingFirstPipetteToHeight = "joggingFirstPipetteToHeight" - comparingFirstPipetteHeight = "comparingFirstPipetteHeight" - joggingFirstPipetteToPointOne = "joggingFirstPipetteToPointOne" - comparingFirstPipettePointOne = "comparingFirstPipettePointOne" - joggingFirstPipetteToPointTwo = "joggingFirstPipetteToPointTwo" - comparingFirstPipettePointTwo = "comparingFirstPipettePointTwo" - joggingFirstPipetteToPointThree = "joggingFirstPipetteToPointThree" - comparingFirstPipettePointThree = "comparingFirstPipettePointThree" - preparingSecondPipette = "preparingSecondPipette" - inspectingSecondTip = "inspectingSecondTip" - joggingSecondPipetteToHeight = "joggingSecondPipetteToHeight" - comparingSecondPipetteHeight = "comparingSecondPipetteHeight" - joggingSecondPipetteToPointOne = "joggingSecondPipetteToPointOne" - comparingSecondPipettePointOne = "comparingSecondPipettePointOne" - returningTip = "returningTip" - sessionExited = "sessionExited" - badCalibrationData = "badCalibrationData" - checkComplete = "checkComplete" - - -class CalibrationCheckTrigger(str, Enum): - load_labware = CalibrationCommand.load_labware.value - prepare_pipette = CalibrationCheckCommand.prepare_pipette.value - jog = CalibrationCommand.jog.value - pick_up_tip = CalibrationCommand.pick_up_tip.value - confirm_tip_attached = CalibrationCommand.confirm_tip_attached.value - invalidate_tip = CalibrationCommand.invalidate_tip.value - compare_point = CalibrationCheckCommand.compare_point.value - go_to_next_check = CalibrationCheckCommand.go_to_next_check.value - exit = CalibrationCommand.exit.value - reject_calibration = CalibrationCheckCommand.reject_calibration.value - - -CHECK_TRANSITIONS: typing.List[typing.Dict[str, typing.Any]] = [ - { - "trigger": CalibrationCheckTrigger.load_labware, - "from_state": CalibrationCheckState.sessionStarted, - "to_state": CalibrationCheckState.labwareLoaded, - "before": "_load_tip_rack_objects" - }, - { - "trigger": CalibrationCheckTrigger.prepare_pipette, - "from_state": CalibrationCheckState.labwareLoaded, - "to_state": CalibrationCheckState.preparingFirstPipette, - "after": "_move_first_pipette" - }, - { - "trigger": CalibrationCheckTrigger.jog, - "from_state": CalibrationCheckState.preparingFirstPipette, - "to_state": CalibrationCheckState.preparingFirstPipette, - "before": "_jog_first_pipette", - }, - { - "trigger": CalibrationCheckTrigger.pick_up_tip, - "from_state": CalibrationCheckState.preparingFirstPipette, - "to_state": CalibrationCheckState.inspectingFirstTip, - "after": [ - "_register_point_first_pipette", - "_pick_up_tip_first_pipette"] - }, - { - "trigger": CalibrationCheckTrigger.invalidate_tip, - "from_state": CalibrationCheckState.inspectingFirstTip, - "to_state": CalibrationCheckState.preparingFirstPipette, - "before": "_return_first_tip", - "after": "_move_first_pipette" - }, - { - "trigger": CalibrationCheckTrigger.confirm_tip_attached, - "from_state": CalibrationCheckState.inspectingFirstTip, - "to_state": CalibrationCheckState.badCalibrationData, - "condition": "_is_tip_pick_up_dangerous", - }, - { - "trigger": CalibrationCheckTrigger.confirm_tip_attached, - "from_state": CalibrationCheckState.inspectingFirstTip, - "to_state": CalibrationCheckState.joggingFirstPipetteToHeight, - "after": "_move_first_pipette", - }, - { - "trigger": CalibrationCheckTrigger.jog, - "from_state": CalibrationCheckState.joggingFirstPipetteToHeight, - "to_state": CalibrationCheckState.joggingFirstPipetteToHeight, - "before": "_jog_first_pipette" - }, - { - "trigger": CalibrationCheckTrigger.compare_point, - "from_state": CalibrationCheckState.joggingFirstPipetteToHeight, - "to_state": CalibrationCheckState.comparingFirstPipetteHeight, - "after": "_register_point_first_pipette" - }, - { - "trigger": CalibrationCheckTrigger.go_to_next_check, - "from_state": CalibrationCheckState.comparingFirstPipetteHeight, - "to_state": CalibrationCheckState.joggingFirstPipetteToPointOne, - "after": "_move_first_pipette", - }, - { - "trigger": CalibrationCheckTrigger.jog, - "from_state": CalibrationCheckState.joggingFirstPipetteToPointOne, - "to_state": CalibrationCheckState.joggingFirstPipetteToPointOne, - "before": "_jog_first_pipette" - }, - { - "trigger": CalibrationCheckTrigger.compare_point, - "from_state": CalibrationCheckState.joggingFirstPipetteToPointOne, - "to_state": CalibrationCheckState.comparingFirstPipettePointOne, - "after": "_register_point_first_pipette" - }, - { - "trigger": CalibrationCheckTrigger.go_to_next_check, - "from_state": CalibrationCheckState.comparingFirstPipettePointOne, - "to_state": CalibrationCheckState.joggingFirstPipetteToPointTwo, - "after": "_move_first_pipette", - }, - { - "trigger": CalibrationCheckTrigger.jog, - "from_state": CalibrationCheckState.joggingFirstPipetteToPointTwo, - "to_state": CalibrationCheckState.joggingFirstPipetteToPointTwo, - "before": "_jog_first_pipette" - }, - { - "trigger": CalibrationCheckTrigger.compare_point, - "from_state": CalibrationCheckState.joggingFirstPipetteToPointTwo, - "to_state": CalibrationCheckState.comparingFirstPipettePointTwo, - "after": "_register_point_first_pipette" - }, - { - "trigger": CalibrationCheckTrigger.go_to_next_check, - "from_state": CalibrationCheckState.comparingFirstPipettePointTwo, - "to_state": CalibrationCheckState.joggingFirstPipetteToPointThree, - "after": "_move_first_pipette", - }, - { - "trigger": CalibrationCheckTrigger.jog, - "from_state": CalibrationCheckState.joggingFirstPipetteToPointThree, - "to_state": CalibrationCheckState.joggingFirstPipetteToPointThree, - "before": "_jog_first_pipette" - }, - { - "trigger": CalibrationCheckTrigger.compare_point, - "from_state": CalibrationCheckState.joggingFirstPipetteToPointThree, - "to_state": CalibrationCheckState.comparingFirstPipettePointThree, - "after": "_register_point_first_pipette" - }, - { - "trigger": CalibrationCheckTrigger.go_to_next_check, - "from_state": CalibrationCheckState.comparingFirstPipettePointThree, - "to_state": CalibrationCheckState.preparingSecondPipette, - "condition": "_is_checking_both_mounts", - "before": "_trash_first_pipette_tip", - "after": "_move_second_pipette", - }, - { - "trigger": CalibrationCheckTrigger.go_to_next_check, - "from_state": CalibrationCheckState.comparingFirstPipettePointThree, - "to_state": CalibrationCheckState.checkComplete, - }, - { - "trigger": CalibrationCheckTrigger.jog, - "from_state": CalibrationCheckState.preparingSecondPipette, - "to_state": CalibrationCheckState.preparingSecondPipette, - "before": "_jog_second_pipette", - }, - { - "trigger": CalibrationCheckTrigger.pick_up_tip, - "from_state": CalibrationCheckState.preparingSecondPipette, - "to_state": CalibrationCheckState.inspectingSecondTip, - "after": [ - "_register_point_second_pipette", - "_pick_up_tip_second_pipette"] - }, - { - "trigger": CalibrationCheckTrigger.invalidate_tip, - "from_state": CalibrationCheckState.inspectingSecondTip, - "to_state": CalibrationCheckState.preparingSecondPipette, - "before": "_return_second_tip", - "after": "_move_second_pipette" - }, - { - "trigger": CalibrationCheckTrigger.confirm_tip_attached, - "from_state": CalibrationCheckState.inspectingSecondTip, - "to_state": CalibrationCheckState.badCalibrationData, - "condition": "_is_tip_pick_up_dangerous", - }, - { - "trigger": CalibrationCheckTrigger.confirm_tip_attached, - "from_state": CalibrationCheckState.inspectingSecondTip, - "to_state": CalibrationCheckState.joggingSecondPipetteToHeight, - "after": "_move_second_pipette", - }, - { - "trigger": CalibrationCheckTrigger.jog, - "from_state": CalibrationCheckState.joggingSecondPipetteToHeight, - "to_state": CalibrationCheckState.joggingSecondPipetteToHeight, - "before": "_jog_second_pipette" - }, - { - "trigger": CalibrationCheckTrigger.compare_point, - "from_state": CalibrationCheckState.joggingSecondPipetteToHeight, - "to_state": CalibrationCheckState.comparingSecondPipetteHeight, - "after": "_register_point_second_pipette" - }, - { - "trigger": CalibrationCheckTrigger.go_to_next_check, - "from_state": CalibrationCheckState.comparingSecondPipetteHeight, - "to_state": CalibrationCheckState.joggingSecondPipetteToPointOne, - "after": "_move_second_pipette", - }, - { - "trigger": CalibrationCheckTrigger.jog, - "from_state": CalibrationCheckState.joggingSecondPipetteToPointOne, - "to_state": CalibrationCheckState.joggingSecondPipetteToPointOne, - "before": "_jog_second_pipette" - }, - { - "trigger": CalibrationCheckTrigger.compare_point, - "from_state": CalibrationCheckState.joggingSecondPipetteToPointOne, - "to_state": CalibrationCheckState.comparingSecondPipettePointOne, - "after": "_register_point_second_pipette" - }, - { - "trigger": CalibrationCheckTrigger.go_to_next_check, - "from_state": CalibrationCheckState.comparingSecondPipettePointOne, - "to_state": CalibrationCheckState.checkComplete, - }, - { - "trigger": CalibrationCheckTrigger.exit, - "from_state": WILDCARD, - "to_state": CalibrationCheckState.sessionExited - }, - { - "trigger": CalibrationCheckTrigger.reject_calibration, - "from_state": WILDCARD, - "to_state": CalibrationCheckState.badCalibrationData - } -] - - -@dataclass -class ComparisonParams: - reference_state: CalibrationCheckState - - -COMPARISON_STATE_MAP: typing.Dict[CalibrationCheckState, ComparisonParams] = { - CalibrationCheckState.comparingFirstPipetteHeight: ComparisonParams( - reference_state=CalibrationCheckState.joggingFirstPipetteToHeight, - ), - CalibrationCheckState.comparingFirstPipettePointOne: ComparisonParams( - reference_state=CalibrationCheckState.joggingFirstPipetteToPointOne, - ), - CalibrationCheckState.comparingFirstPipettePointTwo: ComparisonParams( - reference_state=CalibrationCheckState.joggingFirstPipetteToPointTwo, - ), - CalibrationCheckState.comparingFirstPipettePointThree: ComparisonParams( - reference_state=CalibrationCheckState.joggingFirstPipetteToPointThree, - ), - CalibrationCheckState.comparingSecondPipetteHeight: ComparisonParams( - reference_state=CalibrationCheckState.joggingSecondPipetteToHeight, - ), - CalibrationCheckState.comparingSecondPipettePointOne: ComparisonParams( - reference_state=CalibrationCheckState.joggingSecondPipetteToPointOne, - ), -} - - -class CheckCalibrationSession(CalibrationSession, StateMachine): - def __init__(self, hardware: 'ThreadManager', - lights_on_before: bool = False): - CalibrationSession.__init__(self, hardware, lights_on_before) - StateMachine.__init__(self, states=[s for s in CalibrationCheckState], - transitions=CHECK_TRANSITIONS, - initial_state="sessionStarted") - self.session_type = 'check' - self._saved_points: typing.Dict[CalibrationCheckState, Point] = {} - - async def handle_command(self, - name: str, - data: typing.Dict[typing.Any, typing.Any]): - """ - Handle a client command - - :param name: Name of the command - :param data: Data supplied in command - :return: None - """ - await self.trigger_transition(trigger=name, **data) - - def _get_pipette_by_rank(self, rank: PipetteRank) -> \ - typing.Optional[PipetteInfo]: - try: - return next(p for p in self._pip_info_by_mount.values() - if p.rank == rank) - except StopIteration: - return None - - def can_distinguish_instr_offset(self): - """ - whether or not we can separate out - calibration diffs that are due to instrument - offset or deck transform or both - """ - first_pip = self._get_pipette_by_rank(PipetteRank.first) - return first_pip and first_pip.mount != Mount.LEFT - - @property - def _initial_z_offset(self): - return Point(0, 0, 0.3) - - async def _is_checking_both_mounts(self): - return len(self._pip_info_by_mount) == 2 - - async def _load_tip_rack_objects(self): - """ - A function that takes tip rack information - and loads them onto the deck. - """ - second_pip = self._get_pipette_by_rank(PipetteRank.second) - for name, lw_data in self._labware_info.items(): - parent = self._deck.position_for(lw_data.slot) - lw = labware.Labware( - implementation=LabwareImplementation( - lw_data.definition, - parent - ) - ) - self._deck[lw_data.slot] = lw - - for mount in lw_data.forMounts: - is_second_mount = second_pip and second_pip.mount == mount - pips_share_rack = len(lw_data.forMounts) == 2 - well_name = 'A1' - if is_second_mount and pips_share_rack: - well_name = 'B1' - well = lw.wells_by_name()[well_name] - position = well.top().point + MOVE_TO_TIP_RACK_SAFETY_BUFFER - move = CheckMove(position=position, locationId=uuid4()) - - if is_second_mount: - self._moves.preparingSecondPipette = move - else: - self._moves.preparingFirstPipette = move - - def pipette_status(self) -> typing.Dict[Mount, PipetteStatus]: - """ - Public property to help format the current labware status of a given - session for the client. - """ - to_dict = {} - for mount, pip_info in self._pip_info_by_mount.items(): - hw_pip = self.pipettes[mount] - p = PipetteStatus( - model=str(hw_pip['model']), - name=str(hw_pip['name']), - mount=str(mount), - tip_length=float(hw_pip['tip_length']), - has_tip=bool(hw_pip['has_tip']), - tiprack_id=pip_info.tiprack_id, - rank=str(pip_info.rank), - serial=str(hw_pip['pipette_id']), - ) - to_dict[mount] = p - return to_dict - - async def delete_session(self): - for mount in self._pip_info_by_mount.keys(): - if self.pipettes[mount]['has_tip']: - try: - await self._trash_tip(mount) - except AssertionError: - pass - await self.hardware.home() - if not self._lights_on_before: - await self.hardware.set_lights(rails=False) - - def _get_preparing_state_mount(self) -> typing.Optional[Mount]: - pip = None - if self.current_state_name == \ - CalibrationCheckState.inspectingFirstTip: - pip = self._get_pipette_by_rank(PipetteRank.first) - elif self.current_state_name == \ - CalibrationCheckState.inspectingSecondTip: - pip = self._get_pipette_by_rank(PipetteRank.second) - assert pip, f'cannot check prepare pipette from state:' \ - f' {self.current_state_name}' - return pip.mount - - def _look_up_state(self) -> CalibrationCheckState: - """ - We want to check whether a tip pick up was dangerous during the - tip inspection state, but the reference points are actually saved - during the preparing pipette state, so we should reference those - states when looking up the reference point. - - :return: The calibration check state that the reference point - was saved under for tip pick up. - """ - if self.current_state_name == CalibrationCheckState.inspectingFirstTip: - return CalibrationCheckState.preparingFirstPipette - elif self.current_state_name == \ - CalibrationCheckState.inspectingSecondTip: - return CalibrationCheckState.preparingSecondPipette - else: - raise RobotServerError( - definition=CalibrationError.NO_STATE_TRANSITION, - state=self.current_state_name) - - async def _is_tip_pick_up_dangerous(self): - """ - Function to determine whether jogged to pick up tip position is - outside of the safe threshold for conducting the rest of the check. - """ - mount = self._get_preparing_state_mount() - assert mount, 'cannot attempt tip pick up, no mount specified' - - ref_state = self._look_up_state() - jogged_pt = self._saved_points[getattr(CalibrationCheckState, - self.current_state_name)] - - ref_pt = self._saved_points[getattr(CalibrationCheckState, - ref_state)] - - ref_pt_no_safety = ref_pt - MOVE_TO_TIP_RACK_SAFETY_BUFFER - threshold_vector = DEFAULT_OK_TIP_PICK_UP_VECTOR - pip_model = self.pipettes[mount]['model'] - if str(pip_model).startswith('p1000'): - threshold_vector = P1000_OK_TIP_PICK_UP_VECTOR - xyThresholdMag = Point(0, 0, 0).magnitude_to( - threshold_vector._replace(z=0)) - zThresholdMag = Point(0, 0, 0).magnitude_to( - threshold_vector._replace(x=0, y=0)) - xyDiffMag = ref_pt_no_safety._replace(z=0).magnitude_to( - jogged_pt._replace(z=0)) - zDiffMag = ref_pt_no_safety._replace(x=0, y=0).magnitude_to( - jogged_pt._replace(x=0, y=0)) - return xyDiffMag > xyThresholdMag or zDiffMag > zThresholdMag - - async def _pick_up_tip_first_pipette(self): - """ - Function to pick up tip. It will attempt to pick up a tip in - the current location, and save any offset it might have from the - original position. - """ - pip = self._get_pipette_by_rank(PipetteRank.first) - assert pip, 'No pipette attached on first mount' - - mount = pip.mount - - assert mount, 'cannot attempt tip pick up, no mount specified' - assert self.pipettes[mount]['has_tip'] is False, \ - f"Tip is already attached to {mount} pipette, " \ - "cannot pick up another" - - await self._pick_up_tip(mount) - - async def _pick_up_tip_second_pipette(self): - """ - Function to pick up tip. It will attempt to pick up a tip in - the current location, and save any offset it might have from the - original position. - """ - pip = self._get_pipette_by_rank(PipetteRank.second) - assert pip, 'No pipette attached on second mount' - - mount = pip.mount - - assert mount, 'cannot attempt tip pick up, no mount specified' - assert self.pipettes[mount]['has_tip'] is False, \ - f"Tip is already attached to {mount} pipette, " \ - "cannot pick up another" - - await self._pick_up_tip(mount) - - async def _trash_first_pipette_tip(self): - first_pip = self._get_pipette_by_rank(PipetteRank.first) - assert first_pip, \ - 'cannot trash tip from first mount, pipette not present' - await self._trash_tip(first_pip.mount) - - async def _trash_second_pipette_tip(self): - second_pip = self._get_pipette_by_rank(PipetteRank.second) - assert second_pip, \ - 'cannot trash tip from first mount, pipette not present' - await self._trash_tip(second_pip.mount) - - @staticmethod - def _create_tiprack_param(position: typing.Dict): - new_dict = {} - for loc, data in position.items(): - for loc_id, values in data.items(): - offset = list(values['offset']) - pos_dict = {'offset': offset, 'locationId': str(loc)} - new_dict[str(loc_id)] = {'pipetteId': str(loc_id), - 'location': pos_dict} - return new_dict - - def format_params(self, next_state: str) -> typing.Dict: - template_dict = {} - if next_state == 'jog': - template_dict['vector'] = [0, 0, 0] - return template_dict - - def _determine_threshold(self, state: CalibrationCheckState) -> Point: - """ - Helper function used to determine the threshold for comparison - based on the state currently being compared and the pipette. - """ - first_pipette = [ - CalibrationCheckState.comparingFirstPipetteHeight, - CalibrationCheckState.comparingFirstPipettePointOne, - CalibrationCheckState.comparingFirstPipettePointTwo, - CalibrationCheckState.comparingFirstPipettePointThree, - ] - if state in first_pipette: - pip = self._get_pipette_by_rank(PipetteRank.first) - else: - pip = self._get_pipette_by_rank(PipetteRank.second) - - pipette_type = '' - if pip and pip.mount: - pipette_type = str(self.pipettes[pip.mount]['name']) - - is_p1000 = pipette_type in ['p1000_single_gen2', 'p1000_single'] - is_p20 = pipette_type in \ - ['p20_single_gen2', 'p10_single', 'p20_multi_gen2', 'p10_multi'] - height_states = [ - CalibrationCheckState.comparingFirstPipetteHeight, - CalibrationCheckState.comparingSecondPipetteHeight] - cross_states = [ - CalibrationCheckState.comparingFirstPipettePointOne, - CalibrationCheckState.comparingFirstPipettePointTwo, - CalibrationCheckState.comparingFirstPipettePointThree, - CalibrationCheckState.comparingSecondPipettePointOne - ] - if is_p1000 and state in cross_states: - return PIPETTE_TOLERANCES['p1000_crosses'] - elif is_p1000 and state in height_states: - return PIPETTE_TOLERANCES['p1000_height'] - elif is_p20 and state in cross_states: - return PIPETTE_TOLERANCES['p20_crosses'] - elif state in cross_states: - return PIPETTE_TOLERANCES['p300_crosses'] - else: - return PIPETTE_TOLERANCES['other_height'] - - def _get_error_source( - self, - comparisons: typing.Dict[CalibrationCheckState, ComparisonStatus], - comparison_state: CalibrationCheckState) -> DeckCalibrationError: - is_second_pip = comparison_state in [ - CalibrationCheckState.comparingSecondPipetteHeight, - CalibrationCheckState.comparingSecondPipettePointOne, - ] - first_pip_keys = [ - CalibrationCheckState.comparingFirstPipetteHeight, - CalibrationCheckState.comparingFirstPipettePointOne, - CalibrationCheckState.comparingFirstPipettePointTwo, - CalibrationCheckState.comparingFirstPipettePointThree, - ] - compared_first = all((k in comparisons) for k in first_pip_keys) - first_pip_steps_passed = compared_first - for key in first_pip_keys: - c = comparisons.get(key, None) - if c and c.exceedsThreshold: - first_pip_steps_passed = False - break - if is_second_pip and first_pip_steps_passed: - return DeckCalibrationError.BAD_INSTRUMENT_OFFSET - elif self.can_distinguish_instr_offset() and not is_second_pip: - return DeckCalibrationError.BAD_DECK_TRANSFORM - else: - return DeckCalibrationError.UNKNOWN - - def get_comparisons_by_step( - self) -> typing.Dict[CalibrationCheckState, ComparisonStatus]: - comparisons: typing.Dict[CalibrationCheckState, ComparisonStatus] = {} - for comparison_state, comp in COMPARISON_STATE_MAP.items(): - ref_pt = self._saved_points.get(getattr(CalibrationCheckState, - comp.reference_state), - None) - - jogged_pt = self._saved_points.get(getattr(CalibrationCheckState, - comparison_state), None) - - threshold_vector = self._determine_threshold(comparison_state) - if (ref_pt is not None and jogged_pt is not None): - diff_magnitude = None - if threshold_vector.z == 0.0: - diff_magnitude = ref_pt._replace(z=0.0).magnitude_to( - jogged_pt._replace(z=0.0)) - elif threshold_vector.x == 0.0 and \ - threshold_vector.y == 0.0: - diff_magnitude = ref_pt._replace( - x=0.0, y=0.0).magnitude_to(jogged_pt._replace( - x=0.0, y=0.0)) - assert diff_magnitude is not None, \ - 'step comparisons must check z or (x and y) magnitude' - - threshold_mag = Point(0, 0, 0).magnitude_to( - threshold_vector) - exceeds = diff_magnitude > threshold_mag - tform_type = DeckCalibrationError.UNKNOWN - - if exceeds: - tform_type = self._get_error_source(comparisons, - comparison_state) - comparisons[getattr(CalibrationCheckState, - comparison_state)] = \ - ComparisonStatus(differenceVector=(jogged_pt - ref_pt), - thresholdVector=threshold_vector, - exceedsThreshold=exceeds, - transformType=str(tform_type)) - return comparisons - - async def _register_point_first_pipette(self): - first_pip = self._get_pipette_by_rank(PipetteRank.first) - assert first_pip, 'cannot register point for missing first pipette' - buffer = Point(0, 0, 0) - if self.current_state_name ==\ - CalibrationCheckState.comparingFirstPipetteHeight: - buffer = HEIGHT_SAFETY_BUFFER - current_point = self.hardware.gantry_position( - first_pip.mount, critical_point=first_pip.critical_point) - self._saved_points[getattr(CalibrationCheckState, - self.current_state_name)] = \ - await current_point + buffer - - async def _register_point_second_pipette(self): - second_pip = self._get_pipette_by_rank(PipetteRank.second) - assert second_pip, 'cannot register point for missing second pipette' - buffer = Point(0, 0, 0) - if self.current_state_name ==\ - CalibrationCheckState.comparingSecondPipetteHeight: - buffer = HEIGHT_SAFETY_BUFFER - current_point = self.hardware.gantry_position( - second_pip.mount, critical_point=second_pip.critical_point - ) - self._saved_points[getattr(CalibrationCheckState, - self.current_state_name)] = \ - await current_point + buffer - - async def _move_first_pipette(self): - first_pip = self._get_pipette_by_rank(PipetteRank.first) - assert first_pip, \ - 'cannot move pipette on first mount, pipette not present' - loc_to_move = Location(getattr(self._moves, - self.current_state_name).position, - None) - - saved_z_allowlist = \ - [CalibrationCheckState.joggingFirstPipetteToPointOne, - CalibrationCheckState.joggingFirstPipetteToPointTwo, - CalibrationCheckState.joggingFirstPipetteToPointThree] - if self.current_state_name in saved_z_allowlist: - saved_height =\ - self._saved_points[getattr(CalibrationCheckState, - 'comparingFirstPipetteHeight')] - z_point = \ - saved_height + self._initial_z_offset - HEIGHT_SAFETY_BUFFER - updated_point = loc_to_move.point + z_point._replace(x=0.0, y=0.0) - loc_to_move = Location(updated_point, None) - await self._move(first_pip.mount, loc_to_move) - await self._register_point_first_pipette() - - async def _move_second_pipette(self): - second_pip = self._get_pipette_by_rank(PipetteRank.second) - assert second_pip, \ - 'cannot move pipette on second mount, pipette not present' - loc_to_move = Location(getattr(self._moves, - self.current_state_name).position, - None) - if self.current_state_name ==\ - CalibrationCheckState.joggingSecondPipetteToPointOne: - saved_height =\ - self._saved_points[getattr(CalibrationCheckState, - 'comparingSecondPipetteHeight')] - z_point = \ - saved_height + self._initial_z_offset - HEIGHT_SAFETY_BUFFER - updated_point = loc_to_move.point + z_point._replace(x=0.0, y=0.0) - loc_to_move = Location(updated_point, None) - await self._move(second_pip.mount, loc_to_move) - await self._register_point_second_pipette() - - async def _jog_first_pipette(self, vector: OffsetVector): - first_pip = self._get_pipette_by_rank(PipetteRank.first) - assert first_pip, \ - 'cannot jog pipette on first mount, pipette not present' - await super(self.__class__, self)._jog(first_pip.mount, Point(*vector)) - - async def _jog_second_pipette(self, vector: OffsetVector): - second_pip = self._get_pipette_by_rank(PipetteRank.second) - assert second_pip, \ - 'cannot jog pipette on second mount, pipette not present' - await super(self.__class__, self)._jog(second_pip.mount, - Point(*vector)) - - async def _return_first_tip(self): - first_pip = self._get_pipette_by_rank(PipetteRank.first) - assert first_pip, \ - 'cannot drop tip on first mount, pipette not present' - mount = first_pip.mount - z_value = float(self.pipettes[mount]['tip_length']) * 0.5 - state_name = CalibrationCheckState.inspectingFirstTip - - return_pt = self._saved_points[getattr(CalibrationCheckState, - state_name)] - account_for_tip = return_pt - Point(0, 0, z_value) - loc = Location(account_for_tip, None) - await self._move(first_pip.mount, loc) - await self._drop_tip(first_pip.mount) - - async def _return_second_tip(self): - second_pip = self._get_pipette_by_rank(PipetteRank.second) - assert second_pip, \ - 'cannot drop tip on second mount, pipette not present' - mount = second_pip.mount - z_value = float(self.pipettes[mount]['tip_length']) * 0.5 - state_name = CalibrationCheckState.inspectingSecondTip - return_pt = self._saved_points[getattr(CalibrationCheckState, - state_name)] - account_for_tip = return_pt - Point(0, 0, z_value) - loc = Location(account_for_tip, None) - await self._move(second_pip.mount, loc) - await self._drop_tip(second_pip.mount) diff --git a/robot-server/robot_server/robot/calibration/check/state_machine.py b/robot-server/robot_server/robot/calibration/check/state_machine.py new file mode 100644 index 00000000000..b5a8999367c --- /dev/null +++ b/robot-server/robot_server/robot/calibration/check/state_machine.py @@ -0,0 +1,75 @@ +from typing import Dict + +from robot_server.service.session.models.command import ( + CommandDefinition, CalibrationCommand, + CheckCalibrationCommand, DeckCalibrationCommand) +from robot_server.robot.calibration.util import ( + SimpleStateMachine, StateTransitionError) + +from .constants import CalibrationCheckState as State + + +CALIBRATION_CHECK_TRANSITIONS: Dict[State, Dict[CommandDefinition, State]] = { + State.sessionStarted: { + CalibrationCommand.load_labware: State.labwareLoaded + }, + State.labwareLoaded: { + CalibrationCommand.move_to_tip_rack: State.preparingPipette + }, + State.preparingPipette: { + CalibrationCommand.jog: State.preparingPipette, + CalibrationCommand.pick_up_tip: State.inspectingTip, + }, + State.inspectingTip: { + CalibrationCommand.invalidate_tip: State.preparingPipette, + CalibrationCommand.move_to_deck: State.comparingHeight, + }, + State.comparingHeight: { + CalibrationCommand.jog: State.comparingHeight, + CheckCalibrationCommand.compare_point: State.comparingHeight, + CalibrationCommand.move_to_point_one: State.comparingPointOne, + }, + State.comparingPointOne: { + CalibrationCommand.jog: State.comparingPointOne, + CheckCalibrationCommand.compare_point: State.comparingPointOne, + DeckCalibrationCommand.move_to_point_two: State.comparingPointTwo, + CalibrationCommand.move_to_tip_rack: State.returningTip + }, + State.comparingPointTwo: { + CalibrationCommand.jog: State.comparingPointTwo, + CheckCalibrationCommand.compare_point: State.comparingPointTwo, + DeckCalibrationCommand.move_to_point_three: State.comparingPointThree + }, + State.comparingPointThree: { + CalibrationCommand.jog: State.comparingPointThree, + CheckCalibrationCommand.compare_point: State.comparingPointThree, + CalibrationCommand.move_to_tip_rack: State.returningTip, + }, + State.returningTip: { + CheckCalibrationCommand.return_tip: State.returningTip, + CheckCalibrationCommand.transition: State.resultsSummary, + CheckCalibrationCommand.switch_pipette: State.labwareLoaded + }, + State.badCalibrationData: { + CalibrationCommand.move_to_tip_rack: State.badCalibrationData, + CheckCalibrationCommand.return_tip: State.WILDCARD, + }, + State.WILDCARD: { + CalibrationCommand.exit: State.sessionExited + } +} + + +class CalibrationCheckStateMachine: + def __init__(self): + self._state_machine = SimpleStateMachine( + states=set(s for s in State), + transitions=CALIBRATION_CHECK_TRANSITIONS + ) + + def get_next_state(self, from_state: State, command: CommandDefinition): + next_state = self._state_machine.get_next_state(from_state, command) + if next_state: + return next_state + else: + raise StateTransitionError(command, from_state) diff --git a/robot-server/robot_server/robot/calibration/check/user_flow.py b/robot-server/robot_server/robot/calibration/check/user_flow.py new file mode 100644 index 00000000000..e13fc87078b --- /dev/null +++ b/robot-server/robot_server/robot/calibration/check/user_flow.py @@ -0,0 +1,653 @@ +import logging +from typing import ( + List, Optional, Tuple, Awaitable, + Callable, Dict, Any, TYPE_CHECKING) + +from opentrons.calibration_storage import get +from opentrons.calibration_storage.types import TipLengthCalNotFound +from opentrons.types import Mount, Point, Location +from opentrons.hardware_control import ThreadManager, CriticalPoint, Pipette +from opentrons.protocol_api import labware +from opentrons.config import feature_flags as ff +from opentrons.protocols.geometry.deck import Deck + +from robot_server.robot.calibration.constants import ( + SHORT_TRASH_DECK, STANDARD_DECK, MOVE_TO_DECK_SAFETY_BUFFER, + MOVE_TO_TIP_RACK_SAFETY_BUFFER, JOG_TO_DECK_SLOT, + TIP_RACK_LOOKUP_BY_MAX_VOL) +import robot_server.robot.calibration.util as uf +from robot_server.robot.calibration.helper_classes import ( + DeckCalibrationError, PipetteRank, PipetteInfo, + RequiredLabware) + +from robot_server.service.session.models.command import ( + CalibrationCommand, CheckCalibrationCommand, DeckCalibrationCommand) +from robot_server.service.errors import RobotServerError + +from .util import ( + PointTypes, ReferencePoints, + ComparisonMap, ComparisonStatePerPipette) +from .models import ComparisonStatus, CheckAttachedPipette +from .state_machine import CalibrationCheckStateMachine + +from .constants import (PIPETTE_TOLERANCES, + P1000_OK_TIP_PICK_UP_VECTOR, + DEFAULT_OK_TIP_PICK_UP_VECTOR, + MOVE_POINT_STATE_MAP, + CalibrationCheckState as State, + TIPRACK_SLOT) +from ..errors import CalibrationError + +if TYPE_CHECKING: + from opentrons_shared_data.labware import LabwareDefinition + +MODULE_LOG = logging.getLogger(__name__) + +""" +A collection of functions that allow a consumer to determine the health +of the current calibration saved on a robot. +""" + +# TODO: BC 2020-07-08: type all command logic here with actual Model type +COMMAND_HANDLER = Callable[..., Awaitable] + +COMMAND_MAP = Dict[str, COMMAND_HANDLER] + + +class CheckCalibrationUserFlow: + def __init__( + self, hardware: 'ThreadManager', + tip_rack_defs: Optional[List['LabwareDefinition']] = None): + self._hardware = hardware + self._state_machine = CalibrationCheckStateMachine() + self._current_state = State.sessionStarted + self._reference_points = ReferencePoints( + tip=PointTypes(), + height=PointTypes(), + one=PointTypes(), + two=PointTypes(), + three=PointTypes() + ) + self._comparison_map = ComparisonStatePerPipette( + first=ComparisonMap(), + second=ComparisonMap() + ) + + self._active_pipette, self._pip_info = self._select_starting_pipette() + self._mount = self._active_pipette.mount + self._tip_origin_pt: Optional[Point] = None + self._z_height_reference: Optional[float] = None + + deck_load_name = SHORT_TRASH_DECK if ff.short_fixed_trash() \ + else STANDARD_DECK + self._deck = Deck(load_name=deck_load_name) + self._tip_racks: Optional[List['LabwareDefinition']] = tip_rack_defs + self._active_tiprack = self._load_active_tiprack() + + self._command_map: COMMAND_MAP = { + CalibrationCommand.load_labware: self.transition, + CalibrationCommand.jog: self.jog, + CalibrationCommand.pick_up_tip: self.pick_up_tip, + CalibrationCommand.invalidate_tip: self.invalidate_tip, + CheckCalibrationCommand.compare_point: self.update_comparison_map, + CalibrationCommand.move_to_tip_rack: self.move_to_tip_rack, + CalibrationCommand.move_to_deck: self.move_to_deck, + CalibrationCommand.move_to_point_one: self.move_to_point_one, + DeckCalibrationCommand.move_to_point_two: self.move_to_point_two, + DeckCalibrationCommand.move_to_point_three: self.move_to_point_three, # noqa: E501 + CheckCalibrationCommand.switch_pipette: self.change_active_pipette, + CheckCalibrationCommand.return_tip: self.return_tip, + CheckCalibrationCommand.transition: self.transition, + CalibrationCommand.exit: self.exit_session, + } + + @property + def deck(self) -> Deck: + return self._deck + + @property + def hardware(self) -> ThreadManager: + return self._hardware + + @property + def tip_origin(self) -> Point: + if self._tip_origin_pt: + return self._tip_origin_pt + else: + return self.active_tiprack.wells()[0].top().point +\ + MOVE_TO_TIP_RACK_SAFETY_BUFFER + + @tip_origin.setter + def tip_origin(self, new_val: Optional[Point]): + self._tip_origin_pt = new_val + + def reset_tip_origin(self): + self._tip_origin_pt = None + + @property + def current_state(self) -> State: + return self._current_state + + @property + def mount(self) -> Mount: + return self.active_pipette.mount + + @property + def active_pipette(self) -> PipetteInfo: + return self._active_pipette + + @property + def comparison_map(self) -> ComparisonStatePerPipette: + return self._comparison_map + + @property + def active_tiprack(self) -> labware.Labware: + return self._active_tiprack + + @property + def hw_pipette(self) -> Pipette: + return self._get_hw_pipettes()[0] + + async def transition(self): + pass + + async def change_active_pipette(self): + second_pip =\ + self._get_pipette_by_rank(PipetteRank.second) + if not second_pip: + raise RobotServerError( + definition=CalibrationError.UNMET_STATE_TRANSITION_REQ, + state=self._current_state, + handler="change_active_pipette", + condition="second pipette") + self._active_pipette = second_pip + del self._deck[TIPRACK_SLOT] + self._active_tiprack = self._load_active_tiprack() + + def _set_current_state(self, to_state: State): + self._current_state = to_state + + @property + def critical_point_override(self) -> Optional[CriticalPoint]: + return (CriticalPoint.FRONT_NOZZLE if + self.hw_pipette.config.channels == 8 else None) + + async def handle_command(self, + name: Any, + data: Dict[Any, Any]): + """ + Handle a client command + + :param name: Name of the command + :param data: Data supplied in command + :return: None + """ + next_state = self._state_machine.get_next_state(self._current_state, + name) + + handler = self._command_map.get(name) + if handler is not None: + await handler(**data) + self._set_current_state(next_state) + MODULE_LOG.debug( + f'CalibrationCheckUserFlow handled command {name}, transitioned' + f'from {self._current_state} to {next_state}') + + def get_required_labware(self) -> List[RequiredLabware]: + slots = self._deck.get_non_fixture_slots() + lw_by_slot = {s: self._deck[s] for s in slots if self._deck[s]} + return [ + RequiredLabware.from_lw(lw, s) # type: ignore + for s, lw in lw_by_slot.items()] + + def get_active_tiprack(self) -> RequiredLabware: + return RequiredLabware.from_lw(self.active_tiprack) + + def _select_starting_pipette( + self) -> Tuple[PipetteInfo, List[PipetteInfo]]: + """ + Select pipette for calibration based on: + 1: larger max volume + 2: single-channel over multi + 3: right mount over left + """ + if not any(self._hardware._attached_instruments.values()): + raise RobotServerError( + definition=CalibrationError.NO_PIPETTE_ATTACHED, + flow='Calibration Health Check') + pips = {m: p for m, p in self._hardware._attached_instruments.items() + if p} + if len(pips) == 1: + for mount, pip in pips.items(): + info = PipetteInfo( + channels=pip.config.channels, + rank=PipetteRank.first, + max_volume=pip.config.max_volume, + mount=mount) + return info, [info] + + right_pip = pips[Mount.RIGHT] + left_pip = pips[Mount.LEFT] + + r_info = PipetteInfo( + channels=right_pip.config.channels, + max_volume=right_pip.config.max_volume, + rank=PipetteRank.first, + mount=Mount.RIGHT) + l_info = PipetteInfo( + channels=left_pip.config.channels, + max_volume=left_pip.config.max_volume, + rank=PipetteRank.first, + mount=Mount.LEFT) + if left_pip.config.max_volume > right_pip.config.max_volume or \ + right_pip.config.channels > left_pip.config.channels: + r_info.rank = PipetteRank.second + return l_info, [l_info, r_info] + else: + l_info.rank = PipetteRank.second + return r_info, [r_info, l_info] + + async def get_current_point( + self, + critical_point: CriticalPoint = None) -> Point: + return await self._hardware.gantry_position(self.mount, + critical_point) + + def _get_pipette_by_rank(self, rank: PipetteRank) -> \ + Optional[PipetteInfo]: + try: + return next(p for p in self._pip_info + if p.rank == rank) + except StopIteration: + return None + + def can_distinguish_instr_offset(self): + """ + TODO (lc 10-20-2020) we can now always distinguish + between instrument offset and mount offset. We + should remove this use-case during the cal check + refactor. + """ + first_pip = self._get_pipette_by_rank(PipetteRank.first) + return first_pip and first_pip.mount != Mount.LEFT + + def _is_checking_both_mounts(self): + return len(self._pip_info) == 2 + + def _get_volume_from_tiprack_def( + self, tip_rack_def: 'LabwareDefinition') -> float: + first_well = tip_rack_def['wells']['A1'] + return float(first_well['totalLiquidVolume']) + + def _load_active_tiprack(self) -> labware.Labware: + """ + load onto the deck the default opentrons tip rack labware for this + pipette and return the tip rack labware. If tip_rack_def is supplied, + load specific tip rack from def onto the deck and return the labware. + + TODO (lc 10-20-2020) we should load the tipracks from a pipette + offset before trying to load from default. + """ + active_max_vol = self.active_pipette.max_volume + if self._tip_racks: + for tip_rack_def in self._tip_racks: + tiprack_vol = self._get_volume_from_tiprack_def(tip_rack_def) + if active_max_vol == tiprack_vol: + tr_lw = labware.load_from_definition( + tip_rack_def, + self._deck.position_for(TIPRACK_SLOT)) + else: + tr_load_name =\ + TIP_RACK_LOOKUP_BY_MAX_VOL[str(active_max_vol)].load_name + tr_lw = labware.load(tr_load_name, + self._deck.position_for(TIPRACK_SLOT)) + self._deck[TIPRACK_SLOT] = tr_lw + return tr_lw + + def _get_hw_pipettes(self) -> List[Pipette]: + # Return a list of instruments, ordered with the active pipette first + active_mount = self.active_pipette.mount + hw_instruments = self._hardware._attached_instruments + if active_mount == Mount.RIGHT: + other_mount = Mount.LEFT + else: + other_mount = Mount.RIGHT + if self._is_checking_both_mounts(): + return [hw_instruments[active_mount], hw_instruments[other_mount]] + else: + return [hw_instruments[active_mount]] + + def _get_ordered_info_pipettes(self) -> List[PipetteInfo]: + active_rank = self.active_pipette.rank + if active_rank == PipetteRank.first: + other_rank = PipetteRank.second + else: + other_rank = PipetteRank.first + pip1 = self._get_pipette_by_rank(active_rank) + assert pip1 + if self._is_checking_both_mounts(): + pip2 = self._get_pipette_by_rank(other_rank) + assert pip2 + return [pip1, pip2] + else: + return [pip1] + + def get_instruments(self) -> List[CheckAttachedPipette]: + """ + Public property to help format the current pipettes + being used for a given session for the client. + """ + hw_pips = self._get_hw_pipettes() + info_pips = self._get_ordered_info_pipettes() + return [ + CheckAttachedPipette( # type: ignore[call-arg] + model=hw_pip.model, + name=hw_pip.name, + tipLength=hw_pip.config.tip_length, + rank=str(info_pip.rank), + mount=str(self.mount), + serial=hw_pip.pipette_id) # type: ignore[arg-type] + for hw_pip, info_pip in zip(hw_pips, info_pips)] + + def get_active_pipette(self) -> CheckAttachedPipette: + # TODO(mc, 2020-09-17): type of pipette_id does not match expected + # type of AttachedPipette.serial + assert self.hw_pipette + assert self.active_pipette + return CheckAttachedPipette( # type: ignore[call-arg] + model=self.hw_pipette.model, + name=self.hw_pipette.name, + tipLength=self.hw_pipette.config.tip_length, + rank=str(self.active_pipette.rank), + mount=str(self.mount), + serial=self.hw_pipette.pipette_id) # type: ignore[arg-type] + + async def _is_tip_pick_up_dangerous(self): + """ + Function to determine whether jogged to pick up tip position is + outside of the safe threshold for conducting the rest of the check. + """ + ref_pt, jogged_pt = self._get_reference_points_by_state() + + ref_pt_no_safety = ref_pt - MOVE_TO_TIP_RACK_SAFETY_BUFFER + threshold_vector = DEFAULT_OK_TIP_PICK_UP_VECTOR + pip_model = self._get_hw_pipettes()[0].model + if str(pip_model).startswith('p1000'): + threshold_vector = P1000_OK_TIP_PICK_UP_VECTOR + xyThresholdMag = Point(0, 0, 0).magnitude_to( + threshold_vector._replace(z=0)) + zThresholdMag = Point(0, 0, 0).magnitude_to( + threshold_vector._replace(x=0, y=0)) + xyDiffMag = ref_pt_no_safety._replace(z=0).magnitude_to( + jogged_pt._replace(z=0)) + zDiffMag = ref_pt_no_safety._replace(x=0, y=0).magnitude_to( + jogged_pt._replace(x=0, y=0)) + return xyDiffMag > xyThresholdMag or zDiffMag > zThresholdMag + + async def check_tip_threshold(self): + dangerous = await self._is_tip_pick_up_dangerous() + if dangerous: + self._set_current_state(State.badCalibrationData) + else: + self._set_current_state(State.inspectingTip) + + def _determine_threshold(self) -> Point: + """ + Helper function used to determine the threshold for comparison + based on the state currently being compared and the pipette. + """ + active_pipette = self.active_pipette + + pipette_type = '' + if active_pipette and active_pipette.mount: + pipette_type = str(self._get_hw_pipettes()[0].name) + + is_p1000 = pipette_type in ['p1000_single_gen2', 'p1000_single'] + is_p20 = pipette_type in \ + ['p20_single_gen2', 'p10_single', 'p20_multi_gen2', 'p10_multi'] + cross_states = [ + State.comparingPointOne, + State.comparingPointTwo, + State.comparingPointThree] + if is_p1000 and self.current_state in cross_states: + return PIPETTE_TOLERANCES['p1000_crosses'] + elif is_p1000 and self.current_state == State.comparingHeight: + return PIPETTE_TOLERANCES['p1000_height'] + elif is_p20 and self.current_state in cross_states: + return PIPETTE_TOLERANCES['p20_crosses'] + elif self.current_state in cross_states: + return PIPETTE_TOLERANCES['p300_crosses'] + else: + return PIPETTE_TOLERANCES['other_height'] + + def _get_error_source( + self, + comparisons: ComparisonStatePerPipette + ) -> DeckCalibrationError: + """ + TODO(lc 10-20-2020), this needs to be refactored to fit + the current system. Error sources are no longer muddled + by mount offset or mounts. + """ + is_second_pip = self.active_pipette.rank is PipetteRank.second + compare_states = [ + State.comparingHeight, + State.comparingPointOne, + State.comparingPointTwo, + State.comparingPointThree, + ] + compared_first =\ + all(hasattr(comparisons.first, k.name) for k in compare_states) + first_pip_steps_passed = compared_first + for key in compare_states: + c = getattr(comparisons.first, key.name) + if c and c.exceedsThreshold: + first_pip_steps_passed = False + break + if is_second_pip and first_pip_steps_passed: + return DeckCalibrationError.BAD_INSTRUMENT_OFFSET + elif self.can_distinguish_instr_offset() and not is_second_pip: + return DeckCalibrationError.BAD_DECK_TRANSFORM + else: + return DeckCalibrationError.UNKNOWN + + def _update_compare_status_by_rank( + self, rank: PipetteRank, + status: ComparisonStatus) -> ComparisonMap: + intermediate_map = getattr(self._comparison_map, rank.name) + intermediate_map.set_value(self.current_state.name, status) + return intermediate_map + + async def update_comparison_map(self): + ref_pt, jogged_pt = self._get_reference_points_by_state() + rank = self.active_pipette.rank + threshold_vector = self._determine_threshold() + MODULE_LOG.info(f"State {self.current_state}") + MODULE_LOG.info(f"Reference pts {ref_pt} {jogged_pt}") + if (ref_pt is not None and jogged_pt is not None): + diff_magnitude = None + if threshold_vector.z == 0.0: + diff_magnitude = ref_pt._replace(z=0.0).magnitude_to( + jogged_pt._replace(z=0.0)) + elif threshold_vector.x == 0.0 and \ + threshold_vector.y == 0.0: + diff_magnitude = ref_pt._replace( + x=0.0, y=0.0).magnitude_to(jogged_pt._replace( + x=0.0, y=0.0)) + assert diff_magnitude is not None, \ + 'step comparisons must check z or (x and y) magnitude' + + threshold_mag = Point(0, 0, 0).magnitude_to( + threshold_vector) + exceeds = diff_magnitude > threshold_mag + tform_type = DeckCalibrationError.UNKNOWN + + if exceeds: + tform_type = self._get_error_source(self._comparison_map) + + status = ComparisonStatus(differenceVector=(jogged_pt - ref_pt), + thresholdVector=threshold_vector, + exceedsThreshold=exceeds, + transformType=str(tform_type)) + intermediate_map =\ + self._update_compare_status_by_rank(rank, status) + self._comparison_map.set_value(rank.name, intermediate_map) + + def _get_reference_points_by_state(self): + saved_points = self._reference_points + if self.current_state == State.preparingPipette: + return saved_points.tip.initial_point,\ + saved_points.tip.final_point + elif self.current_state == State.comparingHeight: + return saved_points.height.initial_point,\ + saved_points.height.final_point + elif self.current_state == State.comparingPointOne: + return saved_points.one.initial_point,\ + saved_points.one.final_point + elif self.current_state == State.comparingPointTwo: + return saved_points.two.initial_point,\ + saved_points.two.final_point + elif self.current_state == State.comparingPointThree: + return saved_points.three.initial_point,\ + saved_points.three.final_point + + async def register_initial_point(self): + """ + Here we will register the initial and final + points to the current point before jogging + in the instance that a user doesn't jog at + all. + """ + critical_point = self.critical_point_override + current_point = \ + await self.get_current_point(critical_point) + if self.current_state == State.labwareLoaded: + self._reference_points.tip.initial_point = \ + current_point + self._reference_points.tip.final_point = \ + current_point + elif self.current_state == State.inspectingTip: + self._reference_points.height.initial_point = \ + current_point + self._reference_points.height.final_point = \ + current_point + elif self.current_state == State.comparingHeight: + self._reference_points.one.initial_point = \ + current_point + self._reference_points.one.final_point = \ + current_point + elif self.current_state == State.comparingPointOne: + self._reference_points.two.initial_point = \ + current_point + self._reference_points.two.final_point = \ + current_point + elif self.current_state == State.comparingPointTwo: + self._reference_points.three.initial_point = \ + current_point + self._reference_points.three.final_point = \ + current_point + + async def register_final_point(self): + critical_point = self.critical_point_override + current_point = \ + await self.get_current_point(critical_point) + if self.current_state == State.preparingPipette: + self._reference_points.tip.final_point = \ + current_point + elif self.current_state == State.comparingHeight: + self._reference_points.height.final_point = \ + current_point + MOVE_TO_DECK_SAFETY_BUFFER + self._z_height_reference = current_point.z + elif self.current_state == State.comparingPointOne: + self._reference_points.one.final_point = \ + current_point + elif self.current_state == State.comparingPointTwo: + self._reference_points.two.final_point = \ + current_point + elif self.current_state == State.comparingPointThree: + self._reference_points.three.final_point = \ + current_point + + def _get_tip_length(self) -> float: + pip_id = self.hw_pipette.pipette_id + assert pip_id + assert self.active_tiprack + try: + return get.load_tip_length_calibration( + pip_id, + self.active_tiprack._implementation.get_definition(), + '')['tipLength'] + except TipLengthCalNotFound: + tip_overlap = self.hw_pipette.config.tip_overlap.get( + self.active_tiprack.uri, + self.hw_pipette.config.tip_overlap['default']) + tip_length = self.active_tiprack.tip_length + return tip_length - tip_overlap + + async def move_to_tip_rack(self): + if not self.active_tiprack: + raise RobotServerError( + definition=CalibrationError.UNMET_STATE_TRANSITION_REQ, + state=self.current_state, + handler="move_to_tip_rack", + condition="active tiprack") + if self.current_state == State.labwareLoaded: + MODULE_LOG.debug("homing plunger") + await self.hardware.home_plunger(self.mount) + await self._move(Location(self.tip_origin, None)) + await self.register_initial_point() + await self.register_final_point() + + async def move_to_deck(self): + deck_pt = self._deck.get_slot_center(JOG_TO_DECK_SLOT) + ydim = self._deck.get_slot_definition( + JOG_TO_DECK_SLOT)['boundingBox']['yDimension'] + new_pt = deck_pt - Point(0, (ydim/2), deck_pt.z) + \ + MOVE_TO_DECK_SAFETY_BUFFER + to_loc = Location(new_pt, None) + await self._move(to_loc) + await self.register_initial_point() + + def _get_move_to_point_loc_by_state(self) -> Location: + assert self._z_height_reference is not None, \ + "comparePoint has not been called yet" + pt_id = MOVE_POINT_STATE_MAP[self._current_state] + coords = self._deck.get_calibration_position(pt_id).position + loc = Location(Point(*coords), None) + return loc.move(point=Point(0, 0, self._z_height_reference)) + + async def move_to_point_one(self): + await self._move(self._get_move_to_point_loc_by_state()) + await self.register_initial_point() + + async def move_to_point_two(self): + await self._move(self._get_move_to_point_loc_by_state()) + await self.register_initial_point() + + async def move_to_point_three(self): + await self._move(self._get_move_to_point_loc_by_state()) + await self.register_initial_point() + + async def jog(self, vector): + await self._hardware.move_rel(mount=self.mount, + delta=Point(*vector)) + await self.register_final_point() + + async def pick_up_tip(self): + await uf.pick_up_tip(self, tip_length=self._get_tip_length()) + await self.check_tip_threshold() + + async def invalidate_tip(self): + await uf.invalidate_tip(self) + + async def return_tip(self): + await uf.return_tip(self, tip_length=self._get_tip_length()) + + async def _move(self, to_loc: Location): + await uf.move(self, to_loc) + + async def exit_session(self): + if self.hw_pipette.has_tip: + await self.move_to_tip_rack() + await self.return_tip() + await self._hardware.home() diff --git a/robot-server/robot_server/robot/calibration/check/util.py b/robot-server/robot_server/robot/calibration/check/util.py index 251ac96298b..74e2d9f7c84 100644 --- a/robot-server/robot_server/robot/calibration/check/util.py +++ b/robot-server/robot_server/robot/calibration/check/util.py @@ -1,226 +1,50 @@ -import logging -import enum -from typing import (Dict, Union, Awaitable, Optional, Callable, Set, Any, List) +from dataclasses import dataclass +from typing import Optional -log = logging.getLogger(__name__) +from opentrons.types import Point - -# Transition callbacks pass through all params from trigger call -TransitionCallback = Union[ - Callable[..., Awaitable[Any]], - List[Callable[..., Awaitable[Any]]]] - -# Condition callbacks pass through all params from trigger call, -# and must return bool -ConditionCallback = Callable[..., Awaitable[bool]] +from .models import ComparisonStatus WILDCARD = '*' -class State: - def __init__(self, name: str): - self._name = name - - @property - def name(self) -> str: - return self._name - - -class Transition: - def __init__(self, - from_state: str, - to_state: str, - before: TransitionCallback = None, - after: TransitionCallback = None, - condition: ConditionCallback = None): - self.from_state = from_state - self.to_state = to_state - self.before = before - self.after = after - self.condition = condition - - async def execute(self, set_current_state, *args, **kwargs): - if self.condition and not await self.condition(*args, **kwargs): - return False - if isinstance(self.before, list): - for b in self.before: - await b(*args, **kwargs) - elif self.before: - await self.before(*args, **kwargs) - set_current_state(self.to_state) - if isinstance(self.after, list): - for a in self.after: - await a(*args, **kwargs) - elif self.after: - await self.after(*args, **kwargs) - return True - - -class StateMachineError(Exception): - def __init__(self, msg: str): - self.msg = msg or '' - super().__init__() - - def __repr__(self): - return f'<{str(self)}>' - - def __str__(self): - return f'StateMachineError: {self.msg}' - - -def enum_to_set(e) -> set: - return set(item.name for item in e) - - -StateParams = Union[str, Dict[str, Any]] - - -class TransitionKeys(enum.Enum): - from_state = enum.auto() - to_state = enum.auto() - before = enum.auto() - after = enum.auto() - condition = enum.auto() - - -class CallbackKeys(enum.Enum): - before = enum.auto() - after = enum.auto() - condition = enum.auto() - - -TransitionKwargs = Dict[str, Any] - - -class StateMachine: - def __init__(self, - states: List[StateParams], - transitions: List[TransitionKwargs], - initial_state: str): - """ - Construct a state machine - - :param states: a collection of available states - :param transitions: the transitions from state to state - :param initial_state: the starting state - """ - self._states: Dict[str, State] = {} - self._current_state: Optional[State] = None - self._events: Dict[str, Dict[str, List[Transition]]] = {} - for params in states: - if isinstance(params, dict): - self.add_state(**params) - else: - self.add_state(name=params) - self._set_current_state(initial_state) - for t in transitions: - self.add_transition(**t) - - def _get_state_by_name(self, name: str) -> Optional[State]: - return self._states.get(name) - - def _set_current_state(self, state_name: str): - """ - This method should only be called implicitly via transitions, or - inside a test - """ - goal_state = self._get_state_by_name(state_name) - assert goal_state, f"state {state_name} doesn't exist in machine" - self._current_state = goal_state +@dataclass +class PointTypes: + initial_point: Point = Point(0, 0, 0) + final_point: Point = Point(0, 0, 0) - async def trigger_transition(self, trigger, *args, **kwargs): - """ - Trigger a state transition - :param trigger: The name of the transition - :param args: arg list passed to transition callbacks - :param kwargs: keyword args passed to transition callbacks - :return: None - """ - log.debug(f"trigger_transition for {trigger} " - f"in {self.current_state_name}") - events = self._events.get(trigger, {}) - if events and WILDCARD not in events and \ - self.current_state_name not in events: - raise StateMachineError(f'cannot trigger event {trigger}' - f' from state {self.current_state_name}') - try: - from_state = WILDCARD if WILDCARD in events \ - else self.current_state_name - for transition in events[from_state]: - if await transition.execute(self._set_current_state, - *args, **kwargs): - break - except Exception as e: - log.exception(f"exception raised processing trigger {trigger}" - f" in state {self.current_state_name}") - raise StateMachineError(f'event {trigger} failed to transition ' - f'from {self.current_state_name}: ' - f'{str(e)}') +@dataclass +class ReferencePoints: + tip: PointTypes + height: PointTypes + one: PointTypes + two: PointTypes + three: PointTypes - def _get_cb(self, method_name: Optional[str]): - return getattr(self, method_name) if method_name else None - def _bind_cb_kwarg(self, key, value): - cb_allowlist = ['before', 'after'] - if key in enum_to_set(CallbackKeys): - if key in cb_allowlist and isinstance(value, list): - to_return = [] - for m in value: - to_return.append(self._get_cb(m)) - else: - to_return = self._get_cb(value) - return to_return - return value +@dataclass +class ComparisonMap: + comparingHeight: Optional[ComparisonStatus] = None + comparingPointOne: Optional[ComparisonStatus] = None + comparingPointTwo: Optional[ComparisonStatus] = None + comparingPointThree: Optional[ComparisonStatus] = None - def add_state(self, *args, **kwargs): - state = State(*args, **kwargs) - self._states[state.name] = state + def set_value(self, name: str, value: ComparisonStatus): + setattr(self, name, value) - def add_transition(self, - trigger: str, - from_state: str, - to_state: str, - **kwargs): - """ - Create a transition from state to state - :param trigger: name of the trigger - :param from_state: name of source state - :param to_state: name of target state - :param kwargs: extra arguments - :return: None - """ - if from_state is not WILDCARD: - assert self._get_state_by_name(from_state),\ - f"state {from_state} doesn't exist in machine" - assert self._get_state_by_name(to_state),\ - f"state {to_state} doesn't exist in machine" - bound_kwargs = { - k: self._bind_cb_kwarg(k, v) for k, v in kwargs.items() - } - self._events[trigger] = { - **self._events.get(trigger, {}), - from_state: [ - *self._events.get(trigger, {}).get(from_state, []), - Transition(from_state=from_state, - to_state=to_state, - **bound_kwargs) - ] - } +@dataclass +class ComparisonStatePerPipette: + first: ComparisonMap + second: ComparisonMap - @property - def current_state(self) -> Optional[State]: - return self._current_state + def set_value(self, name: str, value: ComparisonMap): + setattr(self, name, value) - @property - def current_state_name(self) -> str: - return self._current_state.name if self._current_state else "" - def get_potential_triggers(self) -> Set[str]: - """Return a set of currently available triggers""" - potential_triggers = set() - for trigger, events in self._events.items(): - if WILDCARD in events or self.current_state_name in events: - potential_triggers.add(trigger) - return potential_triggers +REFERENCE_POINT_MAP = { + '1BLC': 'one', + '3BRC': 'two', + '7TLC': 'three' +} diff --git a/robot-server/robot_server/robot/calibration/constants.py b/robot-server/robot_server/robot/calibration/constants.py index 3267b86ebb4..e18ee8cd30d 100644 --- a/robot-server/robot_server/robot/calibration/constants.py +++ b/robot-server/robot_server/robot/calibration/constants.py @@ -25,7 +25,7 @@ FILTERTIPRACK_1000 = _lw_fmt.format(_filtertiprack, 1000) -ALLOWED_SESSIONS = {'check'} +JOG_TO_DECK_SLOT: Final = '5' @dataclass diff --git a/robot-server/robot_server/robot/calibration/deck/constants.py b/robot-server/robot_server/robot/calibration/deck/constants.py index 380dd677218..2d43a8a9f97 100644 --- a/robot-server/robot_server/robot/calibration/deck/constants.py +++ b/robot-server/robot_server/robot/calibration/deck/constants.py @@ -23,7 +23,6 @@ class DeckCalibrationState(str, Enum): WILDCARD = STATE_WILDCARD -JOG_TO_DECK_SLOT = '5' TIP_RACK_SLOT = '8' MOVE_POINT_STATE_MAP: StatePointMap = { diff --git a/robot-server/robot_server/robot/calibration/deck/user_flow.py b/robot-server/robot_server/robot/calibration/deck/user_flow.py index 5e374d92a12..1490f59ebac 100644 --- a/robot-server/robot_server/robot/calibration/deck/user_flow.py +++ b/robot-server/robot_server/robot/calibration/deck/user_flow.py @@ -26,11 +26,10 @@ from robot_server.robot.calibration.constants import ( SHORT_TRASH_DECK, STANDARD_DECK, MOVE_TO_DECK_SAFETY_BUFFER, MOVE_TO_TIP_RACK_SAFETY_BUFFER, POINT_ONE_ID, POINT_TWO_ID, - POINT_THREE_ID) + POINT_THREE_ID, JOG_TO_DECK_SLOT) import robot_server.robot.calibration.util as uf from .constants import ( DeckCalibrationState as State, - JOG_TO_DECK_SLOT, TIP_RACK_SLOT, MOVE_POINT_STATE_MAP, SAVE_POINT_STATE_MAP) @@ -257,7 +256,7 @@ async def move_to_deck(self): def _get_move_to_point_loc_by_state(self) -> Location: assert self._z_height_reference is not None, \ "saveOffset has not been called yet" - pt_id = MOVE_POINT_STATE_MAP[self._current_state] + pt_id = MOVE_POINT_STATE_MAP[self.current_state] coords = self._deck.get_calibration_position(pt_id).position loc = Location(Point(*coords), None) return loc.move(point=Point(0, 0, self._z_height_reference)) diff --git a/robot-server/robot_server/robot/calibration/helper_classes.py b/robot-server/robot_server/robot/calibration/helper_classes.py index df30d700c8d..9b10dcc6e82 100644 --- a/robot-server/robot_server/robot/calibration/helper_classes.py +++ b/robot-server/robot_server/robot/calibration/helper_classes.py @@ -5,7 +5,6 @@ from uuid import uuid4, UUID from dataclasses import dataclass from pydantic import BaseModel, Field -from opentrons.hardware_control.types import CriticalPoint from opentrons.protocol_api import labware from opentrons.types import DeckLocation @@ -94,20 +93,8 @@ def __lt__(self, other): class PipetteInfo: rank: PipetteRank mount: Mount - tiprack_id: typing.Optional[UUID] - critical_point: typing.Optional[CriticalPoint] - - -@dataclass -class PipetteStatus: - model: str - name: str - tip_length: float - mount: str - has_tip: bool - rank: str - tiprack_id: typing.Optional[UUID] - serial: str + max_volume: int + channels: int # TODO: BC: the mount field here is typed as a string diff --git a/robot-server/robot_server/robot/calibration/pipette_offset/constants.py b/robot-server/robot_server/robot/calibration/pipette_offset/constants.py index 9fe2326183a..12492b8ca4b 100644 --- a/robot-server/robot_server/robot/calibration/pipette_offset/constants.py +++ b/robot-server/robot_server/robot/calibration/pipette_offset/constants.py @@ -44,5 +44,4 @@ class PipetteOffsetWithTipLengthCalibrationState(GenericState): WILDCARD = STATE_WILDCARD -JOG_TO_DECK_SLOT: Final = '5' TIP_RACK_SLOT: Final = '8' diff --git a/robot-server/robot_server/robot/calibration/pipette_offset/user_flow.py b/robot-server/robot_server/robot/calibration/pipette_offset/user_flow.py index aa1a39c7d8c..93f7f3e8ee3 100644 --- a/robot-server/robot_server/robot/calibration/pipette_offset/user_flow.py +++ b/robot-server/robot_server/robot/calibration/pipette_offset/user_flow.py @@ -22,13 +22,14 @@ POINT_ONE_ID, MOVE_TO_DECK_SAFETY_BUFFER, MOVE_TO_TIP_RACK_SAFETY_BUFFER, - CAL_BLOCK_SETUP_BY_MOUNT) + CAL_BLOCK_SETUP_BY_MOUNT, + JOG_TO_DECK_SLOT) from ..errors import CalibrationError from ..helper_classes import (RequiredLabware, AttachedPipette) from .constants import ( PipetteOffsetCalibrationState as POCState, PipetteOffsetWithTipLengthCalibrationState as POWTState, - GenericState, TIP_RACK_SLOT, JOG_TO_DECK_SLOT) + GenericState, TIP_RACK_SLOT) from .state_machine import ( PipetteOffsetCalibrationStateMachine, PipetteOffsetWithTipLengthStateMachine) diff --git a/robot-server/robot_server/robot/calibration/session.py b/robot-server/robot_server/robot/calibration/session.py deleted file mode 100644 index 809cb85b1e6..00000000000 --- a/robot-server/robot_server/robot/calibration/session.py +++ /dev/null @@ -1,248 +0,0 @@ -import contextlib -import typing -from uuid import UUID, uuid4 - -from robot_server.robot.calibration.constants import ( - TIP_RACK_LOOKUP_BY_MAX_VOL, - SHORT_TRASH_DECK, - STANDARD_DECK -) -from robot_server.robot.calibration.errors import CalibrationError -from robot_server.robot.calibration.helper_classes import PipetteInfo, \ - PipetteRank, LabwareInfo, Moves, CheckMove -from opentrons.config import feature_flags as ff -from opentrons.hardware_control import ThreadManager, Pipette, CriticalPoint -from opentrons.hardware_control.util import plan_arc -from opentrons.protocol_api import labware -from opentrons.protocols.geometry import deck, planning -from opentrons.types import Mount, Point, Location - -from robot_server.service.errors import RobotServerError -from .util import save_default_pick_up_current - - -class SessionManager: - """Small wrapper to keep track of robot calibration sessions created.""" - def __init__(self): - self._sessions = {} - - @property - def sessions(self): - return self._sessions - - -# vector from front bottom left of slot 12 -HEIGHT_SAFETY_BUFFER = Point(0, 0, 5.0) - - -class CalibrationSession: - """Class that controls state of the current robot calibration session""" - def __init__(self, hardware: ThreadManager, - lights_on_before: bool = False): - self._hardware = hardware - self._lights_on_before = lights_on_before - - deck_load_name = SHORT_TRASH_DECK if ff.short_fixed_trash() \ - else STANDARD_DECK - self._deck = deck.Deck(load_name=deck_load_name) - self._pip_info_by_mount = self._get_pip_info_by_mount( - hardware.get_attached_instruments()) - self._labware_info = self._determine_required_labware() - self._moves = self._build_deck_moves() - - @classmethod - async def build(cls, hardware: ThreadManager): - lights_on = hardware.get_lights()['rails'] - await hardware.cache_instruments() - await hardware.set_lights(rails=True) - await hardware.home() - return cls(hardware=hardware, lights_on_before=lights_on) - - @staticmethod - def _get_pip_info_by_mount( - new_pipettes: typing.Dict[Mount, Pipette.DictType]) \ - -> typing.Dict[Mount, PipetteInfo]: - pip_info_by_mount = {} - attached_pips = {m: p for m, p in new_pipettes.items() if p} - num_pips = len(attached_pips) - if num_pips > 0: - for mount, data in attached_pips.items(): - if data: - rank = PipetteRank.first - if num_pips == 2 and mount == Mount.LEFT: - rank = PipetteRank.second - cp = None - if data['channels'] == 8: - cp = CriticalPoint.FRONT_NOZZLE - pip_info_by_mount[mount] = PipetteInfo(tiprack_id=None, - critical_point=cp, - rank=rank, - mount=mount) - return pip_info_by_mount - else: - raise RobotServerError( - definition=CalibrationError.NO_PIPETTE_ATTACHED, - flow='calibration check') - - def _determine_required_labware(self) -> typing.Dict[UUID, LabwareInfo]: - """ - A function that inserts tiprack information into two dataclasses - :py:class:`.LabwareInfo` and :py:class:`.LabwareDefinition` based - on the current pipettes attached. - """ - lw: typing.Dict[UUID, LabwareInfo] = {} - _prev_lw_uuid: typing.Optional[UUID] = None - - for mount, pip_info in self._pip_info_by_mount.items(): - load_name: str = self._load_name_for_mount(mount) - prev_lw = lw.get(_prev_lw_uuid, None) if _prev_lw_uuid else None - if _prev_lw_uuid and prev_lw and prev_lw.loadName == load_name: - # pipette uses same tiprack as previous, use existing - lw[_prev_lw_uuid].forMounts.append(mount) - self._pip_info_by_mount[mount].tiprack_id = _prev_lw_uuid - else: - lw_def = labware.get_labware_definition(load_name) - new_uuid: UUID = uuid4() - _prev_lw_uuid = new_uuid - slot = self._get_tip_rack_slot_for_mount(mount) - lw[new_uuid] = LabwareInfo( - alternatives=self._alt_load_names_for_mount(mount), - forMounts=[mount], - loadName=load_name, - slot=slot, - namespace=lw_def['namespace'], - version=lw_def['version'], - id=new_uuid, - definition=lw_def) - self._pip_info_by_mount[mount].tiprack_id = new_uuid - return lw - - def _alt_load_names_for_mount(self, mount: Mount) -> typing.List[str]: - pip_vol = self.pipettes[mount]['max_volume'] - return list(TIP_RACK_LOOKUP_BY_MAX_VOL[str(pip_vol)].alternatives) - - def _load_name_for_mount(self, mount: Mount) -> str: - pip_vol = self.pipettes[mount]['max_volume'] - return TIP_RACK_LOOKUP_BY_MAX_VOL[str(pip_vol)].load_name - - def _build_deck_moves(self) -> Moves: - return Moves( - joggingFirstPipetteToHeight=self._build_height_dict('5'), - joggingFirstPipetteToPointOne=self._build_cross_dict('1BLC'), - joggingFirstPipetteToPointTwo=self._build_cross_dict('3BRC'), - joggingFirstPipetteToPointThree=self._build_cross_dict('7TLC'), - joggingSecondPipetteToHeight=self._build_height_dict('5'), - joggingSecondPipetteToPointOne=self._build_cross_dict('1BLC')) - - def _build_cross_dict(self, pos_id: str) -> CheckMove: - cross_coords = self._deck.get_calibration_position(pos_id).position - return CheckMove(position=Point(*cross_coords), locationId=uuid4()) - - def _build_height_dict(self, slot: str) -> CheckMove: - pos = self._deck.get_slot_center(slot) - ydim: float\ - = self._deck.get_slot_definition(slot)['boundingBox']['yDimension'] - # shift down to 10mm +y of the slot edge to both stay clear of the - # slot boundary, avoid the engraved slot number, and avoid the - # tiprack colliding if this is a multi - updated_pos = pos - Point(0, (ydim/2)-10, pos.z) + HEIGHT_SAFETY_BUFFER - return CheckMove(position=updated_pos, locationId=uuid4()) - - def _get_tip_rack_slot_for_mount(self, mount) -> str: - if len(self._pip_info_by_mount) == 2: - shared_tiprack = self._load_name_for_mount(Mount.LEFT) == \ - self._load_name_for_mount(Mount.RIGHT) - if mount == Mount.LEFT and not shared_tiprack: - return '6' - else: - return '8' - else: - return '8' - - async def _jog(self, mount: Mount, vector: Point): - """ - General function that can be used by all session types to jog around - a specified pipette. - """ - await self.hardware.move_rel(mount, vector) - - async def _pick_up_tip(self, mount: Mount): - pip_info = self._pip_info_by_mount[mount] - instr = self._hardware._attached_instruments[mount] - - if pip_info.tiprack_id: - lw_info = self.get_tiprack(pip_info.tiprack_id) - # Note: ABC DeckItem cannot have tiplength b/c of - # mod geometry contexts. Ignore type checking error here. - tiprack = self._deck[lw_info.slot] - full_length = tiprack.tip_length # type: ignore - overlap_dict: typing.Dict =\ - self.pipettes[mount]['tip_overlap'] # type: ignore - default = overlap_dict['default'] - overlap = overlap_dict.get( - tiprack.uri, # type: ignore - default) - tip_length = full_length - overlap - else: - tip_length = self.pipettes[mount]['fallback_tip_length'] - - with contextlib.ExitStack() as stack: - if pip_info.critical_point: - # If the pipette we're picking up tip for - # has a critical point, we know it is a multichannel - stack.enter_context(save_default_pick_up_current(instr)) - await self.hardware.pick_up_tip(mount, tip_length) - - async def _trash_tip(self, mount: Mount): - trash_lw = self._deck.get_fixed_trash() - assert trash_lw - to_loc = trash_lw.wells()[0].top() - await self._move(mount, to_loc, CriticalPoint.XY_CENTER) - await self._drop_tip(mount) - - async def _drop_tip(self, mount: Mount): - await self.hardware.drop_tip(mount) - - async def cache_instruments(self): - await self.hardware.cache_instruments() - new_dict = self._get_pip_info_by_mount( - self.hardware.get_attached_instruments()) - self._pip_info_by_mount.clear() - self._pip_info_by_mount.update(new_dict) - - @property - def hardware(self) -> ThreadManager: - return self._hardware - - def get_tiprack(self, uuid: UUID) -> LabwareInfo: - return self._labware_info[uuid] - - @property - def pipettes(self) -> typing.Dict[Mount, Pipette.DictType]: - return self.hardware.attached_instruments - - @property - def labware_status(self) -> typing.Dict[UUID, LabwareInfo]: - """ - Public property to help format the current labware status of a given - session for the client. - """ - return self._labware_info - - async def _move(self, - mount: Mount, - to_loc: Location, - cp_override: CriticalPoint = None): - from_pt = await self.hardware.gantry_position(mount) - from_loc = Location(from_pt, None) - cp = cp_override or self._pip_info_by_mount[mount].critical_point - - max_height = self.hardware.get_instrument_max_height(mount) - safe = planning.safe_height( - from_loc, to_loc, self._deck, max_height) - moves = plan_arc(from_pt, to_loc.point, safe, - origin_cp=None, - dest_cp=cp) - for move in moves: - await self.hardware.move_to( - mount, move[0], critical_point=move[1]) diff --git a/robot-server/robot_server/robot/calibration/util.py b/robot-server/robot_server/robot/calibration/util.py index e444c4ed656..9367f199014 100644 --- a/robot-server/robot_server/robot/calibration/util.py +++ b/robot-server/robot_server/robot/calibration/util.py @@ -1,3 +1,4 @@ +import logging import contextlib from typing import Set, Dict, Any, Union, TYPE_CHECKING @@ -20,14 +21,16 @@ from .pipette_offset.constants import ( PipetteOffsetCalibrationState, PipetteOffsetWithTipLengthCalibrationState) from .deck.constants import DeckCalibrationState +from .check.constants import CalibrationCheckState if TYPE_CHECKING: from .deck.user_flow import DeckCalibrationUserFlow from .tip_length.user_flow import TipCalibrationUserFlow from .pipette_offset.user_flow import PipetteOffsetCalibrationUserFlow + from .check.user_flow import CheckCalibrationUserFlow ValidState = Union[TipCalibrationState, DeckCalibrationState, - PipetteOffsetCalibrationState, + PipetteOffsetCalibrationState, CalibrationCheckState, PipetteOffsetWithTipLengthCalibrationState] @@ -41,6 +44,7 @@ def __init__(self, TransitionMap = Dict[Any, Dict[Any, Any]] +MODULE_LOG = logging.getLogger(__name__) class SimpleStateMachine: @@ -84,7 +88,8 @@ def get_next_state(self, from_state, command): CalibrationUserFlow = Union[ 'DeckCalibrationUserFlow', 'TipCalibrationUserFlow', - 'PipetteOffsetCalibrationUserFlow'] + 'PipetteOffsetCalibrationUserFlow', + 'CheckCalibrationUserFlow'] async def invalidate_tip(user_flow: CalibrationUserFlow): diff --git a/robot-server/robot_server/service/session/models/command.py b/robot-server/robot_server/service/session/models/command.py index 6c01ae8c784..ffe33364c61 100644 --- a/robot-server/robot_server/service/session/models/command.py +++ b/robot-server/robot_server/service/session/models/command.py @@ -192,19 +192,6 @@ def namespace(): return "calibration" -class CalibrationCheckCommand(CommandDefinition): - """Cal Check Specific""" - prepare_pipette = "preparePipette" - compare_point = "comparePoint" - go_to_next_check = "goToNextCheck" - # TODO: remove unused command name and trigger - reject_calibration = "rejectCalibration" - - @staticmethod - def namespace(): - return "calibration.check" - - class DeckCalibrationCommand(CommandDefinition): """Deck Calibration Specific""" move_to_point_two = "moveToPointTwo" @@ -215,6 +202,18 @@ def namespace(): return "calibration.deck" +class CheckCalibrationCommand(CommandDefinition): + """Check Calibration Health Specific""" + compare_point = "comparePoint" + switch_pipette = "switchPipette" + return_tip = "returnTip" + transition = "transition" + + @staticmethod + def namespace(): + return "calibration.check" + + """ IMPORTANT: See note for SessionCreateParamType @@ -235,7 +234,7 @@ def namespace(): CommandDefinitionType = typing.Union[ RobotCommand, CalibrationCommand, - CalibrationCheckCommand, + CheckCalibrationCommand, DeckCalibrationCommand, ProtocolCommand, PipetteCommand, @@ -279,7 +278,7 @@ def _pre_namespace_mapping() -> typing.Dict[str, CommandDefinition]: """Create a dictionary of pre-namespace name to CommandDefinition""" # A tuple of CommandDefinition enums which need to be identified by # localname and full namespaced name - pre_namespace_ns = CalibrationCheckCommand, CalibrationCommand + pre_namespace_ns = CheckCalibrationCommand, CalibrationCommand # Flatten t = tuple(v for k in pre_namespace_ns for v in k) return {k.localname: k for k in t} diff --git a/robot-server/robot_server/service/session/models/session.py b/robot-server/robot_server/service/session/models/session.py index f2bc0a7bf51..bb07c620718 100644 --- a/robot-server/robot_server/service/session/models/session.py +++ b/robot-server/robot_server/service/session/models/session.py @@ -5,12 +5,11 @@ from pydantic import BaseModel, Field, validator -from robot_server.robot.calibration.check.models import \ - CalibrationSessionStatus +from robot_server.robot.calibration.check.models import\ + CalibrationCheckSessionStatus from robot_server.robot.calibration.deck.models import \ DeckCalibrationSessionStatus -from robot_server.robot.calibration.models import \ - SessionCreateParams as TipLengthPipetteOffsetSessionCreateParams +from robot_server.robot.calibration.models import SessionCreateParams from robot_server.robot.calibration.pipette_offset.models import\ PipetteOffsetCalibrationSessionStatus from robot_server.robot.calibration.tip_length.models import\ @@ -41,13 +40,11 @@ def __new__(cls, value, create_param_model=None): default = 'default' calibration_check = 'calibrationCheck' tip_length_calibration = ( - 'tipLengthCalibration', - TipLengthPipetteOffsetSessionCreateParams - ) + 'tipLengthCalibration', SessionCreateParams) deck_calibration = 'deckCalibration' pipette_offset_calibration = ( 'pipetteOffsetCalibration', - TipLengthPipetteOffsetSessionCreateParams + SessionCreateParams ) protocol = ('protocol', ProtocolCreateParams) live_protocol = 'liveProtocol' @@ -69,7 +66,7 @@ def model(self): https://pydantic-docs.helpmanual.io/usage/types/#literal-type """ SessionCreateParamType = typing.Union[ - TipLengthPipetteOffsetSessionCreateParams, + SessionCreateParams, ProtocolCreateParams, None, EmptyModel @@ -81,7 +78,7 @@ def model(self): Read more here: https://pydantic-docs.helpmanual.io/usage/types/#unions """ SessionDetails = typing.Union[ - CalibrationSessionStatus, + CalibrationCheckSessionStatus, PipetteOffsetCalibrationSessionStatus, TipCalibrationSessionStatus, DeckCalibrationSessionStatus, diff --git a/robot-server/robot_server/service/session/router.py b/robot-server/robot_server/service/session/router.py index 3b96f3c9f39..6d4e11805ea 100644 --- a/robot-server/robot_server/service/session/router.py +++ b/robot-server/robot_server/service/session/router.py @@ -80,7 +80,6 @@ async def delete_session_handler( session_obj = get_session(manager=session_manager, session_id=sessionId, api_router=router) - await session_manager.remove(session_obj.meta.identifier) return SessionResponse( @@ -156,7 +155,6 @@ async def session_command_execute_handler( command_request.data.attributes.data) command_result = await session_obj.command_executor.execute(command) - log.info(f"Command completed: {command}") log.debug(f"Command result: {command_result}") return CommandResponse( diff --git a/robot-server/robot_server/service/session/session_types/check_session.py b/robot-server/robot_server/service/session/session_types/check_session.py index 519a3291ef1..517f5aefab1 100644 --- a/robot-server/robot_server/service/session/session_types/check_session.py +++ b/robot-server/robot_server/service/session/session_types/check_session.py @@ -1,7 +1,11 @@ -from robot_server.robot.calibration.check.session import\ - CheckCalibrationSession -from robot_server.robot.calibration.check import models as calibration_models -from robot_server.robot.calibration.check.util import StateMachineError +from typing import Awaitable, cast, TYPE_CHECKING, List + +from robot_server.robot.calibration.check.user_flow import\ + CheckCalibrationUserFlow +from robot_server.robot.calibration.check.models import ( + ComparisonMap, ComparisonStatePerPipette, + CalibrationCheckSessionStatus) +from robot_server.robot.calibration.check import util from robot_server.service.session.command_execution import \ CommandQueue, CallableExecutor, Command, CompletedCommand @@ -13,13 +17,16 @@ from robot_server.service.session.errors import SessionCreationException, \ CommandExecutionException, UnsupportedFeature +if TYPE_CHECKING: + from opentrons_shared_data.labware import LabwareDefinition + -class CheckSessionStateExecutor(CallableExecutor): +class CheckSessionCommandExecutor(CallableExecutor): async def execute(self, command: Command) -> CompletedCommand: try: return await super().execute(command) - except (StateMachineError, AssertionError) as e: + except AssertionError as e: raise CommandExecutionException(str(e)) @@ -28,66 +35,76 @@ class CheckSession(BaseSession): def __init__(self, configuration: SessionConfiguration, instance_meta: SessionMetaData, - calibration_check: CheckCalibrationSession): + calibration_check: CheckCalibrationUserFlow, + shutdown_handler: Awaitable[None] = None): super().__init__(configuration, instance_meta) self._calibration_check = calibration_check - self._command_executor = CheckSessionStateExecutor( + self._command_executor = CheckSessionCommandExecutor( self._calibration_check.handle_command ) + self._shutdown_coroutine = shutdown_handler @classmethod async def create(cls, configuration: SessionConfiguration, instance_meta: SessionMetaData) -> BaseSession: """Create an instance""" + # (lc, 10-19-2020) For now, only pass in an empty list. We cannot + # have a session model with an optional tiprack for session + # create params right now because of the pydantic union problem. + tip_racks: List = [] + # if lights are on already it's because the user clicked the button, + # so a) we don't need to turn them on now and b) we shouldn't turn them + # off after + session_controls_lights =\ + not configuration.hardware.get_lights()['rails'] + await configuration.hardware.cache_instruments() try: - calibration_check = await CheckCalibrationSession.build( - configuration.hardware - ) + calibration_check = CheckCalibrationUserFlow( + configuration.hardware, + tip_rack_defs=[ + cast('LabwareDefinition', rack) for rack in tip_racks]) except AssertionError as e: raise SessionCreationException(str(e)) + if session_controls_lights: + await configuration.hardware.set_lights(rails=True) + shutdown_handler = configuration.hardware.set_lights(rails=False) + else: + shutdown_handler = None + return cls( configuration=configuration, instance_meta=instance_meta, - calibration_check=calibration_check) + calibration_check=calibration_check, + shutdown_handler=shutdown_handler) - async def clean_up(self): - await super().clean_up() - await self._calibration_check.delete_session() + def _map_to_pydantic_model( + self, comparison_map: util.ComparisonStatePerPipette + ) -> ComparisonStatePerPipette: + first = comparison_map.first + second = comparison_map.second + first_compmap = ComparisonMap( + comparingHeight=first.comparingHeight, + comparingPointOne=first.comparingPointOne, + comparingPointTwo=first.comparingPointTwo, + comparingPointThree=first.comparingPointThree) + second_compmap = ComparisonMap( + comparingHeight=second.comparingHeight, + comparingPointOne=second.comparingPointOne) + return ComparisonStatePerPipette( + first=first_compmap, second=second_compmap) def _get_response_details(self) -> SessionDetails: - instruments = { - str(k): calibration_models.AttachedPipette( - model=v.model, - name=v.name, - tip_length=v.tip_length, - mount=str(v.mount), - has_tip=v.has_tip, - rank=v.rank, - tiprack_id=v.tiprack_id, - serial=v.serial) - for k, v in self._calibration_check.pipette_status().items() - } - labware = [ - calibration_models.LabwareStatus( - alternatives=data.alternatives, - slot=data.slot, - id=data.id, - forMounts=[str(m) for m in data.forMounts], - loadName=data.loadName, - namespace=data.namespace, - version=str(data.version)) for data in - self._calibration_check.labware_status.values() - ] - - # TODO(mc, 2020-09-17): type of get_comparisons_by_step doesn't quite - # match what CalibrationSessionStatus expects for comparisonsByStep - return calibration_models.CalibrationSessionStatus( - instruments=instruments, - currentStep=self._calibration_check.current_state_name, - comparisonsByStep=self._calibration_check.get_comparisons_by_step(), # type: ignore[arg-type] # noqa: e501 - labware=labware, + comparison_map =\ + self._map_to_pydantic_model(self._calibration_check.comparison_map) + return CalibrationCheckSessionStatus( + instruments=self._calibration_check.get_instruments(), + currentStep=self._calibration_check.current_state, + comparisonsByPipette=comparison_map, + labware=self._calibration_check.get_required_labware(), + activePipette=self._calibration_check.get_active_pipette(), + activeTipRack=self._calibration_check.get_active_tiprack() ) @property @@ -101,3 +118,7 @@ def command_queue(self) -> CommandQueue: @property def session_type(self) -> SessionType: return SessionType.calibration_check + + async def clean_up(self): + if self._shutdown_coroutine: + await self._shutdown_coroutine diff --git a/robot-server/robot_server/service/session/session_types/pipette_offset_calibration.py b/robot-server/robot_server/service/session/session_types/pipette_offset_calibration.py index bfe14fb8240..3847bef9444 100644 --- a/robot-server/robot_server/service/session/session_types/pipette_offset_calibration.py +++ b/robot-server/robot_server/service/session/session_types/pipette_offset_calibration.py @@ -53,7 +53,7 @@ async def create(cls, configuration: SessionConfiguration, recalibrate_tip_length\ = instance_meta.create_params.shouldRecalibrateTipLength has_cal_block = instance_meta.create_params.hasCalibrationBlock - tiprack = instance_meta.create_params.tipRackDefinition + tip_rack_def = instance_meta.create_params.tipRackDefinition # if lights are on already it's because the user clicked the button, # so a) we don't need to turn them on now and b) we shouldn't turn them # off after @@ -66,7 +66,7 @@ async def create(cls, configuration: SessionConfiguration, mount=Mount[mount.upper()], recalibrate_tip_length=recalibrate_tip_length, has_calibration_block=has_cal_block, - tip_rack_def=cast('LabwareDefinition', tiprack)) + tip_rack_def=cast('LabwareDefinition', tip_rack_def)) except AssertionError as e: raise SessionCreationException(str(e)) diff --git a/robot-server/tests/integration/sessions/test_calibration_check.tavern.yaml b/robot-server/tests/integration/sessions/test_calibration_check.tavern.yaml index 94b336d60dc..b75811edc86 100644 --- a/robot-server/tests/integration/sessions/test_calibration_check.tavern.yaml +++ b/robot-server/tests/integration/sessions/test_calibration_check.tavern.yaml @@ -20,6 +20,7 @@ stages: save: json: session_id: data.id + - name: Check that current state is sessionStarted request: &get_session url: "{host:s}:{port:d}/sessions/{session_id}" @@ -37,9 +38,11 @@ stages: createParams: null details: &session_data_attribute_details currentStep: sessionStarted - instruments: !anydict + instruments: !anylist + activePipette: !anydict + activeTipRack: !anydict labware: !anylist - comparisonsByStep: {} + comparisonsByPipette: !anydict - name: Load labware request: &post_command @@ -49,7 +52,7 @@ stages: data: type: SessionCommand attributes: - command: loadLabware + command: calibration.loadLabware data: {} response: status_code: 200 @@ -72,10 +75,9 @@ stages: <<: *post_command json: data: - id: "{session_id}" type: SessionCommand attributes: - command: preparePipette + command: calibration.moveToTipRack data: {} response: status_code: 200 @@ -91,7 +93,7 @@ stages: <<: *session_data_attributes details: <<: *session_data_attribute_details - currentStep: preparingFirstPipette + currentStep: preparingPipette - name: Jog first pipette request: @@ -100,7 +102,7 @@ stages: data: type: SessionCommand attributes: - command: jog + command: calibration.jog data: vector: [0, 0, -10] response: @@ -117,7 +119,7 @@ stages: <<: *session_data_attributes details: <<: *session_data_attribute_details - currentStep: preparingFirstPipette + currentStep: preparingPipette - name: Pick up tip request: @@ -126,7 +128,7 @@ stages: data: type: SessionCommand attributes: - command: pickUpTip + command: calibration.pickUpTip data: {} response: status_code: 200 @@ -142,7 +144,7 @@ stages: <<: *session_data_attributes details: <<: *session_data_attribute_details - currentStep: inspectingFirstTip + currentStep: inspectingTip - name: Confirm tip attached request: @@ -151,7 +153,7 @@ stages: data: type: SessionCommand attributes: - command: confirmTip + command: calibration.moveToDeck data: {} response: status_code: 200 @@ -167,7 +169,7 @@ stages: <<: *session_data_attributes details: <<: *session_data_attribute_details - currentStep: joggingFirstPipetteToHeight + currentStep: comparingHeight - name: Jog first pipette to height request: @@ -176,7 +178,7 @@ stages: data: type: SessionCommand attributes: - command: jog + command: calibration.jog data: vector: [0, 0, -10] response: @@ -193,7 +195,7 @@ stages: <<: *session_data_attributes details: <<: *session_data_attribute_details - currentStep: joggingFirstPipetteToHeight + currentStep: comparingHeight - name: Compare first pipette height request: @@ -202,7 +204,7 @@ stages: data: type: SessionCommand attributes: - command: comparePoint + command: calibration.check.comparePoint data: {} response: status_code: 200 @@ -218,9 +220,16 @@ stages: <<: *session_data_attributes details: <<: *session_data_attribute_details - currentStep: comparingFirstPipetteHeight - comparisonsByStep: - comparingFirstPipetteHeight: !anydict + currentStep: comparingHeight + comparisonsByPipette: + first: + comparingHeight: !anydict + comparingPointOne: null + comparingPointTwo: null + comparingPointThree: null + second: + comparingHeight: null + comparingPointOne: null - name: Go to next check request: @@ -229,7 +238,7 @@ stages: data: type: SessionCommand attributes: - command: goToNextCheck + command: calibration.moveToPointOne data: {} response: status_code: 200 @@ -245,9 +254,16 @@ stages: <<: *session_data_attributes details: <<: *session_data_attribute_details - currentStep: joggingFirstPipetteToPointOne - comparisonsByStep: - comparingFirstPipetteHeight: !anydict + currentStep: comparingPointOne + comparisonsByPipette: + first: + comparingHeight: !anydict + comparingPointOne: null + comparingPointTwo: null + comparingPointThree: null + second: + comparingHeight: null + comparingPointOne: null - name: Compare first pipette point one request: @@ -256,7 +272,7 @@ stages: data: type: SessionCommand attributes: - command: comparePoint + command: calibration.check.comparePoint data: {} response: status_code: 200 @@ -272,10 +288,16 @@ stages: <<: *session_data_attributes details: <<: *session_data_attribute_details - currentStep: comparingFirstPipettePointOne - comparisonsByStep: - comparingFirstPipetteHeight: !anydict - comparingFirstPipettePointOne: !anydict + currentStep: comparingPointOne + comparisonsByPipette: + first: + comparingHeight: !anydict + comparingPointOne: !anydict + comparingPointTwo: null + comparingPointThree: null + second: + comparingHeight: null + comparingPointOne: null - name: Go to next check request: @@ -284,7 +306,7 @@ stages: data: type: SessionCommand attributes: - command: goToNextCheck + command: calibration.deck.moveToPointTwo data: {} response: status_code: 200 @@ -300,10 +322,10 @@ stages: <<: *session_data_attributes details: <<: *session_data_attribute_details - currentStep: joggingFirstPipetteToPointTwo - comparisonsByStep: - comparingFirstPipetteHeight: !anydict - comparingFirstPipettePointOne: !anydict + currentStep: comparingPointTwo + comparisonsByPipette: + first: !anydict + second: !anydict - name: Compare first pipette point two request: @@ -312,7 +334,7 @@ stages: data: type: SessionCommand attributes: - command: comparePoint + command: calibration.check.comparePoint data: {} response: status_code: 200 @@ -328,11 +350,10 @@ stages: <<: *session_data_attributes details: <<: *session_data_attribute_details - currentStep: comparingFirstPipettePointTwo - comparisonsByStep: - comparingFirstPipetteHeight: !anydict - comparingFirstPipettePointOne: !anydict - comparingFirstPipettePointTwo: !anydict + currentStep: comparingPointTwo + comparisonsByPipette: + first: !anydict + second: !anydict - name: Go to next check request: @@ -341,7 +362,7 @@ stages: data: type: SessionCommand attributes: - command: goToNextCheck + command: calibration.deck.moveToPointThree data: {} response: status_code: 200 @@ -357,11 +378,10 @@ stages: <<: *session_data_attributes details: <<: *session_data_attribute_details - currentStep: joggingFirstPipetteToPointThree - comparisonsByStep: - comparingFirstPipetteHeight: !anydict - comparingFirstPipettePointOne: !anydict - comparingFirstPipettePointTwo: !anydict + currentStep: comparingPointThree + comparisonsByPipette: + first: !anydict + second: !anydict - name: Compare first pipette point three request: @@ -370,252 +390,7 @@ stages: data: type: SessionCommand attributes: - command: comparePoint - data: {} - response: - status_code: 200 - - name: Check the effect of command - request: *get_session - response: - status_code: 200 - json: - links: !anydict - data: - <<: *session_data - attributes: - <<: *session_data_attributes - details: - <<: *session_data_attribute_details - currentStep: comparingFirstPipettePointThree - comparisonsByStep: - comparingFirstPipetteHeight: !anydict - comparingFirstPipettePointOne: !anydict - comparingFirstPipettePointTwo: !anydict - comparingFirstPipettePointThree: !anydict - - - name: Go to next check - request: - <<: *post_command - json: - data: - type: SessionCommand - attributes: - command: goToNextCheck - data: {} - response: - status_code: 200 - - name: Check the effect of command - request: *get_session - response: - status_code: 200 - json: - links: !anydict - data: - <<: *session_data - attributes: - <<: *session_data_attributes - details: - <<: *session_data_attribute_details - currentStep: preparingSecondPipette - comparisonsByStep: - comparingFirstPipetteHeight: !anydict - comparingFirstPipettePointOne: !anydict - comparingFirstPipettePointTwo: !anydict - comparingFirstPipettePointThree: !anydict - - - name: Jog Second Pipette - request: - <<: *post_command - json: - data: - type: SessionCommand - attributes: - command: jog - data: - vector: [0, 0, -10] - response: - status_code: 200 - - name: Check the effect of command - request: *get_session - response: - status_code: 200 - json: - links: !anydict - data: - <<: *session_data - attributes: - <<: *session_data_attributes - details: - <<: *session_data_attribute_details - currentStep: preparingSecondPipette - comparisonsByStep: - comparingFirstPipetteHeight: !anydict - comparingFirstPipettePointOne: !anydict - comparingFirstPipettePointTwo: !anydict - comparingFirstPipettePointThree: !anydict - - - name: Pick up tip - request: - <<: *post_command - json: - data: - type: SessionCommand - attributes: - command: pickUpTip - data: {} - response: - status_code: 200 - - name: Check the effect of command - request: *get_session - response: - status_code: 200 - json: - links: !anydict - data: - <<: *session_data - attributes: - <<: *session_data_attributes - details: - <<: *session_data_attribute_details - currentStep: inspectingSecondTip - comparisonsByStep: - comparingFirstPipetteHeight: !anydict - comparingFirstPipettePointOne: !anydict - comparingFirstPipettePointTwo: !anydict - comparingFirstPipettePointThree: !anydict - - - name: Confirm tip - request: - <<: *post_command - json: - data: - type: SessionCommand - attributes: - command: confirmTip - data: {} - response: - status_code: 200 - - name: Check the effect of command - request: *get_session - response: - status_code: 200 - json: - links: !anydict - data: - <<: *session_data - attributes: - <<: *session_data_attributes - details: - <<: *session_data_attribute_details - currentStep: joggingSecondPipetteToHeight - comparisonsByStep: - comparingFirstPipetteHeight: !anydict - comparingFirstPipettePointOne: !anydict - comparingFirstPipettePointTwo: !anydict - comparingFirstPipettePointThree: !anydict - - - name: Compare second pipette to height - request: - <<: *post_command - json: - data: - type: SessionCommand - attributes: - command: comparePoint - data: {} - response: - status_code: 200 - - name: Check the effect of command - request: *get_session - response: - status_code: 200 - json: - links: !anydict - data: - <<: *session_data - attributes: - <<: *session_data_attributes - details: - <<: *session_data_attribute_details - currentStep: comparingSecondPipetteHeight - comparisonsByStep: - comparingFirstPipetteHeight: !anydict - comparingFirstPipettePointOne: !anydict - comparingFirstPipettePointTwo: !anydict - comparingFirstPipettePointThree: !anydict - comparingSecondPipetteHeight: !anydict - - - name: Go to next check - request: - <<: *post_command - json: - data: - type: SessionCommand - attributes: - command: goToNextCheck - data: {} - response: - status_code: 200 - - name: Check the effect of command - request: *get_session - response: - status_code: 200 - json: - links: !anydict - data: - <<: *session_data - attributes: - <<: *session_data_attributes - details: - <<: *session_data_attribute_details - currentStep: joggingSecondPipetteToPointOne - comparisonsByStep: - comparingFirstPipetteHeight: !anydict - comparingFirstPipettePointOne: !anydict - comparingFirstPipettePointTwo: !anydict - comparingFirstPipettePointThree: !anydict - comparingSecondPipetteHeight: !anydict - - - name: Compare second pipette to point one - request: - <<: *post_command - json: - data: - type: SessionCommand - attributes: - command: comparePoint - data: {} - response: - status_code: 200 - - name: Check the effect of command - request: *get_session - response: - status_code: 200 - json: - links: !anydict - data: - <<: *session_data - attributes: - <<: *session_data_attributes - details: - <<: *session_data_attribute_details - currentStep: comparingSecondPipettePointOne - comparisonsByStep: - comparingFirstPipetteHeight: !anydict - comparingFirstPipettePointOne: !anydict - comparingFirstPipettePointTwo: !anydict - comparingFirstPipettePointThree: !anydict - comparingSecondPipetteHeight: !anydict - comparingSecondPipettePointOne: !anydict - - - name: Go to next check - request: - <<: *post_command - json: - data: - type: SessionCommand - attributes: - command: goToNextCheck + command: calibration.check.comparePoint data: {} response: status_code: 200 @@ -631,14 +406,10 @@ stages: <<: *session_data_attributes details: <<: *session_data_attribute_details - currentStep: checkComplete - comparisonsByStep: - comparingFirstPipetteHeight: !anydict - comparingFirstPipettePointOne: !anydict - comparingFirstPipettePointTwo: !anydict - comparingFirstPipettePointThree: !anydict - comparingSecondPipetteHeight: !anydict - comparingSecondPipettePointOne: !anydict + currentStep: comparingPointThree + comparisonsByPipette: + first: !anydict + second: !anydict - name: Delete the session request: diff --git a/robot-server/tests/robot/calibration/check/test_check_calibration_session.py b/robot-server/tests/robot/calibration/check/test_check_calibration_session.py deleted file mode 100644 index 1f0451fefef..00000000000 --- a/robot-server/tests/robot/calibration/check/test_check_calibration_session.py +++ /dev/null @@ -1,889 +0,0 @@ -from unittest.mock import patch, call - -import pytest -from opentrons import types -from opentrons.hardware_control import API, ThreadManager - -from robot_server.robot.calibration.check import session, util -from robot_server.service.errors import RobotServerError - - -@pytest.fixture -async def check_calibration_session(loop) -> session.CheckCalibrationSession: - attached_instruments = { - types.Mount.LEFT: { - 'model': 'p10_single_v1', - 'id': 'fake10pip' - }, - types.Mount.RIGHT: { - 'model': 'p300_single_v1', - 'id': 'fake300pip' - } - } - - simulator = ThreadManager(API.build_hardware_simulator, - attached_instruments=attached_instruments) - return await session.CheckCalibrationSession.build(simulator) - - -@pytest.fixture -async def check_calibration_session_shared_tips(loop) \ - -> session.CheckCalibrationSession: - attached_instruments = { - types.Mount.LEFT: { - 'model': 'p300_multi_v1', - 'id': 'fake300multipip' - }, - types.Mount.RIGHT: { - 'model': 'p300_single_v1', - 'id': 'fake300pip' - } - } - - simulator = ThreadManager(API.build_hardware_simulator, - attached_instruments=attached_instruments) - return await session.CheckCalibrationSession.build(simulator) - - -@pytest.fixture -async def check_calibration_session_only_right(loop) \ - -> session.CheckCalibrationSession: - attached_instruments = { - types.Mount.RIGHT: { - 'model': 'p300_single_v1', - 'id': 'fake300pip' - } - } - - simulator = ThreadManager(API.build_hardware_simulator, - attached_instruments=attached_instruments) - return await session.CheckCalibrationSession.build(simulator) - - -@pytest.fixture -async def check_calibration_session_only_left(loop) \ - -> session.CheckCalibrationSession: - attached_instruments = { - types.Mount.LEFT: { - 'model': 'p300_single_v1', - 'id': 'fake300pip' - } - } - simulator = ThreadManager(API.build_hardware_simulator, - attached_instruments=attached_instruments) - return await session.CheckCalibrationSession.build(simulator) - - -BAD_DIFF_VECTOR = types.Point(30, 30, 30) -OK_DIFF_VECTOR = types.Point(1, 1, 0.3) - -# helpers - - -async def in_labware_loaded(check_calibration_session): - await check_calibration_session.trigger_transition( - session.CalibrationCheckTrigger.load_labware - ) - return check_calibration_session - - -async def in_preparing_first_pipette(check_calibration_session): - check_calibration_session = await in_labware_loaded( - check_calibration_session - ) - await check_calibration_session.trigger_transition( - session.CalibrationCheckTrigger.prepare_pipette, - ) - await check_calibration_session.trigger_transition( - session.CalibrationCheckTrigger.jog, - types.Point(0, 0, -10) - ) - return check_calibration_session - - -async def in_inspecting_first_tip(check_calibration_session): - check_calibration_session = await in_preparing_first_pipette( - check_calibration_session - ) - await check_calibration_session.trigger_transition( - session.CalibrationCheckTrigger.jog, - types.Point(0, 0, -0.2) - ) - await check_calibration_session.trigger_transition( - session.CalibrationCheckTrigger.pick_up_tip, - ) - return check_calibration_session - - -async def in_jogging_first_pipette_to_height(check_calibration_session): - check_calibration_session = await in_inspecting_first_tip( - check_calibration_session - ) - await check_calibration_session.trigger_transition( - session.CalibrationCheckTrigger.confirm_tip_attached, - ) - return check_calibration_session - - -async def in_comparing_first_pipette_height(check_calibration_session): - check_calibration_session = await in_jogging_first_pipette_to_height( - check_calibration_session - ) - await check_calibration_session.trigger_transition( - session.CalibrationCheckTrigger.compare_point, - ) - return check_calibration_session - - -async def in_jogging_first_pipette_to_point_one(check_calibration_session): - check_calibration_session = await in_comparing_first_pipette_height( - check_calibration_session - ) - await check_calibration_session.trigger_transition( - session.CalibrationCheckTrigger.go_to_next_check, - ) - return check_calibration_session - - -async def in_comparing_first_pipette_point_one(check_calibration_session): - check_calibration_session = await in_jogging_first_pipette_to_point_one( - check_calibration_session - ) - await check_calibration_session.trigger_transition( - session.CalibrationCheckTrigger.compare_point, - ) - return check_calibration_session - - -async def in_jogging_first_pipette_to_point_two(check_calibration_session): - check_calibration_session = await in_comparing_first_pipette_point_one( - check_calibration_session - ) - await check_calibration_session.trigger_transition( - session.CalibrationCheckTrigger.go_to_next_check, - ) - return check_calibration_session - - -async def in_comparing_first_pipette_point_two(check_calibration_session): - check_calibration_session = await in_jogging_first_pipette_to_point_two( - check_calibration_session - ) - await check_calibration_session.trigger_transition( - session.CalibrationCheckTrigger.compare_point, - ) - return check_calibration_session - - -async def in_jogging_first_pipette_to_point_three(check_calibration_session): - check_calibration_session = await in_comparing_first_pipette_point_two( - check_calibration_session - ) - await check_calibration_session.trigger_transition( - session.CalibrationCheckTrigger.go_to_next_check, - ) - return check_calibration_session - - -async def in_comparing_first_pipette_point_three(check_calibration_session): - check_calibration_session = await in_jogging_first_pipette_to_point_three( - check_calibration_session - ) - await check_calibration_session.trigger_transition( - session.CalibrationCheckTrigger.compare_point, - ) - return check_calibration_session - - -async def in_preparing_second_pipette(check_calibration_session): - check_calibration_session = await in_comparing_first_pipette_point_three( - check_calibration_session - ) - await check_calibration_session.trigger_transition( - session.CalibrationCheckTrigger.go_to_next_check, - ) - await check_calibration_session.trigger_transition( - session.CalibrationCheckTrigger.jog, - types.Point(0, 0, -10) - ) - return check_calibration_session - - -async def in_inspecting_second_tip(check_calibration_session): - check_calibration_session = await in_preparing_second_pipette( - check_calibration_session - ) - await check_calibration_session.trigger_transition( - session.CalibrationCheckTrigger.jog, - types.Point(0, 0, -0.3) - ) - await check_calibration_session.trigger_transition( - session.CalibrationCheckTrigger.pick_up_tip, - ) - return check_calibration_session - - -async def in_jogging_second_pipette_to_height(check_calibration_session): - check_calibration_session = await in_inspecting_second_tip( - check_calibration_session - ) - await check_calibration_session.trigger_transition( - session.CalibrationCheckTrigger.confirm_tip_attached, - ) - return check_calibration_session - - -async def in_comparing_second_pipette_height(check_calibration_session): - check_calibration_session = await in_jogging_second_pipette_to_height( - check_calibration_session - ) - await check_calibration_session.trigger_transition( - session.CalibrationCheckTrigger.compare_point, - ) - return check_calibration_session - - -async def in_jogging_second_pipette_to_point_one(check_calibration_session): - check_calibration_session = await in_comparing_second_pipette_height( - check_calibration_session - ) - await check_calibration_session.trigger_transition( - session.CalibrationCheckTrigger.go_to_next_check, - ) - return check_calibration_session - - -async def in_comparing_second_pipette_point_one(check_calibration_session): - check_calibration_session = await in_jogging_second_pipette_to_point_one( - check_calibration_session - ) - await check_calibration_session.trigger_transition( - session.CalibrationCheckTrigger.compare_point, - ) - return check_calibration_session - -# START misc session attribute tests - - -def test_session_started(check_calibration_session): - assert check_calibration_session.current_state.name == \ - session.CalibrationCheckState.sessionStarted - - -async def test_lights_from_off(check_calibration_session): - # lights were off before starting session - assert check_calibration_session._lights_on_before is False - # lights are on after starting session - assert check_calibration_session._hardware.get_lights()['rails'] is True - await check_calibration_session.delete_session() - # lights should be off after deleting session - assert check_calibration_session._hardware.get_lights()['rails'] is False - - -async def test_lights_from_on(check_calibration_session): - # lights were on before starting session - check_calibration_session._lights_on_before = True - # lights were still on after starting session - assert check_calibration_session._hardware.get_lights()['rails'] is True - await check_calibration_session.delete_session() - # lights should still be on after deleting session - assert check_calibration_session._hardware.get_lights()['rails'] is True - - -async def test_pick_up_tip_sets_current(check_calibration_session_shared_tips): - sess = check_calibration_session_shared_tips - await sess.trigger_transition( - session.CalibrationCheckTrigger.load_labware) - path = "opentrons.hardware_control.pipette.Pipette.update_config_item" - with patch(path) as m: - await sess._pick_up_tip(types.Mount.LEFT) - calls = [call('pick_up_current', 0.1), call('pick_up_current', 0.6)] - assert m.call_args_list == calls - - -async def test_ensure_safety_removed_for_comparison( - check_calibration_session, monkeypatch): - fake_moves_list = [] - - async def fake_move(mount, point): - fake_moves_list.append(point) - - monkeypatch.setattr(check_calibration_session, '_move', fake_move) - - sess = await in_jogging_first_pipette_to_height(check_calibration_session) - - await sess.trigger_transition( - session.CalibrationCheckTrigger.jog, types.Point(0, 0, -.8)) - - await sess.trigger_transition( - session.CalibrationCheckTrigger.compare_point) - - fake_moves_list.clear() - - await sess.trigger_transition( - session.CalibrationCheckTrigger.go_to_next_check, - ) - - await sess.trigger_transition( - session.CalibrationCheckTrigger.compare_point) - - last_point = fake_moves_list[0].point - # assert that the z value is the same for the last point after - # removing z buffer and jog move. - no_jog_and_buffer =\ - last_point + types.Point(0, 0, .8) -\ - types.Point(0, 0, 0.3) - assert sess._saved_points['joggingFirstPipetteToHeight'].z\ - == no_jog_and_buffer.z - - -async def test_session_started_to_labware_loaded(check_calibration_session): - check_calibration_session = await in_labware_loaded( - check_calibration_session - ) - assert check_calibration_session.current_state.name == \ - session.CalibrationCheckState.labwareLoaded - - -async def test_session_started_to_bad_state(check_calibration_session): - with pytest.raises(util.StateMachineError): - await check_calibration_session.trigger_transition( - session.CalibrationCheckTrigger.pick_up_tip - ) - - -async def test_session_no_pipettes_error(): - simulator = ThreadManager(API.build_hardware_simulator) - - with pytest.raises(RobotServerError) as e: - await session.CheckCalibrationSession.build(simulator) - - assert e.value.status_code == 403 - assert e.value.error.title == "No Pipette Attached" - - -async def test_session_started_to_end_state(check_calibration_session): - await check_calibration_session.trigger_transition( - session.CalibrationCheckTrigger.exit - ) - assert check_calibration_session.current_state.name == \ - session.CalibrationCheckState.sessionExited - - -async def test_diff_pips_diff_tipracks(check_calibration_session): - sess = check_calibration_session - await sess.trigger_transition( - session.CalibrationCheckTrigger.load_labware) - assert len(sess._labware_info.keys()) == 2 - for tiprack in sess._labware_info.values(): - assert len(tiprack.forMounts) == 1 - # loads tiprack for right mount in 8 - # and tiprack for left mount in 6 - assert sess._deck['8'] - assert sess._deck['8'].name == 'opentrons_96_tiprack_300ul' - assert sess._deck['6'] - assert sess._deck['6'].name == 'opentrons_96_tiprack_10ul' - - -async def test_same_size_pips_share_tiprack( - check_calibration_session_shared_tips): - sess = check_calibration_session_shared_tips - await sess.trigger_transition( - session.CalibrationCheckTrigger.load_labware) - assert len(sess._labware_info.keys()) == 1 - assert len(next(iter(sess._labware_info.values())).forMounts) == 2 - - # loads tiprack in 8 only - assert sess._deck['8'] - assert sess._deck['8'].name == 'opentrons_96_tiprack_300ul' - assert sess._deck['6'] is None - - # z and x values should be the same, but y should be different - # if accessing different tips (A1, B1) on same tiprack - assert sess._moves.preparingFirstPipette.position.x == \ - sess._moves.preparingSecondPipette.position.x - assert sess._moves.preparingFirstPipette.position.z == \ - sess._moves.preparingSecondPipette.position.z - assert sess._moves.preparingFirstPipette.position.y != \ - sess._moves.preparingSecondPipette.position.y - - -async def test_jog_pipette(check_calibration_session): - sess = await in_preparing_first_pipette(check_calibration_session) - - last_pos = await sess.hardware.gantry_position( - sess._get_pipette_by_rank(session.PipetteRank.first).mount) - - jog_vector_map = { - 'front': types.Point(0, -0.1, 0), - 'back': types.Point(0, 0.1, 0), - 'left': types.Point(-0.1, 0, 0), - 'right': types.Point(0.1, 0, 0), - 'up': types.Point(0, 0, 0.1), - 'down': types.Point(0, 0, -0.1) - } - for dir, vector in jog_vector_map.items(): - await sess.trigger_transition( - session.CalibrationCheckTrigger.jog, vector) - jog_pos = await sess.hardware.gantry_position( - sess._get_pipette_by_rank(session.PipetteRank.first).mount) - assert jog_pos == vector + last_pos - last_pos = jog_pos - - -async def test_first_pick_up_tip(check_calibration_session): - sess = await in_inspecting_first_tip(check_calibration_session) - first_pip = sess._get_pipette_by_rank(session.PipetteRank.first) - second_pip = sess._get_pipette_by_rank(session.PipetteRank.second) - assert sess.pipettes[first_pip.mount]['has_tip'] is True - assert sess.pipettes[first_pip.mount]['tip_length'] > 0.0 - assert sess.pipettes[second_pip.mount]['has_tip'] is False - - -async def test_second_pick_up_tip(check_calibration_session): - sess = await in_inspecting_second_tip(check_calibration_session) - first_pip = sess._get_pipette_by_rank(session.PipetteRank.first) - second_pip = sess._get_pipette_by_rank(session.PipetteRank.second) - assert sess.pipettes[second_pip.mount]['has_tip'] is True - assert sess.pipettes[second_pip.mount]['tip_length'] > 0.0 - assert sess.pipettes[first_pip.mount]['has_tip'] is False - - -async def test_invalidate_first_tip(check_calibration_session): - sess = await in_inspecting_first_tip(check_calibration_session) - first_pip = sess._get_pipette_by_rank(session.PipetteRank.first) - assert sess.pipettes[first_pip.mount]['has_tip'] is True - await sess.trigger_transition( - session.CalibrationCheckTrigger.invalidate_tip) - assert sess.current_state.name == \ - session.CalibrationCheckState.preparingFirstPipette - assert sess.pipettes[first_pip.mount]['has_tip'] is False - - -async def test_invalidate_second_tip(check_calibration_session): - sess = await in_inspecting_second_tip(check_calibration_session) - second_pip = sess._get_pipette_by_rank(session.PipetteRank.second) - assert sess.pipettes[second_pip.mount]['has_tip'] is True - await sess.trigger_transition( - session.CalibrationCheckTrigger.invalidate_tip) - assert sess.current_state.name == \ - session.CalibrationCheckState.preparingSecondPipette - assert sess.pipettes[second_pip.mount]['has_tip'] is False - - -async def test_complete_check_one_pip(check_calibration_session_only_right): - sess = await in_comparing_first_pipette_point_three( - check_calibration_session_only_right) - first_pip = sess._get_pipette_by_rank(session.PipetteRank.first) - assert sess.pipettes[first_pip.mount]['has_tip'] is True - await sess.trigger_transition( - session.CalibrationCheckTrigger.go_to_next_check) - assert sess.current_state.name == \ - session.CalibrationCheckState.checkComplete - - -async def test_complete_check_both_pips(check_calibration_session): - sess = await in_comparing_second_pipette_point_one( - check_calibration_session) - second_pip = sess._get_pipette_by_rank(session.PipetteRank.second) - assert sess.pipettes[second_pip.mount]['has_tip'] is True - await sess.trigger_transition( - session.CalibrationCheckTrigger.go_to_next_check) - assert sess.current_state.name == \ - session.CalibrationCheckState.checkComplete - - -# START flow testing both mounts - - -async def test_load_labware_to_preparing_first_pipette( - check_calibration_session): - sess = await in_preparing_first_pipette(check_calibration_session) - tip_pt = sess._moves.preparingFirstPipette.position - curr_pos = await sess.hardware.gantry_position( - sess._get_pipette_by_rank(session.PipetteRank.first).mount) - assert curr_pos == tip_pt - types.Point(0, 0, 10) - - assert check_calibration_session.current_state.name == \ - session.CalibrationCheckState.preparingFirstPipette - await check_calibration_session.trigger_transition( - session.CalibrationCheckTrigger.jog, OK_DIFF_VECTOR) - assert check_calibration_session.current_state.name == \ - session.CalibrationCheckState.preparingFirstPipette - - -async def test_preparing_first_pipette_to_bad_calibration( - check_calibration_session): - check_calibration_session = await in_preparing_first_pipette( - check_calibration_session) - await check_calibration_session.trigger_transition( - session.CalibrationCheckTrigger.jog, BAD_DIFF_VECTOR) - assert check_calibration_session.current_state.name == \ - session.CalibrationCheckState.preparingFirstPipette - await check_calibration_session.trigger_transition( - session.CalibrationCheckTrigger.pick_up_tip) - await check_calibration_session.trigger_transition( - session.CalibrationCheckTrigger.confirm_tip_attached) - assert check_calibration_session.current_state.name == \ - session.CalibrationCheckState.badCalibrationData - - -async def test_preparing_first_pipette_to_inspecting( - check_calibration_session): - await in_inspecting_first_tip(check_calibration_session) - assert check_calibration_session.current_state.name == \ - session.CalibrationCheckState.inspectingFirstTip - - -async def test_inspecting_first_pipette_to_jogging_height( - check_calibration_session): - sess = await in_jogging_first_pipette_to_height(check_calibration_session) - tip_pt = sess._moves.joggingFirstPipetteToHeight.position - curr_pos = await sess.hardware.gantry_position( - sess._get_pipette_by_rank(session.PipetteRank.first).mount) - assert curr_pos == tip_pt - assert check_calibration_session.current_state.name == \ - session.CalibrationCheckState.joggingFirstPipetteToHeight - await check_calibration_session.trigger_transition( - session.CalibrationCheckTrigger.jog, OK_DIFF_VECTOR) - assert check_calibration_session.current_state.name == \ - session.CalibrationCheckState.joggingFirstPipetteToHeight - - -async def test_jogging_first_pipette_height_to_comparing( - check_calibration_session): - await in_comparing_first_pipette_height(check_calibration_session) - assert check_calibration_session.current_state.name == \ - session.CalibrationCheckState.comparingFirstPipetteHeight - - -async def test_comparing_first_pipette_height_to_jogging_point_one( - check_calibration_session): - sess = await in_jogging_first_pipette_to_point_one( - check_calibration_session) - tip_pt = sess._moves.joggingFirstPipetteToPointOne.position - curr_pos = await sess.hardware.gantry_position( - sess._get_pipette_by_rank(session.PipetteRank.first).mount) - assert curr_pos == tip_pt + types.Point(0, 0, 5.3) - assert check_calibration_session.current_state.name == \ - session.CalibrationCheckState.joggingFirstPipetteToPointOne - await check_calibration_session.trigger_transition( - session.CalibrationCheckTrigger.jog, OK_DIFF_VECTOR) - assert check_calibration_session.current_state.name == \ - session.CalibrationCheckState.joggingFirstPipetteToPointOne - - -async def test_jogging_first_pipette_point_one_to_comparing( - check_calibration_session): - await in_comparing_first_pipette_point_one(check_calibration_session) - assert check_calibration_session.current_state.name == \ - session.CalibrationCheckState.comparingFirstPipettePointOne - - -async def test_comparing_first_pipette_point_one_to_jogging_point_two( - check_calibration_session): - sess = await in_jogging_first_pipette_to_point_two( - check_calibration_session) - tip_pt = sess._moves.joggingFirstPipetteToPointTwo.position - curr_pos = await sess.hardware.gantry_position( - sess._get_pipette_by_rank(session.PipetteRank.first).mount) - assert curr_pos == tip_pt + types.Point(0, 0, 5.3) - assert check_calibration_session.current_state.name == \ - session.CalibrationCheckState.joggingFirstPipetteToPointTwo - await check_calibration_session.trigger_transition( - session.CalibrationCheckTrigger.jog, OK_DIFF_VECTOR) - assert check_calibration_session.current_state.name == \ - session.CalibrationCheckState.joggingFirstPipetteToPointTwo - - -async def test_jogging_first_pipette_point_two_to_comparing( - check_calibration_session): - await in_comparing_first_pipette_point_two(check_calibration_session) - assert check_calibration_session.current_state.name == \ - session.CalibrationCheckState.comparingFirstPipettePointTwo - - -async def test_comparing_first_pipette_point_two_to_jogging_point_three( - check_calibration_session): - sess = await in_jogging_first_pipette_to_point_three( - check_calibration_session) - tip_pt = sess._moves.joggingFirstPipetteToPointThree.position - curr_pos = await sess.hardware.gantry_position( - sess._get_pipette_by_rank(session.PipetteRank.first).mount) - assert curr_pos == tip_pt + types.Point(0, 0, 5.3) - assert check_calibration_session.current_state.name == \ - session.CalibrationCheckState.joggingFirstPipetteToPointThree - await check_calibration_session.trigger_transition( - session.CalibrationCheckTrigger.jog, OK_DIFF_VECTOR) - assert check_calibration_session.current_state.name == \ - session.CalibrationCheckState.joggingFirstPipetteToPointThree - - -async def test_jogging_first_pipette_point_three_to_comparing( - check_calibration_session): - await in_comparing_first_pipette_point_three(check_calibration_session) - assert check_calibration_session.current_state.name == \ - session.CalibrationCheckState.comparingFirstPipettePointThree - - -async def test_load_labware_to_preparing_second_pipette( - check_calibration_session): - sess = await in_preparing_second_pipette(check_calibration_session) - tip_pt = sess._moves.preparingSecondPipette.position - curr_pos = await sess.hardware.gantry_position( - sess._get_pipette_by_rank(session.PipetteRank.second).mount) - assert curr_pos == tip_pt - types.Point(0, 0, 10) - assert check_calibration_session.current_state.name == \ - session.CalibrationCheckState.preparingSecondPipette - await check_calibration_session.trigger_transition( - session.CalibrationCheckTrigger.jog, OK_DIFF_VECTOR) - assert check_calibration_session.current_state.name == \ - session.CalibrationCheckState.preparingSecondPipette - - -async def test_preparing_second_pipette_to_inspecting( - check_calibration_session): - await in_inspecting_second_tip(check_calibration_session) - assert check_calibration_session.current_state.name == \ - session.CalibrationCheckState.inspectingSecondTip - - -async def test_inspecting_second_pipette_to_jogging_height( - check_calibration_session): - sess = await in_jogging_second_pipette_to_height( - check_calibration_session) - tip_pt = sess._moves.joggingSecondPipetteToHeight.position - curr_pos = await sess.hardware.gantry_position( - sess._get_pipette_by_rank(session.PipetteRank.second).mount) - assert curr_pos == tip_pt - assert check_calibration_session.current_state.name == \ - session.CalibrationCheckState.joggingSecondPipetteToHeight - await check_calibration_session.trigger_transition( - session.CalibrationCheckTrigger.jog, OK_DIFF_VECTOR) - assert check_calibration_session.current_state.name == \ - session.CalibrationCheckState.joggingSecondPipetteToHeight - - -async def test_jogging_second_pipette_height_to_comparing( - check_calibration_session): - await in_comparing_second_pipette_height(check_calibration_session) - assert check_calibration_session.current_state.name == \ - session.CalibrationCheckState.comparingSecondPipetteHeight - - -async def test_comparing_second_pipette_height_to_jogging_point_one( - check_calibration_session): - sess = await in_jogging_second_pipette_to_point_one( - check_calibration_session) - tip_pt = sess._moves.joggingSecondPipetteToPointOne.position - curr_pos = await sess.hardware.gantry_position( - sess._get_pipette_by_rank(session.PipetteRank.second).mount) - rounded_pos = types.Point( - round(curr_pos[0], 2), - round(curr_pos[1], 2), - round(curr_pos[2], 2)) - assert rounded_pos == tip_pt + types.Point(0, 0, 5.3) - assert check_calibration_session.current_state.name == \ - session.CalibrationCheckState.joggingSecondPipetteToPointOne - await check_calibration_session.trigger_transition( - session.CalibrationCheckTrigger.jog, OK_DIFF_VECTOR) - assert check_calibration_session.current_state.name == \ - session.CalibrationCheckState.joggingSecondPipetteToPointOne - - -async def test_jogging_second_pipette_point_one_to_comparing( - check_calibration_session): - await in_comparing_second_pipette_point_one(check_calibration_session) - assert check_calibration_session.current_state.name == \ - session.CalibrationCheckState.comparingSecondPipettePointOne - - -# END flow testing both mounts - -# START flow testing right only - - -async def test_right_load_labware_to_preparing_first_pipette( - check_calibration_session_only_right): - await in_preparing_first_pipette( - check_calibration_session_only_right) - assert check_calibration_session_only_right.current_state.name == \ - session.CalibrationCheckState.preparingFirstPipette - - -async def test_right_preparing_first_pipette_to_inspecting( - check_calibration_session_only_right): - await in_inspecting_first_tip( - check_calibration_session_only_right) - assert check_calibration_session_only_right.current_state.name == \ - session.CalibrationCheckState.inspectingFirstTip - - -async def test_right_inspecting_first_pipette_to_jogging_height( - check_calibration_session_only_right): - await in_jogging_first_pipette_to_height( - check_calibration_session_only_right) - assert check_calibration_session_only_right.current_state.name == \ - session.CalibrationCheckState.joggingFirstPipetteToHeight - - -async def test_right_jogging_first_pipette_height_to_comparing( - check_calibration_session_only_right): - await in_comparing_first_pipette_height( - check_calibration_session_only_right) - assert check_calibration_session_only_right.current_state.name == \ - session.CalibrationCheckState.comparingFirstPipetteHeight - - -async def test_right_comparing_first_pipette_height_to_jogging_point_one( - check_calibration_session_only_right): - await in_jogging_first_pipette_to_point_one( - check_calibration_session_only_right) - assert check_calibration_session_only_right.current_state.name == \ - session.CalibrationCheckState.joggingFirstPipetteToPointOne - - -async def test_right_jogging_first_pipette_point_one_to_comparing( - check_calibration_session_only_right): - await in_comparing_first_pipette_point_one( - check_calibration_session_only_right) - assert check_calibration_session_only_right.current_state.name == \ - session.CalibrationCheckState.comparingFirstPipettePointOne - - -async def test_right_comparing_first_pipette_point_one_to_jogging_point_two( - check_calibration_session_only_right): - await in_jogging_first_pipette_to_point_two( - check_calibration_session_only_right) - assert check_calibration_session_only_right.current_state.name == \ - session.CalibrationCheckState.joggingFirstPipetteToPointTwo - - -async def test_right_jogging_first_pipette_point_two_to_comparing( - check_calibration_session_only_right): - await in_comparing_first_pipette_point_two( - check_calibration_session_only_right) - assert check_calibration_session_only_right.current_state.name == \ - session.CalibrationCheckState.comparingFirstPipettePointTwo - - -async def test_right_comparing_first_pipette_point_two_to_jogging_point_three( - check_calibration_session_only_right): - await in_jogging_first_pipette_to_point_three( - check_calibration_session_only_right) - assert check_calibration_session_only_right.current_state.name == \ - session.CalibrationCheckState.joggingFirstPipetteToPointThree - - -async def test_right_jogging_first_pipette_point_three_to_comparing( - check_calibration_session_only_right): - await in_comparing_first_pipette_point_three( - check_calibration_session_only_right) - assert check_calibration_session_only_right.current_state.name == \ - session.CalibrationCheckState.comparingFirstPipettePointThree - - -async def test_right_jogging_first_pipette_point_three_to_complete( - check_calibration_session_only_right): - await in_comparing_first_pipette_point_three( - check_calibration_session_only_right) - await check_calibration_session_only_right.trigger_transition( - session.CalibrationCheckTrigger.go_to_next_check) - assert check_calibration_session_only_right.current_state.name == \ - session.CalibrationCheckState.checkComplete - - -# END flow testing right only - -# START flow testing left only - - -async def test_left_load_labware_to_preparing_first_pipette( - check_calibration_session_only_left): - await in_preparing_first_pipette(check_calibration_session_only_left) - assert check_calibration_session_only_left.current_state.name == \ - session.CalibrationCheckState.preparingFirstPipette - - -async def test_left_preparing_first_pipette_to_inspecting( - check_calibration_session_only_left): - await in_inspecting_first_tip(check_calibration_session_only_left) - assert check_calibration_session_only_left.current_state.name == \ - session.CalibrationCheckState.inspectingFirstTip - - -async def test_left_inspecting_first_pipette_to_jogging_height( - check_calibration_session_only_left): - await in_jogging_first_pipette_to_height( - check_calibration_session_only_left) - assert check_calibration_session_only_left.current_state.name == \ - session.CalibrationCheckState.joggingFirstPipetteToHeight - - -async def test_left_jogging_first_pipette_height_to_comparing( - check_calibration_session_only_left): - await in_comparing_first_pipette_height( - check_calibration_session_only_left) - assert check_calibration_session_only_left.current_state.name == \ - session.CalibrationCheckState.comparingFirstPipetteHeight - - -async def test_left_comparing_first_pipette_height_to_jogging_point_one( - check_calibration_session_only_left): - await in_jogging_first_pipette_to_point_one( - check_calibration_session_only_left) - assert check_calibration_session_only_left.current_state.name == \ - session.CalibrationCheckState.joggingFirstPipetteToPointOne - - -async def test_left_jogging_first_pipette_point_one_to_comparing( - check_calibration_session_only_left): - await in_comparing_first_pipette_point_one( - check_calibration_session_only_left) - assert check_calibration_session_only_left.current_state.name == \ - session.CalibrationCheckState.comparingFirstPipettePointOne - - -async def test_left_comparing_first_pipette_point_one_to_jogging_point_two( - check_calibration_session_only_left): - await in_jogging_first_pipette_to_point_two( - check_calibration_session_only_left) - assert check_calibration_session_only_left.current_state.name == \ - session.CalibrationCheckState.joggingFirstPipetteToPointTwo - - -async def test_left_jogging_first_pipette_point_two_to_comparing( - check_calibration_session_only_left): - await in_comparing_first_pipette_point_two( - check_calibration_session_only_left) - assert check_calibration_session_only_left.current_state.name == \ - session.CalibrationCheckState.comparingFirstPipettePointTwo - - -async def test_left_comparing_first_pipette_point_two_to_jogging_point_three( - check_calibration_session_only_left): - await in_jogging_first_pipette_to_point_three( - check_calibration_session_only_left) - assert check_calibration_session_only_left.current_state.name == \ - session.CalibrationCheckState.joggingFirstPipetteToPointThree - - -async def test_left_jogging_first_pipette_point_three_to_comparing( - check_calibration_session_only_left): - await in_comparing_first_pipette_point_three( - check_calibration_session_only_left) - assert check_calibration_session_only_left.current_state.name == \ - session.CalibrationCheckState.comparingFirstPipettePointThree - - -async def test_left_jogging_first_pipette_point_three_to_complete( - check_calibration_session_only_left): - await in_comparing_first_pipette_point_three( - check_calibration_session_only_left) - await check_calibration_session_only_left.trigger_transition( - session.CalibrationCheckTrigger.go_to_next_check) - assert check_calibration_session_only_left.current_state.name == \ - session.CalibrationCheckState.checkComplete - - -# END flow testing left only diff --git a/robot-server/tests/robot/calibration/check/test_state_machine.py b/robot-server/tests/robot/calibration/check/test_state_machine.py new file mode 100644 index 00000000000..51a20bcc624 --- /dev/null +++ b/robot-server/tests/robot/calibration/check/test_state_machine.py @@ -0,0 +1,51 @@ +import pytest +from typing import List, Tuple + +from robot_server.service.session.models.command import ( + CalibrationCommand as CalCommand, + DeckCalibrationCommand as DeckCommand, + CheckCalibrationCommand as CheckCommand) +from robot_server.robot.calibration.check.state_machine import \ + CalibrationCheckStateMachine + +valid_commands: List[Tuple[str, str, str]] = [ + (CalCommand.load_labware, 'sessionStarted', 'labwareLoaded'), + (CalCommand.move_to_tip_rack, 'labwareLoaded', 'preparingPipette'), + (CalCommand.jog, 'preparingPipette', 'preparingPipette'), + (CalCommand.pick_up_tip, 'preparingPipette', 'inspectingTip'), + (CalCommand.invalidate_tip, 'inspectingTip', 'preparingPipette'), + (CalCommand.move_to_deck, 'inspectingTip', 'comparingHeight'), + (CalCommand.jog, 'comparingHeight', 'comparingHeight'), + (CheckCommand.compare_point, 'comparingHeight', 'comparingHeight'), + (CalCommand.move_to_point_one, 'comparingHeight', 'comparingPointOne'), + (CalCommand.jog, 'comparingPointOne', 'comparingPointOne'), + (CheckCommand.compare_point, 'comparingPointOne', 'comparingPointOne'), + (DeckCommand.move_to_point_two, 'comparingPointOne', 'comparingPointTwo'), + (CalCommand.jog, 'comparingPointTwo', 'comparingPointTwo'), + (CheckCommand.compare_point, 'comparingPointTwo', 'comparingPointTwo'), + (DeckCommand.move_to_point_three, + 'comparingPointTwo', 'comparingPointThree'), + (CalCommand.jog, 'comparingPointThree', 'comparingPointThree'), + (CheckCommand.compare_point, 'comparingPointThree', 'comparingPointThree'), + (CalCommand.move_to_tip_rack, 'comparingPointThree', 'returningTip'), + (CheckCommand.return_tip, 'returningTip', 'returningTip'), + (CheckCommand.transition, 'returningTip', 'resultsSummary'), + (CalCommand.exit, 'calibrationComplete', 'sessionExited'), + (CalCommand.exit, 'sessionStarted', 'sessionExited'), + (CalCommand.exit, 'labwareLoaded', 'sessionExited'), + (CalCommand.exit, 'preparingPipette', 'sessionExited'), + (CalCommand.exit, 'comparingPointOne', 'sessionExited'), + (CalCommand.exit, 'comparingPointTwo', 'sessionExited'), + (CalCommand.exit, 'comparingPointThree', 'sessionExited'), +] + + +@pytest.fixture +def state_machine(): + return CalibrationCheckStateMachine() + + +@pytest.mark.parametrize('command,from_state,to_state', valid_commands) +async def test_valid_commands(command, from_state, to_state, state_machine): + next_state = state_machine.get_next_state(from_state, command) + assert next_state == to_state diff --git a/robot-server/tests/robot/calibration/check/test_user_flow.py b/robot-server/tests/robot/calibration/check/test_user_flow.py new file mode 100644 index 00000000000..ef2400dc6d5 --- /dev/null +++ b/robot-server/tests/robot/calibration/check/test_user_flow.py @@ -0,0 +1,259 @@ +from typing import List, Tuple +from unittest.mock import call, MagicMock + +import pytest +from opentrons.hardware_control import pipette +from opentrons.types import Mount, Point +from opentrons.calibration_storage import types as cal_types +from opentrons.config import robot_configs +from opentrons.config.pipette_config import load + +from robot_server.robot.calibration.check.user_flow import\ + CheckCalibrationUserFlow +from robot_server.robot.calibration.check.constants import\ + CalibrationCheckState +from robot_server.robot.calibration.check.models import\ + ComparisonStatus +from robot_server.service.errors import RobotServerError +from robot_server.robot.calibration.constants import ( + POINT_ONE_ID, POINT_TWO_ID, POINT_THREE_ID) + + +PIP_OFFSET = cal_types.PipetteOffsetByPipetteMount( + offset=robot_configs.DEFAULT_PIPETTE_OFFSET, + source=cal_types.SourceType.user, + status=cal_types.CalibrationStatus()) + + +@pytest.fixture +def mock_hw(hardware): + pip = pipette.Pipette(load("p300_single_v2.1", 'testiId'), + { + 'single': [0, 0, 0], + 'multi': [0, 0, 0] + }, + PIP_OFFSET, + 'testId') + hardware._attached_instruments = {Mount.RIGHT: pip, Mount.LEFT: pip} + hardware._current_pos = Point(0, 0, 0) + + async def async_mock(*args, **kwargs): + pass + + async def async_mock_move_rel(*args, **kwargs): + delta = kwargs.get('delta', Point(0, 0, 0)) + hardware._current_pos += delta + + async def async_mock_move_to(*args, **kwargs): + to_pt = kwargs.get('abs_position', Point(0, 0, 0)) + hardware._current_pos = to_pt + + async def gantry_pos_mock(*args, **kwargs): + return hardware._current_pos + + hardware.move_rel = MagicMock(side_effect=async_mock_move_rel) + hardware.pick_up_tip = MagicMock(side_effect=async_mock) + hardware.drop_tip = MagicMock(side_effect=async_mock) + hardware.gantry_position = MagicMock(side_effect=gantry_pos_mock) + hardware.move_to = MagicMock(side_effect=async_mock_move_to) + hardware.get_instrument_max_height.return_value = 180 + return hardware + + +pipette_combos: List[Tuple[List[str], Mount]] = [ + (['p20_multi_v2.1', 'p20_multi_v2.1'], Mount.RIGHT), + (['p20_single_v2.1', 'p20_multi_v2.1'], Mount.LEFT), + (['p20_multi_v2.1', 'p300_single_v2.1'], Mount.RIGHT), + (['p300_multi_v2.1', 'p1000_single_v2.1'], Mount.RIGHT), + (['p1000_single_v2.1', ''], Mount.LEFT), + (['', 'p300_multi_v2.1'], Mount.RIGHT) +] + + +@pytest.mark.parametrize('pipettes,target_mount', pipette_combos) +def test_user_flow_select_pipette(pipettes, target_mount, hardware): + pip, pip2 = None, None + if pipettes[0]: + pip = pipette.Pipette(load(pipettes[0], 'testId'), + {'single': [0, 0, 0], 'multi': [0, 0, 0]}, + PIP_OFFSET, + 'testId') + if pipettes[1]: + pip2 = pipette.Pipette(load(pipettes[1], 'testId'), + {'single': [0, 0, 0], 'multi': [0, 0, 0]}, + PIP_OFFSET, + 'testId2') + hardware._attached_instruments = {Mount.LEFT: pip, Mount.RIGHT: pip2} + + uf = CheckCalibrationUserFlow(hardware=hardware) + assert uf.hw_pipette == \ + hardware._attached_instruments[target_mount] + + +@pytest.mark.parametrize('pipettes,target_mount', pipette_combos) +async def test_switching_to_second_pipette(pipettes, target_mount, hardware): + pip, pip2 = None, None + if pipettes[0]: + pip = pipette.Pipette(load(pipettes[0], 'testId'), + {'single': [0, 0, 0], 'multi': [0, 0, 0]}, + PIP_OFFSET, + 'testId') + if pipettes[1]: + pip2 = pipette.Pipette(load(pipettes[1], 'testId'), + {'single': [0, 0, 0], 'multi': [0, 0, 0]}, + PIP_OFFSET, + 'testId2') + hardware._attached_instruments = {Mount.LEFT: pip, Mount.RIGHT: pip2} + uf = CheckCalibrationUserFlow(hardware=hardware) + if pip and pip2: + assert uf.mount == target_mount + await uf.change_active_pipette() + assert uf.mount != target_mount + else: + with pytest.raises(RobotServerError): + await uf.change_active_pipette() + + +@pytest.fixture +def mock_user_flow(mock_hw): + m = CheckCalibrationUserFlow(hardware=mock_hw) + initial_pt = Point(1, 1, 5) + final_pt = Point(1, 1, 0) + m._get_reference_points_by_state =\ + MagicMock(return_value=(initial_pt, final_pt)) + yield m + + +async def test_move_to_tip_rack(mock_user_flow): + uf = mock_user_flow + await uf.move_to_tip_rack() + cur_pt = await uf.get_current_point(None) + assert cur_pt == uf.active_tiprack.wells()[0].top().point + Point(0, 0, 10) + + +async def test_pick_up_tip(mock_user_flow): + uf = mock_user_flow + assert uf._tip_origin_pt is None + await uf.move_to_tip_rack() + cur_pt = await uf.get_current_point(None) + await uf.jog(vector=(0, 0, 1)) + await uf.pick_up_tip() + assert uf._tip_origin_pt == cur_pt + Point(0, 0, 1) + + +async def test_return_tip(mock_user_flow): + uf = mock_user_flow + uf._tip_origin_pt = Point(1, 1, 1) + uf.hw_pipette._has_tip = True + z_offset = uf.hw_pipette.config.return_tip_height * \ + uf._get_tip_length() + await uf.return_tip() + # should move to return tip + move_calls = [ + call( + mount=Mount.RIGHT, + abs_position=Point(1, 1, 1 - z_offset), + critical_point=uf.critical_point_override + ), + ] + uf._hardware.move_to.assert_has_calls(move_calls) + uf._hardware.drop_tip.assert_called() + + +async def test_jog(mock_user_flow): + uf = mock_user_flow + await uf.jog(vector=(0, 0, 0.1)) + assert await uf.get_current_point(None) == Point(0, 0, 0.1) + await uf.jog(vector=(1, 0, 0)) + assert await uf.get_current_point(None) == Point(1, 0, 0.1) + + +@pytest.mark.parametrize( + "state,point_id", [ + (CalibrationCheckState.comparingHeight, POINT_ONE_ID), + (CalibrationCheckState.comparingPointOne, POINT_TWO_ID), + (CalibrationCheckState.comparingPointTwo, POINT_THREE_ID)]) +async def test_get_move_to_cal_point_location(mock_user_flow, + state, point_id): + uf = mock_user_flow + uf._z_height_reference = 30 + + pt_list = uf._deck.get_calibration_position(point_id).position + exp = Point(pt_list[0], pt_list[1], 30) + + uf._current_state = state + assert uf._get_move_to_point_loc_by_state().point == exp + + +async def test_compare_z_height(mock_user_flow): + uf = mock_user_flow + uf._current_state = CalibrationCheckState.comparingHeight + await uf._hardware.move_to( + mount=uf._mount, + abs_position=Point(x=10, y=10, z=10), + critical_point=uf.hw_pipette.critical_point + ) + await uf.update_comparison_map() + # The initial and final mocked points have a 5 mm + # difference and so it should exceed the threshold + expected_status = ComparisonStatus( + differenceVector=(0.0, 0.0, -5.0), + thresholdVector=(0.0, 0.0, 0.8), + exceedsThreshold=True, + transformType='BAD_DECK_TRANSFORM') + assert uf.comparison_map.first.comparingHeight == expected_status + assert uf.comparison_map.second.comparingHeight is None + + +async def test_compare_points(mock_user_flow): + uf = mock_user_flow + uf._current_state = CalibrationCheckState.comparingPointOne + + expected_status = ComparisonStatus( + differenceVector=(0.0, 0.0, -5.0), + thresholdVector=(1.8, 1.8, 0.0), + exceedsThreshold=False, + transformType='UNKNOWN') + await uf._hardware.move_to( + mount=uf._mount, + abs_position=Point(x=10, y=10, z=10), + critical_point=uf.hw_pipette.critical_point + ) + await uf.update_comparison_map() + + assert uf.comparison_map.first.comparingPointOne == expected_status + assert uf.comparison_map.second.comparingPointOne is None + + uf._current_state = CalibrationCheckState.comparingPointTwo + await uf._hardware.move_to( + mount=uf._mount, + abs_position=Point(x=10, y=10, z=10), + critical_point=uf.hw_pipette.critical_point + ) + await uf.update_comparison_map() + assert uf.comparison_map.first.comparingPointTwo == expected_status + assert uf.comparison_map.second.comparingPointTwo is None + + uf._current_state = CalibrationCheckState.comparingPointThree + await uf._hardware.move_to( + mount=uf._mount, + abs_position=Point(x=10, y=10, z=10), + critical_point=uf.hw_pipette.critical_point + ) + await uf.update_comparison_map() + + assert uf.comparison_map.first.comparingPointThree == expected_status + assert uf.comparison_map.second.comparingPointThree is None + + await uf.change_active_pipette() + + uf._current_state = CalibrationCheckState.comparingPointOne + await uf._hardware.move_to( + mount=uf._mount, + abs_position=Point(x=10, y=10, z=10), + critical_point=uf.hw_pipette.critical_point + ) + await uf.update_comparison_map() + + assert uf.comparison_map.first.comparingPointOne == expected_status + assert uf.comparison_map.second.comparingPointOne == expected_status diff --git a/robot-server/tests/robot/calibration/check/test_util.py b/robot-server/tests/robot/calibration/check/test_util.py deleted file mode 100644 index 63ee1dcbb02..00000000000 --- a/robot-server/tests/robot/calibration/check/test_util.py +++ /dev/null @@ -1,114 +0,0 @@ -import pytest - -from robot_server.robot.calibration.check import util - - -@pytest.fixture -def machine(loop): - states = ['Working', - 'ThinkingAboutCats', - { - 'name': 'BrowsingCatPictures', - }, - 'LookingAtTime', - {'name': "Sleeping"}, - 'Dreaming', - 'OnVacation'] - transitions = [ - {"trigger": "start_thinking_about_cats", "from_state": "Working", - "to_state": "ThinkingAboutCats"}, - {"trigger": "look_at_time", "from_state": "ThinkingAboutCats", - "to_state": "Working"}, - {"trigger": "write_some_code", "from_state": "Working", - "to_state": "Working", "after": "update_written_code"}, - {"trigger": "look_at_cats", "from_state": "ThinkingAboutCats", - "to_state": "BrowsingCatPictures", "after": "open_reddit_tab"}, - {"trigger": "look_at_time", "from_state": "BrowsingCatPictures", - "to_state": "Working"}, - {"trigger": "become_exhausted", "from_state": "Working", - "to_state": "Sleeping"}, - {"trigger": "become_exhausted", "from_state": "ThinkingAboutCats", - "to_state": "Sleeping"}, - {"trigger": "become_exhausted", "from_state": "BrowsingCatPictures", - "to_state": "Sleeping", "before": ["close_reddit_tab", "lay_in_bed"], - "after": "reach_rem"}, - {"trigger": "start_dreaming", "from_state": "*", - "to_state": "Dreaming"}, - {"trigger": "dream_about_code", "from_state": "Dreaming", - "to_state": "Working"}, - {"trigger": "go_on_a_trip", "from_state": "*", "to_state": "OnVacation"} - ] - - class TestModelWithMachine(util.StateMachine): - def __init__(self): - super().__init__(states=states, - transitions=transitions, - initial_state="Working") - self.reached_rem = False - self.in_bed = False - self.code_written = '' - self.tabs_open = {'github'} - - async def reach_rem(self): - self.reached_rem = True - - async def lay_in_bed(self): - self.in_bed = True - - async def update_written_code(self, code: str): - self.code_written = code - - async def open_reddit_tab(self): - self.tabs_open.add('reddit') - - async def close_reddit_tab(self): - self.tabs_open.remove('reddit') - - return TestModelWithMachine() - - -async def test_state_machine(machine): - # normal transitions update state - assert machine.current_state.name == 'Working' - await machine.trigger_transition("start_thinking_about_cats") - assert machine.current_state.name == 'ThinkingAboutCats' - await machine.trigger_transition("look_at_time") - assert machine.current_state.name == 'Working' - - # transitions with enter/exit callback - # receives params and updates state - assert machine.code_written == '' - await machine.trigger_transition("write_some_code", 'fake_code') - assert machine.code_written == 'fake_code' - assert machine.current_state.name == 'Working' - - # states with enter/exit callbacks updates state and side effects - await machine.trigger_transition("start_thinking_about_cats") - assert machine.current_state.name == 'ThinkingAboutCats' - assert 'reddit' not in machine.tabs_open - await machine.trigger_transition("look_at_cats") - assert machine.current_state.name == 'BrowsingCatPictures' - assert 'reddit' in machine.tabs_open - assert not machine.reached_rem - assert not machine.in_bed - # check that you can have mulitple actions in the before/after - # callbacks. - await machine.trigger_transition("become_exhausted") - assert machine.current_state.name == 'Sleeping' - assert 'reddit' not in machine.tabs_open - assert machine.reached_rem - assert machine.in_bed - - # wild card from_state transitions - await machine.trigger_transition("start_dreaming") - assert machine.current_state.name == 'Dreaming' - await machine.trigger_transition("dream_about_code") - assert machine.current_state.name == 'Working' - await machine.trigger_transition("go_on_a_trip") - assert machine.current_state.name == 'OnVacation' - - # trigger fails if no transition from current state - try: - await machine.trigger_transition("write_some_code", 'other_fake_code') - except util.StateMachineError: - pass diff --git a/robot-server/tests/service/session/session_types/test_check_session.py b/robot-server/tests/service/session/session_types/test_check_session.py deleted file mode 100644 index 40415f749ce..00000000000 --- a/robot-server/tests/service/session/session_types/test_check_session.py +++ /dev/null @@ -1,213 +0,0 @@ -import pytest -from unittest.mock import MagicMock, PropertyMock, patch - -from robot_server.robot.calibration.check.session import \ - CheckCalibrationSession, CalibrationCheckState, CalibrationCheckTrigger -from robot_server.robot.calibration.helper_classes import PipetteInfo,\ - PipetteRank -from opentrons import types -from robot_server.robot.calibration.check.util import StateMachineError - -from robot_server.service.session.command_execution import create_command -from robot_server.service.session.configuration import SessionConfiguration -from robot_server.service.session.models.common import ( - EmptyModel, JogPosition) -from robot_server.service.session.models.command import CalibrationCommand -from robot_server.service.session.session_types import CheckSession, \ - SessionMetaData, BaseSession - -from robot_server.service.session.errors import SessionCreationException, \ - SessionCommandException - - -@pytest.fixture -def mock_cal_session(hardware): - mock_pipette_info_by_mount = { - types.Mount.LEFT: PipetteInfo( - tiprack_id=None, - critical_point=None, - rank=PipetteRank.second, - mount=types.Mount.LEFT, - ), - types.Mount.RIGHT: PipetteInfo( - tiprack_id=None, - critical_point=None, - rank=PipetteRank.first, - mount=types.Mount.RIGHT - ) - } - mock_hw_pipettes = { - types.Mount.LEFT: { - 'model': 'p10_single_v1', - 'has_tip': False, - 'max_volume': 10, - 'name': 'p10_single', - 'tip_length': 0, - 'channels': 1, - 'pipette_id': 'pipette id 1'}, - types.Mount.RIGHT: { - 'model': 'p300_single_v1', - 'has_tip': False, - 'max_volume': 300, - 'name': 'p300_single', - 'tip_length': 0, - 'channels': 1, - 'pipette_id': 'pipette id 2'} - } - - CheckCalibrationSession._get_pip_info_by_mount =\ - MagicMock(return_value=mock_pipette_info_by_mount) - CheckCalibrationSession.pipettes = mock_hw_pipettes - - m = CheckCalibrationSession(hardware) - - async def async_mock(*args, **kwargs): - pass - - m.trigger_transition = MagicMock(side_effect=async_mock) - m.delete_session = MagicMock(side_effect=async_mock) - - path = 'robot_server.robot.calibration.check.session.' \ - 'CheckCalibrationSession.current_state_name' - with patch(path, new_callable=PropertyMock) as p: - p.return_value = CalibrationCheckState.preparingFirstPipette.value - - m.get_potential_triggers = MagicMock(return_value={ - CalibrationCheckTrigger.jog, - CalibrationCheckTrigger.pick_up_tip, - CalibrationCheckTrigger.exit - }) - yield m - - -@pytest.fixture -def patch_build_session(mock_cal_session): - r = "robot_server.service.session.session_types." \ - "check_session.CheckCalibrationSession.build" - with patch(r) as p: - async def build(hardware): - return mock_cal_session - p.side_effect = build - yield p - - -@pytest.fixture -def check_session_instance(patch_build_session, hardware, loop) -> BaseSession: - return loop.run_until_complete( - CheckSession.create( - configuration=SessionConfiguration(hardware=hardware, - is_active=lambda x: False, - motion_lock=None, - protocol_manager=None), - instance_meta=SessionMetaData() - ) - ) - - -@pytest.fixture -def session_hardware_info(mock_cal_session): - current_state = mock_cal_session.current_state_name - lw_status = mock_cal_session.labware_status.values() - comparisons_by_step = mock_cal_session.get_comparisons_by_step() - instruments = { - str(k): {'model': v.model, - 'name': v.name, - 'tip_length': v.tip_length, - 'mount': v.mount, - 'has_tip': v.has_tip, - 'tiprack_id': v.tiprack_id, - 'rank': v.rank, - 'serial': v.serial} - for k, v in mock_cal_session.pipette_status().items() - } - info = { - 'instruments': instruments, - 'labware': [{ - 'alternatives': data.alternatives, - 'slot': data.slot, - 'id': data.id, - 'forMounts': [str(m) for m in data.forMounts], - 'loadName': data.loadName, - 'namespace': data.namespace, - 'version': str(data.version)} for data in lw_status], - 'currentStep': current_state, - 'comparisonsByStep': comparisons_by_step, - 'nextSteps': None, - } - return info - - -@pytest.mark.parametrize(argnames="build_exception", - argvalues=[AssertionError]) -async def test_create_session_error(hardware, patch_build_session, - build_exception): - async def raiser(x): - raise build_exception("Please attach pipettes before proceeding") - - patch_build_session.side_effect = raiser - - with pytest.raises(SessionCreationException): - await CheckSession.create( - configuration=SessionConfiguration(hardware=hardware, - is_active=lambda x: False, - motion_lock=None, - protocol_manager=None), - instance_meta=SessionMetaData() - ) - - -async def test_clean_up_deletes_session(check_session_instance, - mock_cal_session): - await check_session_instance.clean_up() - mock_cal_session.delete_session.assert_called_once() - - -def test_get_response_details(check_session_instance, session_hardware_info): - response = check_session_instance._get_response_details() - assert response.dict() == session_hardware_info - - -async def test_session_command_execute(check_session_instance, - mock_cal_session): - await check_session_instance.command_executor.execute( - create_command( - CalibrationCommand.jog, - JogPosition(vector=(1, 2, 3))) - ) - - mock_cal_session.trigger_transition.assert_called_once_with( - trigger="calibration.jog", - vector=(1.0, 2.0, 3.0) - ) - - -async def test_session_command_execute_no_body(check_session_instance, - mock_cal_session): - await check_session_instance.command_executor.execute( - create_command( - CalibrationCommand.load_labware, - EmptyModel()) - ) - - mock_cal_session.trigger_transition.assert_called_once_with( - trigger="calibration.loadLabware" - ) - - -@pytest.mark.parametrize(argnames="command_exception", - argvalues=[AssertionError, - StateMachineError]) -async def test_session_command_execute_raise(check_session_instance, - mock_cal_session, - command_exception): - - async def raiser(*args, **kwargs): - raise command_exception("Cannot do it") - - mock_cal_session.trigger_transition.side_effect = raiser - - with pytest.raises(SessionCommandException): - await check_session_instance.command_executor.execute( - create_command(CalibrationCommand.jog, - JogPosition(vector=(1, 2, 3))) - )
- {JOG_UNTIL} - {JUST_BARELY_TOUCHING} - {DECK_IN} - {SLOT_5}. - - - {THEN} - {CHECK_AXES} - {TO_DETERMINE_MATCH} -
- {preamble} - - {displayName} - - {INSPECTING_COMPARISON} -
- -
{CONTINUE_BLURB}
- {JOG_UNTIL} - {PRECISELY_CENTERED} - {ABOVE_THE_CROSS} - {`${SLOT} ${slotNumber || ''}`}. - - - {THEN} - {CHECK_AXES} - {TO_DETERMINE_MATCH} -
{DECK_SETUP_PROMPT}
{ROBOT_CALIBRATION_INTRO_HEADER}
- {ROBOT_CALIBRATION_INTRO_INSTRUCTION} -
- {ROBOT_CALIBRATION_INTRO_OUTCOMES} -
- {getLatestLabwareDef(loadName)?.metadata.displayName} -
- {NOTE_HEADER} - {NOTE_BODY} -
- {getBadOutcomeHeader(comparison.transformType)} -
{STILL_HAVING_PROBLEMS}
- {CONFIRM_TIP_BODY} -
- {jogUntilAbove} - {tipRackWellName} - {`${POSITION} ${AND}`} - {FLUSH} - {WITH_TOP_OF_TIP} -