diff --git a/app/src/robot-controls/__fixtures__/index.js b/app/src/robot-controls/__fixtures__/index.js index 629d9a09cd4..37998a24778 100644 --- a/app/src/robot-controls/__fixtures__/index.js +++ b/app/src/robot-controls/__fixtures__/index.js @@ -2,3 +2,4 @@ export * from './lights' export * from './home' +export * from './move' diff --git a/app/src/robot-controls/__fixtures__/move.js b/app/src/robot-controls/__fixtures__/move.js new file mode 100644 index 00000000000..8baadb74335 --- /dev/null +++ b/app/src/robot-controls/__fixtures__/move.js @@ -0,0 +1,101 @@ +// @flow + +import { mockRobot } from '../../robot-api/__fixtures__' + +// POST /robot/move + +export const mockMoveSuccessMeta = { + method: 'POST', + path: '/robot/move', + ok: true, + status: 200, +} + +export const mockMoveSuccess = { + ...mockMoveSuccessMeta, + host: mockRobot, + body: { message: 'Move complete. New position: [1, 2, 3]' }, +} + +export const mockMoveFailureMeta = { + method: 'POST', + path: '/robot/move', + ok: false, + status: 500, +} + +export const mockMoveFailure = { + ...mockMoveFailureMeta, + host: mockRobot, + body: { message: 'AH' }, +} + +// GET /robot/positions + +export const mockPositions = { + change_pipette: { + target: 'mount', + left: [325, 40, 30], + right: [65, 40, 30], + }, + attach_tip: { + target: 'pipette', + point: [200, 90, 150], + }, +} + +export const mockFetchPositionsSuccessMeta = { + method: 'GET', + path: '/robot/positions', + ok: true, + status: 200, +} + +export const mockFetchPositionsSuccess = { + ...mockFetchPositionsSuccessMeta, + host: mockRobot, + body: { + positions: mockPositions, + }, +} + +export const mockFetchPositionsFailureMeta = { + method: 'GET', + path: '/robot/positions', + ok: false, + status: 500, +} + +export const mockFetchPositionsFailure = { + ...mockFetchPositionsFailureMeta, + host: mockRobot, + body: { message: 'AH' }, +} + +// POST /motors/disengage + +export const mockDisengageMotorsSuccessMeta = { + method: 'POST', + path: '/motors/disengage', + ok: true, + status: 200, +} + +export const mockDisengageMotorsSuccess = { + ...mockDisengageMotorsSuccessMeta, + host: mockRobot, + body: { message: 'Disengaged axes: [a, b, c, x, y, z]' }, +} + +export const mockDisengageMotorsFailureMeta = { + method: 'POST', + path: '/motors/disengage', + ok: false, + status: 500, +} + +export const mockDisengageMotorsFailure = { + ...mockDisengageMotorsFailureMeta, + host: mockRobot, + body: { message: 'AH' }, +} diff --git a/app/src/robot-controls/__tests__/actions.test.js b/app/src/robot-controls/__tests__/actions.test.js index 8db0a588c68..5226df4bf9f 100644 --- a/app/src/robot-controls/__tests__/actions.test.js +++ b/app/src/robot-controls/__tests__/actions.test.js @@ -124,6 +124,41 @@ const SPECS: Array = [ meta: { requestId: 'abc' }, }, }, + { + name: 'robotControls:MOVE', + creator: Actions.move, + args: ['robot-name', 'changePipette', 'left', true], + expected: { + type: 'robotControls:MOVE', + payload: { + robotName: 'robot-name', + position: 'changePipette', + mount: 'left', + disengageMotors: true, + }, + meta: {}, + }, + }, + { + name: 'robotControls:MOVE_SUCCESS', + creator: Actions.moveSuccess, + args: ['robot-name', { requestId: 'abc' }], + expected: { + type: 'robotControls:MOVE_SUCCESS', + payload: { robotName: 'robot-name' }, + meta: { requestId: 'abc' }, + }, + }, + { + name: 'robotControls:MOVE_FAILURE', + creator: Actions.moveFailure, + args: ['robot-name', { message: 'AH' }, { requestId: 'abc' }], + expected: { + type: 'robotControls:MOVE_FAILURE', + payload: { robotName: 'robot-name', error: { message: 'AH' } }, + meta: { requestId: 'abc' }, + }, + }, { name: 'robotControls:CLEAR_MOVEMENT_STATUS', creator: Actions.clearMovementStatus, diff --git a/app/src/robot-controls/__tests__/reducer.test.js b/app/src/robot-controls/__tests__/reducer.test.js index 3fe0f8680ca..c7e9afe9804 100644 --- a/app/src/robot-controls/__tests__/reducer.test.js +++ b/app/src/robot-controls/__tests__/reducer.test.js @@ -67,7 +67,44 @@ const SPECS: Array = [ }, state: { robotName: { movementStatus: 'homing' } }, expected: { - robotName: { movementStatus: 'home-error', movementError: 'AH' }, + robotName: { movementStatus: 'homeError', movementError: 'AH' }, + }, + }, + { + name: 'handles robotControls:MOVE', + action: { + type: 'robotControls:MOVE', + payload: { + robotName: 'robotName', + position: 'attachTip', + mount: 'left', + disengageMotors: false, + }, + meta: {}, + }, + state: { robotName: { movementStatus: null } }, + expected: { robotName: { movementStatus: 'moving', movementError: null } }, + }, + { + name: 'handles robotControls:MOVE_SUCCESS', + action: { + type: 'robotControls:MOVE_SUCCESS', + payload: { robotName: 'robotName' }, + meta: {}, + }, + state: { robotName: { movementStatus: 'moving' } }, + expected: { robotName: { movementStatus: null, movementError: null } }, + }, + { + name: 'handles robotControls:MOVE_FAILURE', + action: { + type: 'robotControls:MOVE_FAILURE', + payload: { robotName: 'robotName', error: { message: 'AH' } }, + meta: {}, + }, + state: { robotName: { movementStatus: 'moving' } }, + expected: { + robotName: { movementStatus: 'moveError', movementError: 'AH' }, }, }, { @@ -76,7 +113,7 @@ const SPECS: Array = [ type: 'robotControls:CLEAR_MOVEMENT_STATUS', payload: { robotName: 'robotName' }, }, - state: { robotName: { movementStatus: 'home-error', movementError: 'AH' } }, + state: { robotName: { movementStatus: 'homeError', movementError: 'AH' } }, expected: { robotName: { movementStatus: null, movementError: null }, }, diff --git a/app/src/robot-controls/actions.js b/app/src/robot-controls/actions.js index e1a4616e090..95126071986 100644 --- a/app/src/robot-controls/actions.js +++ b/app/src/robot-controls/actions.js @@ -95,6 +95,36 @@ export const homeFailure = ( meta, }) +export const move = ( + robotName: string, + position: Types.MovePosition, + mount: Mount, + disengageMotors: boolean = false +): Types.MoveAction => ({ + type: Constants.MOVE, + payload: { robotName, mount, position, disengageMotors }, + meta: {}, +}) + +export const moveSuccess = ( + robotName: string, + meta: RobotApiRequestMeta +): Types.MoveSuccessAction => ({ + type: Constants.MOVE_SUCCESS, + payload: { robotName }, + meta, +}) + +export const moveFailure = ( + robotName: string, + error: {| message: string |}, + meta: RobotApiRequestMeta +): Types.MoveFailureAction => ({ + type: Constants.MOVE_FAILURE, + payload: { robotName, error }, + meta, +}) + export const clearMovementStatus = ( robotName: string ): Types.ClearMovementStatusAction => ({ diff --git a/app/src/robot-controls/constants.js b/app/src/robot-controls/constants.js index a356a92dda9..b62b76f8968 100644 --- a/app/src/robot-controls/constants.js +++ b/app/src/robot-controls/constants.js @@ -1,21 +1,29 @@ // @flow -// homing targets +// homing and move request targets export const ROBOT: 'robot' = 'robot' export const PIPETTE: 'pipette' = 'pipette' +export const MOUNT = 'mount' // movement statuses export const HOMING: 'homing' = 'homing' -export const HOME_ERROR: 'home-error' = 'home-error' +export const HOME_ERROR: 'homeError' = 'homeError' export const MOVING: 'moving' = 'moving' -export const MOVE_ERROR: 'move-error' = 'move-error' +export const MOVE_ERROR: 'moveError' = 'moveError' + +// move positions +export const CHANGE_PIPETTE: 'changePipette' = 'changePipette' +export const ATTACH_TIP: 'attachTip' = 'attachTip' // http paths export const LIGHTS_PATH: '/robot/lights' = '/robot/lights' export const HOME_PATH: '/robot/home' = '/robot/home' +export const POSITIONS_PATH: '/robot/positions' = '/robot/positions' +export const MOVE_PATH: '/robot/move' = '/robot/move' +export const DISENGAGE_MOTORS_PATH: '/motors/disengage' = '/motors/disengage' // action type strings @@ -45,5 +53,13 @@ export const HOME_SUCCESS: 'robotControls:HOME_SUCCESS' = export const HOME_FAILURE: 'robotControls:HOME_FAILURE' = 'robotControls:HOME_FAILURE' +export const MOVE: 'robotControls:MOVE' = 'robotControls:MOVE' + +export const MOVE_SUCCESS: 'robotControls:MOVE_SUCCESS' = + 'robotControls:MOVE_SUCCESS' + +export const MOVE_FAILURE: 'robotControls:MOVE_FAILURE' = + 'robotControls:MOVE_FAILURE' + export const CLEAR_MOVEMENT_STATUS: 'robotControls:CLEAR_MOVEMENT_STATUS' = 'robotControls:CLEAR_MOVEMENT_STATUS' diff --git a/app/src/robot-controls/epic/__tests__/moveEpic.test.js b/app/src/robot-controls/epic/__tests__/moveEpic.test.js new file mode 100644 index 00000000000..6b41916f5ba --- /dev/null +++ b/app/src/robot-controls/epic/__tests__/moveEpic.test.js @@ -0,0 +1,305 @@ +// @flow +import { TestScheduler } from 'rxjs/testing' + +import { mockRobot } from '../../../robot-api/__fixtures__' +import * as RobotApiHttp from '../../../robot-api/http' +import * as DiscoverySelectors from '../../../discovery/selectors' +import * as PipettesSelectors from '../../../pipettes/selectors' +import * as Fixtures from '../../__fixtures__' +import * as Actions from '../../actions' +import * as Types from '../../types' +import { robotControlsEpic } from '..' + +import type { Observable } from 'rxjs' +import type { + RobotHost, + RobotApiRequestOptions, + RobotApiResponse, +} from '../../../robot-api/types' + +jest.mock('../../../robot-api/http') +jest.mock('../../../discovery/selectors') +jest.mock('../../../pipettes/selectors') + +const mockState = { state: true } + +const mockFetchRobotApi: JestMockFn< + [RobotHost, RobotApiRequestOptions], + Observable +> = RobotApiHttp.fetchRobotApi + +const mockGetRobotByName: JestMockFn<[any, string], mixed> = + DiscoverySelectors.getRobotByName + +const mockGetAttachedPipettes: JestMockFn<[any, string], mixed> = + PipettesSelectors.getAttachedPipettes + +describe('moveEpic', () => { + let testScheduler + + beforeEach(() => { + mockGetRobotByName.mockReturnValue(mockRobot) + + testScheduler = new TestScheduler((actual, expected) => { + expect(actual).toEqual(expected) + }) + }) + + afterEach(() => { + jest.resetAllMocks() + }) + + const meta = { requestId: '1234' } + + test('calls GET /robot/positions and then POST /robot/move with position: changePipette', () => { + const action: Types.MoveAction = { + ...Actions.move(mockRobot.name, 'changePipette', 'left'), + meta, + } + + testScheduler.run(({ hot, cold, expectObservable, flush }) => { + mockFetchRobotApi + .mockReturnValueOnce( + cold('p', { p: Fixtures.mockFetchPositionsSuccess }) + ) + .mockReturnValueOnce(cold('m', { m: Fixtures.mockMoveSuccess })) + + const action$ = hot('--a', { a: action }) + const state$ = hot('a-a', { a: mockState }) + const output$ = robotControlsEpic(action$, state$) + + expectObservable(output$) + flush() + + expect(mockGetRobotByName).toHaveBeenCalledWith(mockState, mockRobot.name) + expect(mockFetchRobotApi).toHaveBeenNthCalledWith(1, mockRobot, { + method: 'GET', + path: '/robot/positions', + }) + expect(mockFetchRobotApi).toHaveBeenNthCalledWith(2, mockRobot, { + method: 'POST', + path: '/robot/move', + body: { + target: 'mount', + mount: 'left', + point: Fixtures.mockPositions.change_pipette.left, + }, + }) + }) + }) + + test('calls GET /robot/positions and POST /robot/move with position: attachTip', () => { + const action: Types.MoveAction = { + ...Actions.move(mockRobot.name, 'attachTip', 'right'), + meta, + } + + testScheduler.run(({ hot, cold, expectObservable, flush }) => { + mockFetchRobotApi + .mockReturnValueOnce( + cold('p', { p: Fixtures.mockFetchPositionsSuccess }) + ) + .mockReturnValueOnce(cold('m', { m: Fixtures.mockMoveSuccess })) + + mockGetAttachedPipettes.mockReturnValue({ + left: null, + right: { model: 'p300_single_v2.0' }, + }) + + const action$ = hot('--a', { a: action }) + const state$ = hot('a-a', { a: mockState }) + const output$ = robotControlsEpic(action$, state$) + + expectObservable(output$) + flush() + + expect(mockGetRobotByName).toHaveBeenCalledWith(mockState, mockRobot.name) + expect(mockFetchRobotApi).toHaveBeenNthCalledWith(1, mockRobot, { + method: 'GET', + path: '/robot/positions', + }) + expect(mockFetchRobotApi).toHaveBeenNthCalledWith(2, mockRobot, { + method: 'POST', + path: '/robot/move', + body: { + target: 'pipette', + model: 'p300_single_v2.0', + mount: 'right', + point: Fixtures.mockPositions.attach_tip.point, + }, + }) + }) + }) + + test('calls POST /motors/disengage if disengageMotors: true', () => { + const action: Types.MoveAction = { + ...Actions.move(mockRobot.name, 'changePipette', 'left', true), + meta, + } + + testScheduler.run(({ hot, cold, expectObservable, flush }) => { + mockFetchRobotApi + .mockReturnValueOnce( + cold('p', { p: Fixtures.mockFetchPositionsSuccess }) + ) + .mockReturnValueOnce(cold('m', { m: Fixtures.mockMoveSuccess })) + .mockReturnValueOnce( + cold('d', { d: Fixtures.mockDisengageMotorsSuccess }) + ) + + const action$ = hot('--a', { a: action }) + const state$ = hot('a-a', { a: mockState }) + const output$ = robotControlsEpic(action$, state$) + + expectObservable(output$) + flush() + + expect(mockGetRobotByName).toHaveBeenCalledWith(mockState, mockRobot.name) + expect(mockFetchRobotApi).toHaveBeenNthCalledWith(3, mockRobot, { + method: 'POST', + path: '/motors/disengage', + body: { axes: ['a', 'b', 'c', 'z'] }, + }) + }) + }) + + test('maps successful response to MOVE_SUCCESS without disengage', () => { + const action: Types.MoveAction = { + ...Actions.move(mockRobot.name, 'changePipette', 'left'), + meta, + } + + testScheduler.run(({ hot, cold, expectObservable, flush }) => { + mockFetchRobotApi + .mockReturnValueOnce( + cold('p', { p: Fixtures.mockFetchPositionsSuccess }) + ) + .mockReturnValueOnce(cold('m', { m: Fixtures.mockMoveSuccess })) + .mockReturnValueOnce( + cold('d', { d: Fixtures.mockDisengageMotorsSuccess }) + ) + + const action$ = hot('--a', { a: action }) + const state$ = hot('a-a', { a: {} }) + const output$ = robotControlsEpic(action$, state$) + + expectObservable(output$).toBe('--a', { + a: Actions.moveSuccess(mockRobot.name, { + ...meta, + response: Fixtures.mockMoveSuccessMeta, + }), + }) + }) + }) + + test('maps successful response to MOVE_SUCCESS with disengage', () => { + const action: Types.MoveAction = { + ...Actions.move(mockRobot.name, 'changePipette', 'left', true), + meta, + } + + testScheduler.run(({ hot, cold, expectObservable, flush }) => { + mockFetchRobotApi + .mockReturnValueOnce( + cold('p', { p: Fixtures.mockFetchPositionsSuccess }) + ) + .mockReturnValueOnce(cold('m', { m: Fixtures.mockMoveSuccess })) + .mockReturnValueOnce( + cold('d', { d: Fixtures.mockDisengageMotorsSuccess }) + ) + + const action$ = hot('--a', { a: action }) + const state$ = hot('a-a', { a: {} }) + const output$ = robotControlsEpic(action$, state$) + + expectObservable(output$).toBe('--a', { + a: Actions.moveSuccess(mockRobot.name, { + ...meta, + response: Fixtures.mockMoveSuccessMeta, + }), + }) + }) + }) + + test('maps failed GET /robot/positions to MOVE_FAILURE', () => { + const action: Types.MoveAction = { + ...Actions.move(mockRobot.name, 'changePipette', 'left', true), + meta, + } + + testScheduler.run(({ hot, cold, expectObservable, flush }) => { + mockFetchRobotApi.mockReturnValueOnce( + cold('r', { r: Fixtures.mockFetchPositionsFailure }) + ) + + const action$ = hot('--a', { a: action }) + const state$ = hot('a-a', { a: {} }) + const output$ = robotControlsEpic(action$, state$) + + expectObservable(output$).toBe('--a', { + a: Actions.moveFailure( + mockRobot.name, + { message: 'AH' }, + { ...meta, response: Fixtures.mockFetchPositionsFailureMeta } + ), + }) + }) + }) + + test('maps failed POST /robot/move to MOVE_FAILURE', () => { + const action: Types.MoveAction = { + ...Actions.move(mockRobot.name, 'changePipette', 'left', true), + meta, + } + + testScheduler.run(({ hot, cold, expectObservable, flush }) => { + mockFetchRobotApi + .mockReturnValueOnce( + cold('p', { p: Fixtures.mockFetchPositionsSuccess }) + ) + .mockReturnValueOnce(cold('m', { m: Fixtures.mockMoveFailure })) + + const action$ = hot('--a', { a: action }) + const state$ = hot('a-a', { a: {} }) + const output$ = robotControlsEpic(action$, state$) + + expectObservable(output$).toBe('--a', { + a: Actions.moveFailure( + mockRobot.name, + { message: 'AH' }, + { ...meta, response: Fixtures.mockMoveFailureMeta } + ), + }) + }) + }) + + test('maps failed POST /motors/disengage to MOVE_FAILURE', () => { + const action: Types.MoveAction = { + ...Actions.move(mockRobot.name, 'changePipette', 'left', true), + meta, + } + + testScheduler.run(({ hot, cold, expectObservable, flush }) => { + mockFetchRobotApi + .mockReturnValueOnce( + cold('p', { p: Fixtures.mockFetchPositionsSuccess }) + ) + .mockReturnValueOnce(cold('m', { m: Fixtures.mockMoveSuccess })) + .mockReturnValueOnce( + cold('d', { d: Fixtures.mockDisengageMotorsFailure }) + ) + + const action$ = hot('--a', { a: action }) + const state$ = hot('a-a', { a: {} }) + const output$ = robotControlsEpic(action$, state$) + + expectObservable(output$).toBe('--a', { + a: Actions.moveFailure( + mockRobot.name, + { message: 'AH' }, + { ...meta, response: Fixtures.mockDisengageMotorsFailureMeta } + ), + }) + }) + }) +}) diff --git a/app/src/robot-controls/epic/index.js b/app/src/robot-controls/epic/index.js index be953d6a698..0822ec11ca5 100644 --- a/app/src/robot-controls/epic/index.js +++ b/app/src/robot-controls/epic/index.js @@ -4,11 +4,13 @@ import { combineEpics } from 'redux-observable' import { fetchLightsEpic } from './fetchLightsEpic' import { updateLightsEpic } from './updateLightsEpic' import { homeEpic } from './homeEpic' +import { moveEpic } from './moveEpic' import type { Epic } from '../../types' export const robotControlsEpic: Epic = combineEpics( fetchLightsEpic, updateLightsEpic, - homeEpic + homeEpic, + moveEpic ) diff --git a/app/src/robot-controls/epic/moveEpic.js b/app/src/robot-controls/epic/moveEpic.js new file mode 100644 index 00000000000..41dad154710 --- /dev/null +++ b/app/src/robot-controls/epic/moveEpic.js @@ -0,0 +1,109 @@ +// @flow +import { ofType } from 'redux-observable' +import { of } from 'rxjs' +import { map, switchMap } from 'rxjs/operators' + +import { GET, POST, fetchRobotApi } from '../../robot-api' +import { withRobotHost } from '../../robot-api/operators' +import { getAttachedPipettes } from '../../pipettes' + +import * as Actions from '../actions' +import * as Constants from '../constants' + +import type { State, StrictEpic } from '../../types' +import type { + RobotApiRequestOptions, + RobotApiResponse, +} from '../../robot-api/types' + +import type { MoveAction, MoveDoneAction, PositionsResponse } from '../types' + +const mapActionToRequest = ( + action: MoveAction, + state: State, + positionsResponse: PositionsResponse +): RobotApiRequestOptions => { + const { robotName, position, mount } = action.payload + const attachedPipettes = getAttachedPipettes(state, robotName) + const apiPosition = + position === Constants.CHANGE_PIPETTE + ? positionsResponse.positions.change_pipette + : positionsResponse.positions.attach_tip + + const body = + apiPosition.target === Constants.MOUNT + ? { mount, target: Constants.MOUNT, point: apiPosition[mount] } + : { + mount, + target: Constants.PIPETTE, + point: apiPosition.point, + model: attachedPipettes[mount]?.model || null, + } + + return { method: POST, path: Constants.MOVE_PATH, body } +} + +const mapResponseToAction = ( + response: RobotApiResponse, + originalAction: MoveAction +): MoveDoneAction => { + const { host, body, ...responseMeta } = response + const meta = { ...originalAction.meta, response: responseMeta } + + return response.ok + ? Actions.moveSuccess(host.name, meta) + : Actions.moveFailure(host.name, body, meta) +} + +const fetchPositionsRequest = { method: GET, path: Constants.POSITIONS_PATH } + +const disengageMotorsRequest = { + method: POST, + path: Constants.DISENGAGE_MOTORS_PATH, + body: { axes: ['a', 'b', 'c', 'z'] }, +} + +// complicated epic because the endpoints are complicated +// 1. Call GET /robot/positions +// 2. Call POST /robot/move with result of GET /robot/positions +// 3. Call POST /motors/disengage if we need to +export const moveEpic: StrictEpic = (action$, state$) => { + return action$.pipe( + ofType(Constants.MOVE), + withRobotHost(state$, a => a.payload.robotName), + switchMap(([action, state, host]) => { + // hit GET /robot/positions to figure out what POST /robot/move body will be + return fetchRobotApi(host, fetchPositionsRequest).pipe( + switchMap(positionsResponse => { + // call move endpoint if we have positions, otherwise + // pass the failure response along + return positionsResponse.ok + ? fetchRobotApi( + host, + mapActionToRequest(action, state, positionsResponse.body) + ) + : of(positionsResponse) + }), + // at this point we have either a successful movement call, + // a failed movement call, or a failed position call + switchMap(maybeMoveSuccess => { + // if the last call was successful and we need to disengage motors, + // go ahead and make that call; otherwise pass the response along + return maybeMoveSuccess.ok && action.payload.disengageMotors + ? fetchRobotApi(host, disengageMotorsRequest).pipe( + map(disengageResponse => + // if the disengage call succeeds, make sure we still pass + // our movement success response into our final action for + // consistency + disengageResponse.ok ? maybeMoveSuccess : disengageResponse + ) + ) + : of(maybeMoveSuccess) + }), + // response will be one of: + // movement success, movement fail, positions fail, disengage fail + map(response => mapResponseToAction(response, action)) + ) + }) + ) +} diff --git a/app/src/robot-controls/reducer.js b/app/src/robot-controls/reducer.js index e0f7d03ac2a..3ec450dc598 100644 --- a/app/src/robot-controls/reducer.js +++ b/app/src/robot-controls/reducer.js @@ -37,15 +37,18 @@ export function robotControlsReducer( return updateRobotState(state, robotName, { lightsOn }) } - case Constants.HOME: { + case Constants.HOME: + case Constants.MOVE: { const { robotName } = action.payload return updateRobotState(state, robotName, { - movementStatus: Constants.HOMING, + movementStatus: + action.type === Constants.HOME ? Constants.HOMING : Constants.MOVING, movementError: null, }) } case Constants.HOME_SUCCESS: + case Constants.MOVE_SUCCESS: case Constants.CLEAR_MOVEMENT_STATUS: { const { robotName } = action.payload return updateRobotState(state, robotName, { @@ -54,10 +57,14 @@ export function robotControlsReducer( }) } - case Constants.HOME_FAILURE: { + case Constants.HOME_FAILURE: + case Constants.MOVE_FAILURE: { const { robotName, error } = action.payload return updateRobotState(state, robotName, { - movementStatus: Constants.HOME_ERROR, + movementStatus: + action.type === Constants.HOME_FAILURE + ? Constants.HOME_ERROR + : Constants.MOVE_ERROR, movementError: error.message, }) } diff --git a/app/src/robot-controls/types.js b/app/src/robot-controls/types.js index 03b2cf4d1d8..18e8eb851ff 100644 --- a/app/src/robot-controls/types.js +++ b/app/src/robot-controls/types.js @@ -5,7 +5,22 @@ import type { Mount } from '../pipettes/types' // common types -export type MovementStatus = 'homing' | 'home-error' | 'moving' | 'move-error' +export type MovementStatus = 'homing' | 'homeError' | 'moving' | 'moveError' + +export type MovePosition = 'changePipette' | 'attachTip' + +// http responses + +export type PositionsResponse = {| + positions: {| + change_pipette: {| + target: 'mount', + left: [number, number, number], + right: [number, number, number], + |}, + attach_tip: {| target: 'pipette', point: [number, number, number] |}, + |}, +|} // action types @@ -81,6 +96,33 @@ export type HomeFailureAction = {| export type HomeDoneAction = HomeSuccessAction | HomeFailureAction +// move + +export type MoveAction = {| + type: 'robotControls:MOVE', + payload: {| + robotName: string, + mount: Mount, + position: MovePosition, + disengageMotors: boolean, + |}, + meta: RobotApiRequestMeta, +|} + +export type MoveSuccessAction = {| + type: 'robotControls:MOVE_SUCCESS', + payload: {| robotName: string |}, + meta: RobotApiRequestMeta, +|} + +export type MoveFailureAction = {| + type: 'robotControls:MOVE_FAILURE', + payload: {| robotName: string, error: {| message: string |} |}, + meta: RobotApiRequestMeta, +|} + +export type MoveDoneAction = MoveSuccessAction | MoveFailureAction + // clear homing and movement status and error export type ClearMovementStatusAction = {| @@ -100,6 +142,9 @@ export type RobotControlsAction = | HomeAction | HomeSuccessAction | HomeFailureAction + | MoveAction + | MoveSuccessAction + | MoveFailureAction | ClearMovementStatusAction // state types