From 8ae648062d30022b05c2acd6f2511e655ab6ae12 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 30 Sep 2024 09:54:45 -0400 Subject: [PATCH 01/17] fix(analyses-snapshot-testing): heal edge snapshots (#16377) The edge overnight analyses snapshot test is failing. This PR was opened to alert us to the failure. Co-authored-by: y3rsh <502770+y3rsh@users.noreply.github.com> --- ...89e3ab18][OT2_X_v6_P20S_None_SimpleTransfer].json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4389e3ab18][OT2_X_v6_P20S_None_SimpleTransfer].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4389e3ab18][OT2_X_v6_P20S_None_SimpleTransfer].json index 5339ccdcc00..579a4670bcc 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4389e3ab18][OT2_X_v6_P20S_None_SimpleTransfer].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4389e3ab18][OT2_X_v6_P20S_None_SimpleTransfer].json @@ -1757,13 +1757,13 @@ "createdAt": "TIMESTAMP", "error": { "createdAt": "TIMESTAMP", - "detail": "Cannot aspirate 21.0 µL when only 20.0 is available.", + "detail": "Cannot aspirate 21.0 µL when only 20 is available.", "errorCode": "4000", "errorInfo": { "attempted_aspirate_volume": 21.0, - "available_volume": 20.0, + "available_volume": 20, "max_pipette_volume": 20, - "max_tip_volume": 20.0 + "max_tip_volume": 20 }, "errorType": "InvalidAspirateVolumeError", "id": "UUID", @@ -1800,13 +1800,13 @@ "errors": [ { "createdAt": "TIMESTAMP", - "detail": "Cannot aspirate 21.0 µL when only 20.0 is available.", + "detail": "Cannot aspirate 21.0 µL when only 20 is available.", "errorCode": "4000", "errorInfo": { "attempted_aspirate_volume": 21.0, - "available_volume": 20.0, + "available_volume": 20, "max_pipette_volume": 20, - "max_tip_volume": 20.0 + "max_tip_volume": 20 }, "errorType": "InvalidAspirateVolumeError", "id": "UUID", From 7198034cb8b99889f6b70b750faf4534fad48c50 Mon Sep 17 00:00:00 2001 From: koji Date: Mon, 30 Sep 2024 10:23:00 -0400 Subject: [PATCH 02/17] fix(components): align EmptySelectorButton with the latest design (#16374) * fix(components): align EmptySelectorButton with the latest design --- .../atoms/buttons/EmptySelectorButton.stories.tsx | 8 -------- .../src/atoms/buttons/EmptySelectorButton.tsx | 15 +++------------ .../__tests__/EmptySelectorButton.test.tsx | 2 -- .../src/organisms/EditInstrumentsModal/index.tsx | 3 --- .../CreateNewProtocolWizard/SelectFixtures.tsx | 1 - .../CreateNewProtocolWizard/SelectModules.tsx | 1 - .../CreateNewProtocolWizard/SelectPipettes.tsx | 2 -- .../src/pages/Designer/Offdeck/OffDeckDetails.tsx | 1 - 8 files changed, 3 insertions(+), 30 deletions(-) diff --git a/components/src/atoms/buttons/EmptySelectorButton.stories.tsx b/components/src/atoms/buttons/EmptySelectorButton.stories.tsx index 9251085f13a..150257c784c 100644 --- a/components/src/atoms/buttons/EmptySelectorButton.stories.tsx +++ b/components/src/atoms/buttons/EmptySelectorButton.stories.tsx @@ -6,13 +6,6 @@ const meta: Meta = { title: 'Library/Atoms/Buttons/EmptySelectorButton', component: EmptySelectorButtonComponent, argTypes: { - size: { - control: { - type: 'select', - options: ['large', 'small'], - }, - defaultValue: 'large', - }, textAlignment: { controls: { type: 'select', @@ -37,7 +30,6 @@ export const EmptySelectorButton: Story = { args: { text: 'mock text', iconName: 'plus', - size: 'small', textAlignment: 'left', onClick: () => {}, }, diff --git a/components/src/atoms/buttons/EmptySelectorButton.tsx b/components/src/atoms/buttons/EmptySelectorButton.tsx index 92caa541fce..8a897e3569b 100644 --- a/components/src/atoms/buttons/EmptySelectorButton.tsx +++ b/components/src/atoms/buttons/EmptySelectorButton.tsx @@ -20,7 +20,6 @@ interface EmptySelectorButtonProps { text: string textAlignment: 'left' | 'middle' iconName?: IconName - size?: 'large' | 'small' disabled?: boolean } @@ -28,20 +27,12 @@ interface EmptySelectorButtonProps { export function EmptySelectorButton( props: EmptySelectorButtonProps ): JSX.Element { - const { - onClick, - text, - iconName, - size = 'large', - textAlignment, - disabled = false, - } = props - const buttonSizing = size === 'large' ? '100%' : FLEX_MAX_CONTENT + const { onClick, text, iconName, textAlignment, disabled = false } = props const StyledButton = styled.button` border: none; - width: ${buttonSizing}; - height: ${buttonSizing}; + width: ${FLEX_MAX_CONTENT}; + height: ${FLEX_MAX_CONTENT}; cursor: ${disabled ? CURSOR_DEFAULT : CURSOR_POINTER}; &:focus-visible { outline: 2px solid ${COLORS.white}; diff --git a/components/src/atoms/buttons/__tests__/EmptySelectorButton.test.tsx b/components/src/atoms/buttons/__tests__/EmptySelectorButton.test.tsx index ea7ab6c2b2b..6fea2d8d297 100644 --- a/components/src/atoms/buttons/__tests__/EmptySelectorButton.test.tsx +++ b/components/src/atoms/buttons/__tests__/EmptySelectorButton.test.tsx @@ -18,7 +18,6 @@ describe('EmptySelectorButton', () => { text: 'mock text', iconName: 'add', textAlignment: 'left', - size: 'large', } }) it('renders the props and button cta', () => { @@ -33,7 +32,6 @@ describe('EmptySelectorButton', () => { }) it('renders middled aligned button', () => { props.textAlignment = 'middle' - props.size = 'small' props.iconName = undefined render(props) expect(screen.getByTestId('EmptySelectorButton_container')).toHaveStyle( diff --git a/protocol-designer/src/organisms/EditInstrumentsModal/index.tsx b/protocol-designer/src/organisms/EditInstrumentsModal/index.tsx index 8b50cb41c73..7171b13d8e2 100644 --- a/protocol-designer/src/organisms/EditInstrumentsModal/index.tsx +++ b/protocol-designer/src/organisms/EditInstrumentsModal/index.tsx @@ -270,7 +270,6 @@ export function EditInstrumentsModal( text={t('add_pip')} textAlignment="left" iconName="plus" - size="large" /> )} {rightPip != null && @@ -305,7 +304,6 @@ export function EditInstrumentsModal( text={t('add_pip')} textAlignment="left" iconName="plus" - size="large" /> )} @@ -367,7 +365,6 @@ export function EditInstrumentsModal( text={t('protocol_overview:add_gripper')} textAlignment="left" iconName="plus" - size="large" /> )} diff --git a/protocol-designer/src/pages/CreateNewProtocolWizard/SelectFixtures.tsx b/protocol-designer/src/pages/CreateNewProtocolWizard/SelectFixtures.tsx index 0bdd457f2c7..caebeecbe99 100644 --- a/protocol-designer/src/pages/CreateNewProtocolWizard/SelectFixtures.tsx +++ b/protocol-designer/src/pages/CreateNewProtocolWizard/SelectFixtures.tsx @@ -92,7 +92,6 @@ export function SelectFixtures(props: WizardTileProps): JSX.Element | null { disabled={numSlotsAvailable === 0} key={equipment} textAlignment={TYPOGRAPHY.textAlignLeft} - size="small" iconName="plus" text={t(`${equipment}`)} onClick={() => { diff --git a/protocol-designer/src/pages/CreateNewProtocolWizard/SelectModules.tsx b/protocol-designer/src/pages/CreateNewProtocolWizard/SelectModules.tsx index 72ffa64245a..79701d9def9 100644 --- a/protocol-designer/src/pages/CreateNewProtocolWizard/SelectModules.tsx +++ b/protocol-designer/src/pages/CreateNewProtocolWizard/SelectModules.tsx @@ -141,7 +141,6 @@ export function SelectModules(props: WizardTileProps): JSX.Element | null { numMagneticBlocks === 4) } textAlignment={TYPOGRAPHY.textAlignLeft} - size="small" iconName="plus" text={getModuleDisplayName(moduleModel)} onClick={() => { diff --git a/protocol-designer/src/pages/CreateNewProtocolWizard/SelectPipettes.tsx b/protocol-designer/src/pages/CreateNewProtocolWizard/SelectPipettes.tsx index 9f1b3a4c83f..86f212718ad 100644 --- a/protocol-designer/src/pages/CreateNewProtocolWizard/SelectPipettes.tsx +++ b/protocol-designer/src/pages/CreateNewProtocolWizard/SelectPipettes.tsx @@ -448,7 +448,6 @@ export function SelectPipettes(props: WizardTileProps): JSX.Element | null { text={t('add_pip')} textAlignment="left" iconName="plus" - size="large" /> )} {pipettesByMount.right.pipetteName != null && @@ -480,7 +479,6 @@ export function SelectPipettes(props: WizardTileProps): JSX.Element | null { text={t('add_pip')} textAlignment="left" iconName="plus" - size="large" /> )} diff --git a/protocol-designer/src/pages/Designer/Offdeck/OffDeckDetails.tsx b/protocol-designer/src/pages/Designer/Offdeck/OffDeckDetails.tsx index 5d83ccb34e0..54561d0db9d 100644 --- a/protocol-designer/src/pages/Designer/Offdeck/OffDeckDetails.tsx +++ b/protocol-designer/src/pages/Designer/Offdeck/OffDeckDetails.tsx @@ -152,7 +152,6 @@ export function OffDeckDetails(props: OffDeckDetailsProps): JSX.Element { onClick={addLabware} text={t('add_labware')} textAlignment="middle" - size="large" iconName="plus" /> From 325b66e628f1b0f716e4dfd7c39957130f3e746f Mon Sep 17 00:00:00 2001 From: Jamey Huffnagle Date: Mon, 30 Sep 2024 10:36:29 -0400 Subject: [PATCH 03/17] fix(app): fix error recovery infinite render during takeover (#16364) --- .../__tests__/cleanupRecoveryState.test.ts | 37 +++++++++++++++---- .../useCleanupRecoveryState.ts} | 20 ++++++---- .../ErrorRecoveryFlows/hooks/useERUtils.ts | 9 ++--- .../ErrorRecoveryFlows/utils/index.ts | 1 - 4 files changed, 45 insertions(+), 22 deletions(-) rename app/src/organisms/ErrorRecoveryFlows/{utils => hooks}/__tests__/cleanupRecoveryState.test.ts (53%) rename app/src/organisms/ErrorRecoveryFlows/{utils/cleanupRecoveryState.ts => hooks/useCleanupRecoveryState.ts} (61%) diff --git a/app/src/organisms/ErrorRecoveryFlows/utils/__tests__/cleanupRecoveryState.test.ts b/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/cleanupRecoveryState.test.ts similarity index 53% rename from app/src/organisms/ErrorRecoveryFlows/utils/__tests__/cleanupRecoveryState.test.ts rename to app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/cleanupRecoveryState.test.ts index 0bb17054c11..205e9cee926 100644 --- a/app/src/organisms/ErrorRecoveryFlows/utils/__tests__/cleanupRecoveryState.test.ts +++ b/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/cleanupRecoveryState.test.ts @@ -1,10 +1,11 @@ -import { describe, it, expect, beforeEach, vi } from 'vitest' +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { renderHook } from '@testing-library/react' -import { cleanupRecoveryState } from '../cleanupRecoveryState' +import { useCleanupRecoveryState } from '../useCleanupRecoveryState' import { RECOVERY_MAP } from '../../constants' -describe('cleanupRecoveryState', () => { - let props: Parameters[0] +describe('useCleanupRecoveryState', () => { + let props: Parameters[0] let mockSetRM: ReturnType beforeEach(() => { @@ -22,7 +23,7 @@ describe('cleanupRecoveryState', () => { }) it('does not modify state when isTakeover is false', () => { - cleanupRecoveryState(props) + renderHook(() => useCleanupRecoveryState(props)) expect(props.stashedMapRef.current).toEqual({ route: RECOVERY_MAP.FILL_MANUALLY_AND_SKIP.ROUTE, @@ -33,7 +34,7 @@ describe('cleanupRecoveryState', () => { it('resets state when isTakeover is true', () => { props.isTakeover = true - cleanupRecoveryState(props) + renderHook(() => useCleanupRecoveryState(props)) expect(props.stashedMapRef.current).toBeNull() expect(mockSetRM).toHaveBeenCalledWith({ @@ -45,7 +46,7 @@ describe('cleanupRecoveryState', () => { it('handles case when stashedMapRef.current is already null', () => { props.isTakeover = true props.stashedMapRef.current = null - cleanupRecoveryState(props) + renderHook(() => useCleanupRecoveryState(props)) expect(props.stashedMapRef.current).toBeNull() expect(mockSetRM).toHaveBeenCalledWith({ @@ -53,4 +54,26 @@ describe('cleanupRecoveryState', () => { step: RECOVERY_MAP.OPTION_SELECTION.STEPS.SELECT, }) }) + + it('does not reset state when isTakeover changes from true to false', () => { + const { rerender } = renderHook( + ({ isTakeover }) => useCleanupRecoveryState({ ...props, isTakeover }), + { initialProps: { isTakeover: true } } + ) + + // Reset mockSetRM and stashedMapRef + mockSetRM.mockClear() + props.stashedMapRef.current = { + route: RECOVERY_MAP.FILL_MANUALLY_AND_SKIP.ROUTE, + step: RECOVERY_MAP.FILL_MANUALLY_AND_SKIP.STEPS.SKIP, + } + + rerender({ isTakeover: false }) + + expect(props.stashedMapRef.current).toEqual({ + route: RECOVERY_MAP.FILL_MANUALLY_AND_SKIP.ROUTE, + step: RECOVERY_MAP.FILL_MANUALLY_AND_SKIP.STEPS.SKIP, + }) + expect(mockSetRM).not.toHaveBeenCalled() + }) }) diff --git a/app/src/organisms/ErrorRecoveryFlows/utils/cleanupRecoveryState.ts b/app/src/organisms/ErrorRecoveryFlows/hooks/useCleanupRecoveryState.ts similarity index 61% rename from app/src/organisms/ErrorRecoveryFlows/utils/cleanupRecoveryState.ts rename to app/src/organisms/ErrorRecoveryFlows/hooks/useCleanupRecoveryState.ts index fddc29c1983..3d01e2356c5 100644 --- a/app/src/organisms/ErrorRecoveryFlows/utils/cleanupRecoveryState.ts +++ b/app/src/organisms/ErrorRecoveryFlows/hooks/useCleanupRecoveryState.ts @@ -1,3 +1,5 @@ +import { useEffect } from 'react' + import { RECOVERY_MAP } from '../constants' import type { @@ -13,17 +15,19 @@ export interface UseCleanupProps { } // When certain events (ex, a takeover) occur, reset state that needs to be reset. -export function cleanupRecoveryState({ +export function useCleanupRecoveryState({ isTakeover, stashedMapRef, setRM, }: UseCleanupProps): void { - if (isTakeover) { - stashedMapRef.current = null + useEffect(() => { + if (isTakeover) { + stashedMapRef.current = null - setRM({ - route: RECOVERY_MAP.OPTION_SELECTION.ROUTE, - step: RECOVERY_MAP.OPTION_SELECTION.STEPS.SELECT, - }) - } + setRM({ + route: RECOVERY_MAP.OPTION_SELECTION.ROUTE, + step: RECOVERY_MAP.OPTION_SELECTION.STEPS.SELECT, + }) + } + }, [isTakeover]) } diff --git a/app/src/organisms/ErrorRecoveryFlows/hooks/useERUtils.ts b/app/src/organisms/ErrorRecoveryFlows/hooks/useERUtils.ts index b571bcd890d..9f6a6536504 100644 --- a/app/src/organisms/ErrorRecoveryFlows/hooks/useERUtils.ts +++ b/app/src/organisms/ErrorRecoveryFlows/hooks/useERUtils.ts @@ -5,11 +5,7 @@ import { useRecoveryCommands } from './useRecoveryCommands' import { useRecoveryTipStatus } from './useRecoveryTipStatus' import { useRecoveryRouting } from './useRecoveryRouting' import { useFailedLabwareUtils } from './useFailedLabwareUtils' -import { - getFailedCommandPipetteInfo, - getNextSteps, - cleanupRecoveryState, -} from '../utils' +import { getFailedCommandPipetteInfo, getNextSteps } from '../utils' import { useDeckMapUtils } from './useDeckMapUtils' import { useNotifyAllCommandsQuery, @@ -21,6 +17,7 @@ import { useRunningStepCounts } from '/app/resources/protocols/hooks' import { useRecoveryToasts } from './useRecoveryToasts' import { useRecoveryAnalytics } from '/app/redux-resources/analytics' import { useShowDoorInfo } from './useShowDoorInfo' +import { useCleanupRecoveryState } from './useCleanupRecoveryState' import type { PipetteData } from '@opentrons/api-client' import type { RobotType } from '@opentrons/shared-data' @@ -177,7 +174,7 @@ export function useERUtils({ SUBSEQUENT_COMMAND_DEPTH ) - cleanupRecoveryState({ + useCleanupRecoveryState({ isTakeover: showTakeover, setRM, stashedMapRef: routeUpdateActions.stashedMapRef, diff --git a/app/src/organisms/ErrorRecoveryFlows/utils/index.ts b/app/src/organisms/ErrorRecoveryFlows/utils/index.ts index 42f0a40d6f1..53195d5d5fa 100644 --- a/app/src/organisms/ErrorRecoveryFlows/utils/index.ts +++ b/app/src/organisms/ErrorRecoveryFlows/utils/index.ts @@ -1,4 +1,3 @@ export { getErrorKind } from './getErrorKind' export { getFailedCommandPipetteInfo } from './getFailedCommandPipetteInfo' export { getNextStep, getNextSteps } from './getNextStep' -export { cleanupRecoveryState } from './cleanupRecoveryState' From 047d1424b2e27c915936b4305bf50d82d81f698a Mon Sep 17 00:00:00 2001 From: koji Date: Mon, 30 Sep 2024 11:19:18 -0400 Subject: [PATCH 04/17] fix(protocol-ddesigner): fix select modules unexpected behavior (#16372) * fix(protocol-ddesigner): fix select modules unexpected behavior --- .../localization/en/create_new_protocol.json | 2 +- .../organisms/EditInstrumentsModal/index.tsx | 4 +- .../CreateNewProtocolWizard/SelectModules.tsx | 283 ++++++++++-------- .../SelectPipettes.tsx | 6 +- .../CreateNewProtocolWizard/WizardBody.tsx | 2 +- .../pages/CreateNewProtocolWizard/index.tsx | 4 +- protocol-designer/src/pages/Landing/index.tsx | 12 +- 7 files changed, 170 insertions(+), 143 deletions(-) diff --git a/protocol-designer/src/assets/localization/en/create_new_protocol.json b/protocol-designer/src/assets/localization/en/create_new_protocol.json index ecbb8ac4d0f..a10a1653685 100644 --- a/protocol-designer/src/assets/localization/en/create_new_protocol.json +++ b/protocol-designer/src/assets/localization/en/create_new_protocol.json @@ -3,7 +3,7 @@ "add_fixtures": "Add your fixtures", "add_gripper": "Add a gripper", "add_modules": "Add your modules", - "add_pip": "Add a pipette and tips", + "add_pipette": "Add a pipette and tips", "author_org": "Author/Organization", "basics": "Let’s start with the basics", "description": "Description", diff --git a/protocol-designer/src/organisms/EditInstrumentsModal/index.tsx b/protocol-designer/src/organisms/EditInstrumentsModal/index.tsx index 7171b13d8e2..c01e4ee859f 100644 --- a/protocol-designer/src/organisms/EditInstrumentsModal/index.tsx +++ b/protocol-designer/src/organisms/EditInstrumentsModal/index.tsx @@ -267,7 +267,7 @@ export function EditInstrumentsModal( setMount('left') resetFields() }} - text={t('add_pip')} + text={t('add_pipette')} textAlignment="left" iconName="plus" /> @@ -301,7 +301,7 @@ export function EditInstrumentsModal( setPage('add') setMount('right') }} - text={t('add_pip')} + text={t('add_pipette')} textAlignment="left" iconName="plus" /> diff --git a/protocol-designer/src/pages/CreateNewProtocolWizard/SelectModules.tsx b/protocol-designer/src/pages/CreateNewProtocolWizard/SelectModules.tsx index 79701d9def9..4cf2576c25e 100644 --- a/protocol-designer/src/pages/CreateNewProtocolWizard/SelectModules.tsx +++ b/protocol-designer/src/pages/CreateNewProtocolWizard/SelectModules.tsx @@ -43,9 +43,12 @@ import { HandleEnter } from './HandleEnter' import type { DropdownBorder } from '@opentrons/components' import type { ModuleModel, ModuleType } from '@opentrons/shared-data' -import type { FormModules } from '../../step-forms' +import type { FormModule, FormModules } from '../../step-forms' import type { WizardTileProps } from './types' +const MAX_MAGNETIC_BLOCKS = 4 +const MAGNETIC_BLOCKS_ADJUSTMENT = 3 + export function SelectModules(props: WizardTileProps): JSX.Element | null { const { goBack, proceed, watch, setValue } = props const { t } = useTranslation(['create_new_protocol', 'shared']) @@ -84,16 +87,73 @@ export function SelectModules(props: WizardTileProps): JSX.Element | null { ? [TEMPERATURE_MODULE_TYPE, HEATERSHAKER_MODULE_TYPE, MAGNETIC_BLOCK_TYPE] : [TEMPERATURE_MODULE_TYPE] - const filteredModules: FormModules = {} - const seenModels = new Set() + const handleAddModule = (moduleModel: ModuleModel): void => { + if (hasNoAvailableSlots) { + makeSnackbar(t('slots_limit_reached') as string) + } else { + setValue('modules', { + ...modules, + [uuid()]: { + model: moduleModel, + type: getModuleType(moduleModel), + slot: + robotType === FLEX_ROBOT_TYPE + ? DEFAULT_SLOT_MAP_FLEX[moduleModel] + : DEFAULT_SLOT_MAP_OT2[getModuleType(moduleModel)], + }, + }) + } + } + + const handleRemoveModule = (moduleType: ModuleType): void => { + const updatedModules = + modules != null + ? Object.fromEntries( + Object.entries(modules).filter( + ([key, value]) => value.type !== moduleType + ) + ) + : {} + setValue('modules', updatedModules) + } - if (modules != null) { - Object.entries(modules).forEach(([key, mod]) => { - if (!seenModels.has(mod.model)) { - seenModels.add(mod.model) - filteredModules[parseInt(key)] = mod + const handleQuantityChange = ( + modules: FormModules, + module: FormModule, + newQuantity: number + ): void => { + const moamModules = + modules != null + ? Object.entries(modules).filter( + ([key, mod]) => mod.type === module.type + ) + : [] + if (newQuantity > moamModules.length) { + const newModules = { ...modules } + for (let i = 0; i < newQuantity - moamModules.length; i++) { + // @ts-expect-error: TS can't determine modules's type correctly + newModules[uuid()] = { + model: module.model, + type: module.type, + slot: null, + } } - }) + setValue('modules', newModules) + } else if (newQuantity < moamModules.length) { + const modulesToRemove = moamModules.length - newQuantity + const remainingModules: FormModules = {} + + Object.entries(modules).forEach(([key, mod]) => { + const shouldRemove = moamModules + .slice(-modulesToRemove) + .some(([removeKey]) => removeKey === key) + if (!shouldRemove) { + remainingModules[parseInt(key)] = mod + } + }) + + setValue('modules', remainingModules) + } } return ( @@ -138,40 +198,27 @@ export function SelectModules(props: WizardTileProps): JSX.Element | null { numSlotsAvailable <= 1) || (moduleModel === 'magneticBlockV1' && hasNoAvailableSlots && - numMagneticBlocks === 4) + numMagneticBlocks === MAX_MAGNETIC_BLOCKS) } textAlignment={TYPOGRAPHY.textAlignLeft} iconName="plus" text={getModuleDisplayName(moduleModel)} onClick={() => { - if (hasNoAvailableSlots) { - makeSnackbar(t('slots_limit_reached') as string) - } else { - setValue('modules', { - ...modules, - [uuid()]: { - model: moduleModel, - type: getModuleType(moduleModel), - slot: - robotType === FLEX_ROBOT_TYPE - ? DEFAULT_SLOT_MAP_FLEX[moduleModel] - : DEFAULT_SLOT_MAP_OT2[ - getModuleType(moduleModel) - ], - }, - }) - } + handleAddModule(moduleModel) }} /> ))} - {modules != null && - Object.keys(modules).length > 0 && - Object.keys(filteredModules).length > 0 ? ( + {modules != null && Object.keys(modules).length > 0 ? ( {t('modules_added')} @@ -180,110 +227,82 @@ export function SelectModules(props: WizardTileProps): JSX.Element | null { flexDirection={DIRECTION_COLUMN} gridGap={SPACING.spacing4} > - {Object.values(filteredModules).map((module, index) => { - const length = Object.values(modules).filter( - mod => module.type === mod.type - ).length - - const dropdownProps = { - currentOption: { - name: `${length}`, - value: `${length}`, - }, - onClick: (value: string) => { - const num = parseInt(value) - const moamModules = - modules != null - ? Object.entries(modules).filter( - ([key, mod]) => mod.type === module.type - ) - : [] - - if (num > moamModules.length) { - const newModules = { ...modules } - for (let i = 0; i < num - moamModules.length; i++) { - // @ts-expect-error: TS can't determine modules's type correctly - newModules[uuid()] = { - model: module.model, - type: module.type, - slot: null, - } - } - setValue('modules', newModules) - } else if (num < moamModules.length) { - const modulesToRemove = moamModules.length - num - const remainingModules: FormModules = {} - - Object.entries(modules).forEach(([key, mod]) => { - const shouldRemove = moamModules - .slice(-modulesToRemove) - .some(([removeKey]) => removeKey === key) - if (!shouldRemove) { - remainingModules[parseInt(key)] = mod - } - }) - - setValue('modules', remainingModules) + {Object.entries(modules) + .reduce>( + (acc, [key, module]) => { + const existingModule = acc.find( + m => m.type === module.type + ) + if (existingModule != null) { + existingModule.count++ + } else { + acc.push({ ...module, count: 1, key }) } + return acc }, - dropdownType: 'neutral' as DropdownBorder, - filterOptions: getNumOptions( - module.model === 'magneticBlockV1' - ? numSlotsAvailable + 3 + length - : numSlotsAvailable + length - ), - } - return ( - - { - const updatedModules = - modules != null - ? Object.fromEntries( - Object.entries(modules).filter( - ([key, value]) => - value.type !== module.type - ) - ) - : {} - setValue('modules', updatedModules) - }} - header={getModuleDisplayName(module.model)} - leftHeaderItem={ - - - - } - /> - + [] ) - })} + .map(module => { + const dropdownProps = { + currentOption: { + name: `${module.count}`, + value: `${module.count}`, + }, + onClick: (value: string) => { + handleQuantityChange( + modules, + module as FormModule, + parseInt(value) + ) + }, + dropdownType: 'neutral' as DropdownBorder, + filterOptions: getNumOptions( + module.model === 'magneticBlockV1' + ? numSlotsAvailable + + MAGNETIC_BLOCKS_ADJUSTMENT + + module.count + : numSlotsAvailable + module.count + ), + } + return ( + + { + handleRemoveModule(module.type) + }} + header={getModuleDisplayName(module.model)} + leftHeaderItem={ + + + + } + /> + + ) + })} ) : null} diff --git a/protocol-designer/src/pages/CreateNewProtocolWizard/SelectPipettes.tsx b/protocol-designer/src/pages/CreateNewProtocolWizard/SelectPipettes.tsx index 86f212718ad..7602b091c8f 100644 --- a/protocol-designer/src/pages/CreateNewProtocolWizard/SelectPipettes.tsx +++ b/protocol-designer/src/pages/CreateNewProtocolWizard/SelectPipettes.tsx @@ -125,7 +125,7 @@ export function SelectPipettes(props: WizardTileProps): JSX.Element | null { { @@ -445,7 +445,7 @@ export function SelectPipettes(props: WizardTileProps): JSX.Element | null { setMount('left') resetFields() }} - text={t('add_pip')} + text={t('add_pipette')} textAlignment="left" iconName="plus" /> @@ -476,7 +476,7 @@ export function SelectPipettes(props: WizardTileProps): JSX.Element | null { setMount('right') resetFields() }} - text={t('add_pip')} + text={t('add_pipette')} textAlignment="left" iconName="plus" /> diff --git a/protocol-designer/src/pages/CreateNewProtocolWizard/WizardBody.tsx b/protocol-designer/src/pages/CreateNewProtocolWizard/WizardBody.tsx index b354ab8fe4a..b70f59a6f9c 100644 --- a/protocol-designer/src/pages/CreateNewProtocolWizard/WizardBody.tsx +++ b/protocol-designer/src/pages/CreateNewProtocolWizard/WizardBody.tsx @@ -48,7 +48,7 @@ export function WizardBody(props: WizardBodyProps): JSX.Element { > - - {t('create_a_protocol')} - + {t('create_a_protocol')} } /> @@ -108,6 +106,14 @@ const StyledLabel = styled.label` display: none; } ` + +const ButtonText = styled.span` + line-height: ${TYPOGRAPHY.lineHeight24}; + font-size: 1rem; + font-style: normal; + font-weight: ${TYPOGRAPHY.fontWeightSemiBold}; +` + const StyledNavLink = styled(NavLink)>` color: ${COLORS.white}; text-decoration: none; From 6873c33c59600217fa2a086a3f4d40719deead2d Mon Sep 17 00:00:00 2001 From: Brent Hagen Date: Mon, 30 Sep 2024 12:02:38 -0400 Subject: [PATCH 05/17] feat(app): add localization feature flag (#16341) Adds the localization feature flag to put localization dev work behind, the initial zh translation assets, and temp language toggles to enable dev work. For now, when the feature flag is enabled, the desktop language toggle is in the app settings advanced tab, ODD in the robot settings page. These will be removed when designs are implemented. closes PLAT-496, re PLAT-493 --- app/src/App/DesktopApp.tsx | 6 +- app/src/App/Navbar.tsx | 5 +- .../assets/localization/en/app_settings.json | 1 + app/src/assets/localization/index.ts | 2 + app/src/assets/localization/zh/anonymous.json | 74 ++++ .../assets/localization/zh/app_settings.json | 103 ++++++ app/src/assets/localization/zh/branded.json | 74 ++++ .../localization/zh/change_pipette.json | 52 +++ .../localization/zh/device_details.json | 195 +++++++++++ .../localization/zh/device_settings.json | 324 ++++++++++++++++++ .../localization/zh/devices_landing.json | 46 +++ .../localization/zh/drop_tip_wizard.json | 39 +++ .../localization/zh/error_recovery.json | 73 ++++ .../localization/zh/firmware_update.json | 17 + .../localization/zh/gripper_wizard_flows.json | 39 +++ .../assets/localization/zh/heater_shaker.json | 48 +++ .../localization/zh/incompatible_modules.json | 7 + app/src/assets/localization/zh/index.ts | 63 ++++ .../zh/instruments_dashboard.json | 10 + .../localization/zh/labware_details.json | 33 ++ .../localization/zh/labware_landing.json | 28 ++ .../zh/labware_position_check.json | 116 +++++++ .../localization/zh/module_wizard_flows.json | 47 +++ .../localization/zh/pipette_wizard_flows.json | 90 +++++ .../zh/protocol_command_text.json | 71 ++++ .../localization/zh/protocol_details.json | 95 +++++ .../assets/localization/zh/protocol_info.json | 86 +++++ .../assets/localization/zh/protocol_list.json | 26 ++ .../localization/zh/protocol_setup.json | 279 +++++++++++++++ .../localization/zh/quick_transfer.json | 76 ++++ .../localization/zh/robot_calibration.json | 131 +++++++ .../localization/zh/robot_controls.json | 14 + .../assets/localization/zh/run_details.json | 154 +++++++++ app/src/assets/localization/zh/shared.json | 84 +++++ .../localization/zh/top_navigation.json | 19 + .../Desktop/AppSettings/AdvancedSettings.tsx | 43 ++- .../RobotSettingsList.tsx | 24 +- app/src/redux/config/constants.ts | 1 + app/src/redux/config/schema-types.ts | 1 + app/src/resources/robot-settings/hooks.ts | 3 +- 40 files changed, 2592 insertions(+), 7 deletions(-) create mode 100644 app/src/assets/localization/zh/anonymous.json create mode 100644 app/src/assets/localization/zh/app_settings.json create mode 100644 app/src/assets/localization/zh/branded.json create mode 100644 app/src/assets/localization/zh/change_pipette.json create mode 100644 app/src/assets/localization/zh/device_details.json create mode 100644 app/src/assets/localization/zh/device_settings.json create mode 100644 app/src/assets/localization/zh/devices_landing.json create mode 100644 app/src/assets/localization/zh/drop_tip_wizard.json create mode 100644 app/src/assets/localization/zh/error_recovery.json create mode 100644 app/src/assets/localization/zh/firmware_update.json create mode 100644 app/src/assets/localization/zh/gripper_wizard_flows.json create mode 100644 app/src/assets/localization/zh/heater_shaker.json create mode 100644 app/src/assets/localization/zh/incompatible_modules.json create mode 100644 app/src/assets/localization/zh/index.ts create mode 100644 app/src/assets/localization/zh/instruments_dashboard.json create mode 100644 app/src/assets/localization/zh/labware_details.json create mode 100644 app/src/assets/localization/zh/labware_landing.json create mode 100644 app/src/assets/localization/zh/labware_position_check.json create mode 100644 app/src/assets/localization/zh/module_wizard_flows.json create mode 100644 app/src/assets/localization/zh/pipette_wizard_flows.json create mode 100644 app/src/assets/localization/zh/protocol_command_text.json create mode 100644 app/src/assets/localization/zh/protocol_details.json create mode 100644 app/src/assets/localization/zh/protocol_info.json create mode 100644 app/src/assets/localization/zh/protocol_list.json create mode 100644 app/src/assets/localization/zh/protocol_setup.json create mode 100644 app/src/assets/localization/zh/quick_transfer.json create mode 100644 app/src/assets/localization/zh/robot_calibration.json create mode 100644 app/src/assets/localization/zh/robot_controls.json create mode 100644 app/src/assets/localization/zh/run_details.json create mode 100644 app/src/assets/localization/zh/shared.json create mode 100644 app/src/assets/localization/zh/top_navigation.json diff --git a/app/src/App/DesktopApp.tsx b/app/src/App/DesktopApp.tsx index 496179b5800..3c9dc3ae253 100644 --- a/app/src/App/DesktopApp.tsx +++ b/app/src/App/DesktopApp.tsx @@ -51,7 +51,7 @@ export const DesktopApp = (): JSX.Element => { const desktopRoutes: RouteProps[] = [ { Component: ProtocolsLanding, - name: 'Protocols', + name: 'protocols', navLinkTo: '/protocols', path: '/protocols', }, @@ -67,13 +67,13 @@ export const DesktopApp = (): JSX.Element => { }, { Component: Labware, - name: 'Labware', + name: 'labware', navLinkTo: '/labware', path: '/labware', }, { Component: DevicesLanding, - name: 'Devices', + name: 'devices', navLinkTo: '/devices', path: '/devices', }, diff --git a/app/src/App/Navbar.tsx b/app/src/App/Navbar.tsx index ed960c93d08..db8a4acd005 100644 --- a/app/src/App/Navbar.tsx +++ b/app/src/App/Navbar.tsx @@ -1,4 +1,5 @@ import * as React from 'react' +import { useTranslation } from 'react-i18next' import { NavLink, useNavigate } from 'react-router-dom' import styled from 'styled-components' import debounce from 'lodash/debounce' @@ -110,6 +111,8 @@ const LogoImg = styled('img')` ` export function Navbar({ routes }: { routes: RouteProps[] }): JSX.Element { + const { t } = useTranslation('top_navigation') + const navigate = useNavigate() const navRoutes = routes.filter( ({ navLinkTo }: RouteProps) => navLinkTo != null @@ -148,7 +151,7 @@ export function Navbar({ routes }: { routes: RouteProps[] }): JSX.Element { as="h3" margin={`${SPACING.spacing8} 0 ${SPACING.spacing8} ${SPACING.spacing12}`} > - {name} + {t(name)} ))} diff --git a/app/src/assets/localization/en/app_settings.json b/app/src/assets/localization/en/app_settings.json index adbc00d3181..933fde16eea 100644 --- a/app/src/assets/localization/en/app_settings.json +++ b/app/src/assets/localization/en/app_settings.json @@ -4,6 +4,7 @@ "__dev_internal__protocolTimeline": "Protocol Timeline", "__dev_internal__enableRunNotes": "Display Notes During a Protocol Run", "__dev_internal__enableLabwareCreator": "Enable App Labware Creator", + "__dev_internal__enableLocalization": "Enable App Localization", "add_folder_button": "Add labware source folder", "add_ip_button": "Add", "add_ip_error": "Enter an IP Address or Hostname", diff --git a/app/src/assets/localization/index.ts b/app/src/assets/localization/index.ts index e92a7077ed9..2b16ff8c61b 100644 --- a/app/src/assets/localization/index.ts +++ b/app/src/assets/localization/index.ts @@ -1,5 +1,7 @@ import { en } from './en' +import { zh } from './zh' export const resources = { en, + zh, } diff --git a/app/src/assets/localization/zh/anonymous.json b/app/src/assets/localization/zh/anonymous.json new file mode 100644 index 00000000000..696dff65751 --- /dev/null +++ b/app/src/assets/localization/zh/anonymous.json @@ -0,0 +1,74 @@ +{ + "a_robot_software_update_is_available": "需要更新工作站软件版本才能使用该版本的桌面应用程序运行协议。转到工作站这个金属块是一个特制的工具,完美适配您的甲板,有助于校准。如果您没有校准块,请发送电子邮件给支持团队,以便我们寄送一个给您。在您提供的信息中,请确保包括您的姓名、公司或机构名称和寄送地址。在等待校准块到达过程中,您可以暂时利用工作站里垃圾桶上的平面进行校准。", + "calibration_on_opentrons_tips_is_important": "使用上述吸头和吸头盒进行校准非常重要,因为工作站的准确性是基于这些吸头的已知尺寸来确定的。", + "choose_what_data_to_share": "选择要共享的工作站数据。", + "computer_in_app_is_controlling_robot": "当前正在由一台已联网的计算机控制此工作站。", + "confirm_terminate": "这将立即停止计算机上进行的活动。您或其他用户可能会丢失进度或在该计算机上出现报错提示。", + "connect_and_screw_in_gripper": "连接并固定转板抓手", + "connect_via_usb_description_3": "3. 在连接的计算机上启动工作站应用程序以继续。", + "connection_description_usb": "直接连接到计算机。", + "connection_lost_description": "应用程序现在无法与此工作站通信。请检查与工作站的USB或Wi-Fi连接,然后尝试重新连接。", + "contact_information": "请与支持人员联系以获得帮助。", + "contact_support_for_connection_help": "如果以上方法都无法解决问题,请联系支持人员寻求帮助(通过此应用程序中的问号链接,或发送电子邮件至{{support_email}}。)", + "deck_fixture_setup_modal_bottom_description": "有关安装不同类型固定装置的详细信息,请与支持人员联系。", + "delete_protocol_from_app": "删除协议,针对错误进行修改,然后从桌面应用程序将协议重新发送到此工作站。", + "error_boundary_description": "您需要重新启动触摸屏。联系支持人员以获取帮助。", + "estop_pressed_description": "首先,安全清理甲板上的任何实验耗材和洒出的液体。然后,顺时针旋转急停开关。最后,让工作站将龙门架移动到其原位。", + "find_your_robot": "在应用程序的“设备”栏找到您的工作站,以安装软件更新。", + "firmware_update_download_logs": "请与支持人员联系以获得帮助。", + "general_error_message": "如果该消息反复出现,请尝试重新启动您的应用程序和工作站。如果这不能解决问题,请与支持人员联系。", + "gripper_still_attached": "转板抓手仍处于连接状态", + "gripper_successfully_attached_and_calibrated": "转板抓手已成功连接并校准", + "gripper_successfully_calibrated": "转板抓手已成功校准", + "gripper_successfully_detached": "转板抓手已成功卸下", + "gripper": "转板抓手", + "help_us_improve_send_error_report": "通过向支持团队发送错误报告,帮助我们改进您的使用体验", + "ip_description_second": "请联系网络管理员,为工作站分配静态IP地址。", + "learn_uninstalling": "了解更多有关卸载应用程序的信息", + "loosen_screws_and_detach": "松开螺丝并卸下转板抓手", + "modal_instructions": "有关设置模块的分步说明,请参阅随包装附带的快速指引。", + "module_calibration_failed": "模块校准失败。请确保校准适配器正确放置在模块上,然后重试。如果仍然有问题,请与支持人员联系。{{error}}", + "module_calibration_get_started": "开始前,请从工作台上移除实验耗材并清理工作区,以便于校准。还需准备好右侧显示的所需设备。校准适配器随模块一起提供。移液器探头随移液器一起提供。", + "module_error_contact_support": "尝试关闭模块电源,然后再打开。如果报错仍然存在,请与支持人员联系。", + "network_setup_menu_description": "您将使用此连接来运行软件更新,并将协议加载到您的工作站上。", + "oem_mode_description": "启用OEM模式,以从Flex触摸屏中移除Opentrons的相关信息。", + "opentrons_app_successfully_updated": "应用程序已成功更新。", + "opentrons_app_update": "应用程序更新", + "opentrons_app_update_available": "应用程序可更新", + "opentrons_app_update_available_variation": "有可用的应用程序更新。", + "opentrons_app_will_use_interpreter": "在指定路径后,应用程序将使用此路径的Python解释器,而不是默认绑定的Python解释器。", + "opentrons_cares_about_privacy": "我们注重您的隐私。我们匿名化所有数据,仅用于改进我们的产品。", + "opentrons_def": "已验证的数据", + "opentrons_labware_def": "已验证的实验耗材数据", + "opentrons_tip_racks_recommended": "建议使用Opentrons吸头盒。其他吸头盒无法保证精度。", + "opentrons_tip_rack_name": "opentrons", + "previous_releases": "查看以前的版本", + "receive_alert": "当软件更新可用时接收提醒。", + "restore_description": "不建议恢复到过往的软件版本,但您可以访问下方的过往版本。为了获得最佳效果,请在安装过往版本之前卸载现有应用程序并删除其配置文件。", + "robot_server_version_ot3_description": "工作站软件包括工作站服务器和触摸屏显示界面。", + "robot_software_update_required": "需要对工作站软件进行更新才能运行此版本应用程序的协议。", + "run_failed_modal_description_desktop": "请与支持人员联系以获得帮助。", + "secure_labware_explanation_magnetic_module": "通过调整模块顶部的黑色固定支架,确保您的实验耗材固定在磁性模块上。 您的模块提供了两种尺寸的板固定支架:标准和深孔。这些固定支架可以通过拧开模块的拇指螺钉(前面的银色旋钮)来移除和更换。", + "secure_labware_explanation_thermocycler": "通过关闭热循环仪模块的闩锁将您的实验耗材固定在其上。这样可以确保水平以及板位置的准确,从而获得最佳结果。", + "send_a_protocol_to_store": "向工作站发送协议以开始。", + "setup_instructions_description": "有关设置模块的分步说明,请参阅随包装附带的快速指引。", + "share_app_analytics": "共享应用程序分析数据", + "share_app_analytics_description": "通过自动发送匿名诊断和使用数据来帮助改进此产品。", + "share_display_usage_description": "关于工作站触摸屏的交互数据。", + "share_logs_with_opentrons": "共享工作站日志", + "share_logs_with_opentrons_description": "通过自动发送匿名的工作站日志来帮助改进此产品。这些日志用于解决工作站问题和发现错误趋势。", + "show_labware_offset_snippets_description": "仅适用于需要在应用程序之外应用耗材校准数据的用户。启用后,在设置协议过程中可访问Jupyter Notebook和SSH的代码片段。", + "something_seems_wrong": "您的移液器可能有问题。退出设置并联系支持人员以获取帮助。", + "these_are_advanced_settings": "这些是高级设置。请勿在没有支持团队帮助的情况下尝试调整这些设置。更改这些设置可能会影响您的移液器寿命。这些设置不会覆盖协议中定义的任何移液器设置。", + "update_requires_restarting_app": "更新需要重新启动应用程序。", + "update_robot_software_description": "绕过自动更新过程并手动更新工作站软件", + "update_robot_software_link": "启动软件更新页面", + "versions_sync": "了解更多有关保持应用程序和工作站软件同步的信息", + "view_latest_release_notes_at": "请联系支持人员以获取发布说明。", + "want_to_help_out": "想要帮助吗?", + "welcome_title": "欢迎!", + "why_use_lpc": "耗材位置校准旨在纠正微小的偏差,请不要使用该校准方式来进行较大的位置补偿调整。如果需要进行较大调整的耗材数据校准可能表明工作站的校准存在问题。" +} diff --git a/app/src/assets/localization/zh/app_settings.json b/app/src/assets/localization/zh/app_settings.json new file mode 100644 index 00000000000..9e4a9f1bbc7 --- /dev/null +++ b/app/src/assets/localization/zh/app_settings.json @@ -0,0 +1,103 @@ +{ + "__dev_internal__forceHttpPolling": "强制轮询所有网络请求,而不是使用MQTT", + "__dev_internal__protocolStats": "协议统计", + "__dev_internal__enableRunNotes": "在协议运行期间显示备注", + "__dev_internal__enableQuickTransfer": "启用快速移液", + "__dev_internal__enableLabwareCreator": "Enable App Labware Creator", + "__dev_internal__enableLocalization": "Enable App Localization", + "add_folder_button": "添加实验耗材源文件夹", + "add_ip_button": "添加", + "add_ip_error": "输入IP地址或主机名", + "add_ip_hostname": "添加IP地址或主机名", + "add_override_path": "添加覆盖路径", + "additional_folder_description": "如果要指定一个文件夹来手动管理自定义实验耗材文件,可以在此处添加目录。", + "additional_folder_location": "附加源文件夹", + "additional_labware_folder_title": "其他定制实验耗材源文件夹", + "advanced": "高级", + "app_changes": "应用程序更改于", + "app_settings": "应用设置", + "bug_fixes": "错误修复", + "cal_block": "始终使用校准块进行校准", + "change_folder_button": "更改实验耗材源文件夹", + "channel": "通道", + "clear_confirm": "清除不可用的工作站", + "clear_robots_button": "清除不可用工作站列表", + "clear_robots_description": "清除设备页面上不可用工作站的列表。此操作无法撤消。", + "clear_unavail_robots": "清除不可用工作站", + "clear_unavailable_robots": "清除不可用的工作站?", + "clearing_cannot_be_undone": "在设备页面清除不可用工作站列表的操作无法撤销。", + "close": "关闭", + "connect_ip": "通过IP地址连接到工作站", + "connect_ip_button": "完成", + "connect_ip_link": "了解更多关于手动连接工作站的信息", + "discovery_timeout": "发现超时。", + "download_update": "正在下载更新...", + "enable_dev_tools": "开发者工具", + "enable_dev_tools_description": "启用此设置将在应用启动时打开开发者工具,打开额外的日志记录并访问功能标志。", + "error_boundary_desktop_app_description": "您需要重新加载应用程序。出现以下错误信息,请联系技术支持:", + "error_boundary_title": "发生未知错误", + "feature_flags": "功能标志", + "general": "通用", + "heater_shaker_attach_description": "在进行测试振荡功能或在协议中使用热震荡模块功能之前,显示正确连接热震荡模块的提醒。", + "heater_shaker_attach_visible": "确认热震荡模块连接", + "how_to_restore": "如何恢复过往的软件版本", + "installing_update": "正在安装更新...", + "ip_available": "可用", + "ip_description_first": "输入IP地址或主机名以连接到工作站。", + "manage_versions": "工作站版本和应用程序软件版本必须一致。通过工作站设置 > 高级查看工作站软件版本。", + "new_features": "新功能", + "no_folder": "未指定其他源文件夹", + "no_specified_folder": "未指定路径", + "no_unavail_robots_to_clear": "无待清除的不可用工作站", + "not_found": "未找到", + "opt_in": "加入", + "opt_in_description": "自动向我们发送匿名诊断和使用数据。我们仅将这些信息用于改进我们的产品。", + "opt_out": "退出", + "ot2_advanced_settings": "OT-2高级设置", + "override_path": "覆盖路径", + "override_path_to_python": "覆盖Python路径", + "prevent_robot_caching": "阻止工作站进行缓存", + "prevent_robot_caching_description": "启用此功能后,应用程序将立即清除不可用的工作站,并且不会记住它们。在网络上有许多工作站的情况下,防止缓存可能会提高网络性能,但代价是在应用程序启动时工作站发现的速度变慢且可靠性降低。", + "privacy": "隐私", + "problem_during_update": "此次更新耗时较长。", + "prompt": "始终显示选择校准块或垃圾桶的提示", + "release_notes": "发行说明", + "reload_app": "重新加载应用程序", + "remind_later": "稍后提醒我", + "reset_to_default": "恢复默认设置", + "restart_touchscreen": "重启触摸屏", + "restarting_app": "下载完成,正在重启应用程序...", + "restore_previous": "查看如何恢复过往软件版本", + "searching": "正在搜索30秒", + "setup_connection": "设置连接", + "share_display_usage": "分享屏幕使用情况", + "share_robot_logs": "分享工作站日志", + "share_robot_logs_description": "工作站执行的操作数据,如运行协议。", + "show_labware_offset_snippets": "显示实验耗材校准数据代码片段", + "software_update_available": "有可用的软件更新", + "software_version": "应用程序软件版本", + "successfully_deleted_unavail_robots": "成功删除不可用的工作站", + "tip_length_cal_method": "吸头长度校准方法", + "trash_bin": "始终使用垃圾桶进行校准", + "try_restarting_the_update": "尝试重新启动更新。", + "turn_off_updates": "在应用程序设置中关闭软件更新通知。", + "up_to_date": "最新", + "update_alerts": "软件更新提醒", + "update_app_now": "立即更新应用程序", + "update_available": "更新可用", + "update_channel": "更新通道", + "update_description": "稳定版将接收最新的稳定版本。测试版将允许您在稳定版发布之前尝试正在开发的新功能,但这些功能尚未完成测试。", + "usb_to_ethernet_adapter_description": "描述", + "usb_to_ethernet_adapter_driver_version": "驱动程序版本", + "usb_to_ethernet_adapter_info": "USB-to-Ethernet适配器信息", + "usb_to_ethernet_adapter_info_description": "OT-2内置了一些USB-to-Ethernet适配器。如果您使用的OT-2有这个适配器,当您建立有线连接时,它将添加到您的计算机设备列表中。如果您使用的是 Realtek 适配器,驱动程序必须是最新的。", + "usb_to_ethernet_adapter_link": "前往Realtek.com", + "usb_to_ethernet_adapter_manufacturer": "制造商", + "usb_to_ethernet_adapter_no_driver_version": "未知", + "usb_to_ethernet_adapter_toast_message": "Realtek USB-to-Ethernet适配器驱动程序有更新", + "usb_to_ethernet_not_connected": "没有连接USB-to-Ethernet适配器", + "usb_to_ethernet_unknown_manufacturer": "未知制造商", + "usb_to_ethernet_unknown_product": "未知适配器", + "view_software_update": "查看软件更新", + "view_update": "查看更新" +} diff --git a/app/src/assets/localization/zh/branded.json b/app/src/assets/localization/zh/branded.json new file mode 100644 index 00000000000..4bff5f976d4 --- /dev/null +++ b/app/src/assets/localization/zh/branded.json @@ -0,0 +1,74 @@ +{ + "a_robot_software_update_is_available": "需要更新工作站软件才能使用此版本的Opentrons应用程序运行协议。转到工作站", + "about_flex_gripper": "关于Flex转板抓手", + "alternative_security_types_description": "Opentrons应用程序支持将Flex连接到各种企业接入点。通过USB连接并在应用程序中完成设置。", + "calibration_block_description": "这个金属块是一个特制的工具,完美适配您的甲板,有助于校准。如果您没有校准块,请发送电子邮件至support@opentrons.com,以便我们寄送一个给您。在您提供的信息中,请确保包括您的姓名、公司或机构名称和寄送地址。在等待校准块到达过程中,您可以暂时利用工作站里垃圾桶上的平面进行校准。", + "calibration_on_opentrons_tips_is_important": "使用上述Opentrons吸头和吸头盒进行校准非常重要,因为工作站的准确性是基于这些吸头的已知尺寸来确定的。", + "choose_what_data_to_share": "选择要与Opentrons共享的数据。", + "computer_in_app_is_controlling_robot": "计算机当前正在通过Opentrons应用程序控制此工作站。", + "confirm_terminate": "这将立即停止计算机上开始的活动。您或其他用户可能会丢失进度或在Opentrons应用程序中看到报错", + "connect_and_screw_in_gripper": "连接并固定Flex转板抓手", + "connect_via_usb_description_3": "3. 在计算机上启动Opentrons应用程序以继续。", + "connection_description_usb": "直接连接到计算机(运行Opentrons应用程序)。", + "connection_lost_description": "Opentrons应用程序现在无法与此工作站通信。请仔细检查与工作站的USB或Wi-Fi连接,然后尝试重新连接。", + "contact_information": "从Opentrons应用程序下载工作站日志,并将其发送到support@opentrons.com寻求帮助。", + "contact_support_for_connection_help": "如果以上方法都无法解决问题,请联系Opentrons支持人员寻求帮助(通过此应用程序中的问号链接,或发送电子邮件至{{support_email}}。)", + "deck_fixture_setup_modal_bottom_description": "有关安装不同类型固定装置的详细信息,请扫描二维码或在support.opentrons.com上搜索“deck configuration”", + "delete_protocol_from_app": "删除协议,针对错误进行修改,然后从Opentrons应用程序将协议重新发送到此工作站。", + "error_boundary_description": "您需要重新启动触摸屏。然后从Opentrons应用程序下载工作站日志并将其发送到support@opentrons.com寻求帮助。", + "estop_pressed_description": "首先,安全清理甲板上的任何实验耗材或洒出液体。然后,顺时针旋转急停开关。最后,让Flex将龙门架移动到其原位。", + "find_your_robot": "在Opentrons应用程序中找到您的工作站以安装软件更新。", + "firmware_update_download_logs": "从Opentrons应用程序下载工作站日志并将其发送到support@opentrons.com寻求帮助。", + "general_error_message": "如果您一直收到此消息,请尝试重新启动您的应用程序和工作站。如果这不能解决问题,请与Opentrons支持人员联系。", + "gripper_still_attached": "Flex转板抓手仍处于连接状态", + "gripper_successfully_attached_and_calibrated": "Flex转板抓手已成功连接并校准", + "gripper_successfully_calibrated": "Flex转板抓手已成功校准", + "gripper_successfully_detached": "Flex转板抓手已成功卸下", + "gripper": "Flex转板抓手", + "help_us_improve_send_error_report": "通过向{{support_email}}发送错误报告,帮助我们改进您的使用体验", + "ip_description_second": "Opentrons建议您联系网络管理员,为工作站分配静态IP地址。", + "learn_uninstalling": "了解更多有关卸载Opentrons应用程序的信息", + "loosen_screws_and_detach": "松开螺丝并卸下Flex转板抓手", + "modal_instructions": "有关设置模块的分步说明,请参阅随包装附带的快速指引。您也可以单击下面的链接或扫描二维码访问Opentrons帮助中心的模块部分。", + "module_calibration_failed": "模块校准失败。请确保校准适配器正确放置在模块上,然后重试。如果仍然有问题,请与Opentrons支持人员联系。{{error}}", + "module_calibration_get_started": "开始前,请从工作台上移除实验耗材并清理工作区,以便于校准。还需准备好右侧显示的所需设备。校准适配器随模块一起提供。移液器探头随Flex移液器一起提供。", + "module_error_contact_support": "尝试关闭模块电源,然后再打开。如果报错仍然存在,请与Opentrons支持人员联系。", + "network_setup_menu_description": "您将使用此连接来运行软件更新,并将协议加载到您的Opentrons Flex上。", + "oem_mode_description": "启用OEM模式,以从Flex触摸屏中移除Opentrons的所有信息。", + "opentrons_app_successfully_updated": "Opentrons应用程序已成功更新。.", + "opentrons_app_update": "Opentrons应用程序更新", + "opentrons_app_update_available": "Opentrons应用程序可更新", + "opentrons_app_update_available_variation": "有可用的Opentrons应用程序更新。", + "opentrons_app_will_use_interpreter": "如果指定,Opentrons应用程序将在此路径使用Python解释器,而不是默认绑定的Python解释器。", + "opentrons_cares_about_privacy": "Opentrons关心您的隐私。我们匿名化所有数据,仅用于改进我们的产品。", + "opentrons_def": "已验证的Opentrons数据", + "opentrons_labware_def": "已验证的Opentrons实验耗材数据", + "opentrons_tip_rack_name": "opentrons", + "opentrons_tip_racks_recommended": "建议使用Opentrons吸头盒。其他吸头盒无法保证精度。", + "previous_releases": "查看过往的Opentrons版本", + "receive_alert": "当Opentrons软件更新可用时接收提醒。", + "restore_description": "Opentrons不建议恢复到过往软件版本,但您可以访问下方的过往版本。为了获得最佳效果,请在安装过往版本之前卸载现有应用程序并删除其配置文件。", + "robot_server_version_ot3_description": "Opentrons Flex软件包括工作站服务器和触摸屏显示界面。", + "robot_software_update_required": "需要更新工作站软件才能运行此版本Opentrons应用程序的协议。", + "run_failed_modal_description_desktop": "下载运行日志并将其发送到support@opentrons.com寻求帮助。", + "secure_labware_explanation_magnetic_module": "Opentrons建议通过调整模块顶部的黑色固定支架,确保您的实验耗材固定在磁性模块上。 您的模块提供了两种尺寸的板固定支架:标准和深孔。这些固定支架可以通过拧开模块的拇指螺钉(前面的银色旋钮)来移除和更换。", + "secure_labware_explanation_thermocycler": "Opentrons建议通过关闭热循环仪模块的闩锁将您的实验耗材固定在其上。这样可以确保水平以及板位置的准确,从而获得最佳结果。", + "send_a_protocol_to_store": "从Opentrons应用程序发送协议以开始。", + "setup_instructions_description": "有关设置模块的分步说明,请参阅随包装附带的快速指引,或扫描二维码访问Opentrons帮助中心的模块部分。", + "share_app_analytics": "向Opentrons共享应用分析", + "share_app_analytics_description": "通过自动发送匿名诊断和使用数据,帮助Opentrons改进产品和服务。", + "share_display_usage_description": "关于您如何在Flex上与触摸屏交互的数据。", + "share_logs_with_opentrons": "向opentrons共享工作站日志", + "share_logs_with_opentrons_description": "通过自动发送匿名的工作站日志,帮助Opentrons改进产品和服务。Opentrons使用这些日志来解决工作站问题并发现错误趋势。", + "show_labware_offset_snippets_description": "仅适用于需要在Opentrons应用程序之外应用耗材校准数据的用户。启用后,在设置协议过程中可访问Jupyter Notebook和SSH的代码片段。", + "something_seems_wrong": "您的移液器可能有问题。退出设置并联系Opentrons支持人员以获取帮助。", + "these_are_advanced_settings": "这些是高级设置。请勿在没有Opentrons支持团队帮助的情况下尝试调整这些设置。更改这些设置可能会影响您的移液器寿命。这些设置不会覆盖协议中定义的任何移液器设置。", + "update_requires_restarting_app": "更新需要重新启动Opentrons应用程序。", + "update_robot_software_description": "绕过Opentrons应用程序自动更新过程并手动更新工作站软件", + "update_robot_software_link": "启动Opentrons软件更新页面", + "versions_sync": "了解更多有关保持Opentrons应用程序和工作站软件同步的信息", + "view_latest_release_notes_at": "在{{url}}上查看最新发布说明", + "want_to_help_out": "想帮助Opentrons吗?", + "welcome_title": "欢迎使用您的Opentrons Flex!", + "why_use_lpc": "耗材位置校准旨在纠正微小的偏差,Opentrons不建议使用该校准方式来进行较大的位置补偿调整。如果需要进行较大调整的耗材数据校准可能表明工作站的校准存在问题。" +} diff --git a/app/src/assets/localization/zh/change_pipette.json b/app/src/assets/localization/zh/change_pipette.json new file mode 100644 index 00000000000..9818fd56f85 --- /dev/null +++ b/app/src/assets/localization/zh/change_pipette.json @@ -0,0 +1,52 @@ +{ + "are_you_sure_exit": "您确定要在{{direction}}移液器之前退出吗?", + "attach_name_pipette": "安装一个{{pipette}}移液器", + "attach_pipette_type": "安装一个{{pipetteName}}移液器", + "attach_pipette": "安装一个移液器", + "attach_the_pipette": "

连接移液器

推入白色连接器,直到感觉它插入移液器。", + "attached_pipette_does_not_match": "连接的{{name}}与您最初选择的{{pipette}}不匹配。", + "attaching": "正在连接", + "calibrate_pipette_offset": "校准移液器偏移", + "cancel_attachment": "取消连接", + "check_pipette_is_unplugged": "再次检查以确保移液器连接线已拔下并完全从工作站上卸下。", + "choose_pipette": "选择一个要连接的移液器", + "confirm_attachment": "确认连接", + "confirm_detachment": "确认卸下", + "confirm_level": "确认水平", + "confirming_attachment": "正在确认连接", + "confirming_detachment": "正在确认拆卸", + "continue": "继续", + "detach_pipette_from_mount": "从{{mount}}安装支架上卸下移液器", + "detach_pipette": "从{{mount}}安装支架上卸下{{pipette}}", + "detach_try_again": "卸下并重试", + "detach": "卸下移液器", + "detaching": "正在卸下", + "get_started": "开始", + "go_back": "返回", + "homing": "工作站正在归位", + "incorrect_pipette_attached": "连接了错误的移液器", + "insert_screws": "

插入螺钉

使用 2.5 毫米螺丝刀,插入移液器背面的三个螺丝。", + "leave_attached": "保持连接", + "level_the_pipette": "

调整移液器水平

用您的手,轻轻地、慢慢地将移液器向上推。将移液器向下拉,使 8 个喷嘴都接触到校准块的表面。按住移液器的同时,拧紧三个螺钉。", + "loosen_the_screws": "

松开螺丝

用一把 2.5 毫米的螺丝刀,拧松移液器背面的三颗螺丝。.", + "mount": "{{mount}}安装支架", + "moving_gantry": "移动龙门架...", + "pipette_attached": "移液器已连接!", + "pipette_is_ready_to_use": "{{pipette}}现在可以使用。", + "pipette_movement": "{{mount}}移液器托架正在向{{location}}移动。", + "pipette_setup": "移液器设置", + "pipette_still_detected": "仍然检测到移液器", + "press_white_connector": "确保将白色连接器按到底,并感觉到它与移液器连接。", + "progress_will_be_lost": "进度将丢失", + "recheck_connection": "重新检查连接", + "remove_labware_before_start": "

开始之前

开始前,请从平台上取下所有实验耗材,并从移液器上取下所有吸头。龙门将移动到工作站前端.", + "remove_pipette": "

卸下移液器

抓紧移液器,以免其掉落。拉动白色连接器,断开移液器与工作站的连接。", + "successfully_detached_pipette": "成功卸下移液器!", + "tighten_screws_multi": "从 1 号螺丝开始,以顺时针方向轻轻地拧紧螺丝。您将在后面的步骤中完全拧紧。", + "tighten_screws_single": "从第1号螺丝开始,顺时针方向拧紧螺丝。", + "to_front_left": "到左前方", + "to_front_right": "到右前方", + "unable_to_detect_pipette": "无法检测到{{pipette}}", + "up": "上", + "use_attached_pipette": "使用连接的移液器" +} diff --git a/app/src/assets/localization/zh/device_details.json b/app/src/assets/localization/zh/device_details.json new file mode 100644 index 00000000000..39115321f99 --- /dev/null +++ b/app/src/assets/localization/zh/device_details.json @@ -0,0 +1,195 @@ +{ + "about_gripper": "关于转板抓手", + "about_module": "关于{{name}}", + "about_pipette_name": "关于{{name}}移液器", + "about_pipette": "关于移液器", + "add_fixture_description": "将此硬件添加至甲板配置。它在协议分析期间将会被引用。", + "add_to_slot": "添加到板位{{slotName}}", + "add": "添加", + "an_error_occurred_while_updating_module": "更新{{moduleName}}时出现错误,请重试。", + "an_error_occurred_while_updating_please_try_again": "更新移液器设置时出错,请重试。", + "an_error_occurred_while_updating": "更新移液器设置时发生错误。", + "attach_gripper": "安装转板抓手", + "attach_pipette": "安装移液器", + "bad_run": "无法加载运行", + "both_mounts": "两侧支架", + "bundle_firmware_file_not_found": "未找到类型为{{module}}的模块固件包文件。", + "calibrate_gripper": "校准转板抓手", + "calibrate_now": "立即校准", + "calibrate_pipette_offset": "校准移液器数据", + "calibrate_pipette": "校准移液器", + "calibration_needed_without_link": "需要校准。", + "calibration_needed": "需要校准。 立即校准", + "canceled": "已取消", + "changes_will_be_lost_description": "确定不保存甲板配置直接退出而吗?", + "changes_will_be_lost": "更改将丢失", + "choose_protocol_to_run": "选择在{{name}}上运行的协议", + "close_lid": "关闭上盖", + "completed": "已完成", + "confirm": "确认", + "continue_editing": "继续编辑", + "controls": "控制", + "csv_required_for_analysis": "分析需要CSV文件。请在运行设置时添加。", + "current_speed": "当前:{{speed}}rpm", + "current_temp": "当前:{{temp}}°C", + "current_version": "当前版本", + "deck_cal_missing": "缺少移液器校准数据,请先校准甲板。", + "deck_configuration_is_not_available_when_robot_is_busy": "工作站忙碌时,甲板配置不可用", + "deck_configuration_is_not_available_when_run_is_in_progress": "工作站运行时,甲板配置不可用", + "deck_configuration": "甲板配置", + "deck_fixture_setup_instructions": "甲板配置安装说明", + "deck_fixture_setup_modal_bottom_description_desktop": "针对不同类型的配置,扫描二维码或访问下方链接获取详细说明。", + "deck_fixture_setup_modal_top_description": "首先,拧松并移除计划安装模组的甲板。然后放置模组,并进行固定。", + "deck_hardware": "甲板硬件", + "deck_slot": "甲板板位{{slot}}", + "delete_run": "删除协议运行记录", + "detach_gripper": "卸下转板抓手", + "detach_pipette": "卸下移液器", + "discard_changes": "放弃更改", + "disengaged": "不启用", + "download_run_log": "下载协议运行日志", + "drop_tips": "丢弃吸头", + "empty": "空", + "error_details": "错误详情", + "estop_disconnected": "急停断开。工作站运动已停止。", + "estop_disengaged": "急停解除,但工作站操作仍暂停中。", + "estop_pressed": "急停按钮被按下。工作站运动已停止。", + "failed": "失败", + "files": "文件", + "firmware_update_needed": "需要更新工作站固件。请在工作站的触摸屏上开始更新。", + "firmware_update_available": "固件更新可用。", + "firmware_update_failed": "未能更新模块固件", + "firmware_updated_successfully": "固件更新成功", + "firmware_update_occurring": "固件更新正在进行中...", + "fixture": "配置模组", + "have_not_run_description": "运行一些协议后,它们会在这里显示。", + "have_not_run": "无最近运行记录", + "heater": "加热器", + "height_ranges": "{{gen}}高度范围", + "hot_to_the_touch": "模块接触时很热", + "input_out_of_range": "输入超出范围", + "instrument_attached": "设备已连接", + "instruments_and_modules": "设备与模块", + "labware_bottom": "耗材底部", + "last_run_time": "最后一次运行{{number}}", + "left_right": "左右支架", + "left": "左侧", + "lights": "灯光", + "link_firmware_update": "查看固件更新", + "location_conflicts": "位置冲突", + "location": "位置", + "magdeck_gen1_height": "高度:{{height}}", + "magdeck_gen2_height": "高度:{{height}}毫米", + "max_engage_height": "最大可用高度", + "missing_fixture": "缺少{{num}}个配置", + "missing_fixtures_plural": "缺少{{count}}个配置", + "missing_hardware": "缺少硬件", + "missing_instrument": "缺少{{num}}个设备", + "missing_instruments_plural": "缺少{{count}}个设备", + "missing_module_plural": "缺少{{count}}个模块", + "missing_module": "缺少{{num}}个模块", + "module_actions_unavailable": "协议运行时模块操作不可用", + "module_calibration_required_no_pipette_attached": "需要模块校准。在运行模块校准前,请连接移液器。", + "module_calibration_required_no_pipette_calibrated": "需要模块校准。在校准模块前,请先校准移液器。", + "module_calibration_required_update_pipette_FW": "在进行必要的模块校准前,请先更新移液器固件。", + "module_calibration_required": "需要模块校准。", + "module_controls": "模块控制", + "module_error": "模块错误", + "module_name_error": "{{moduleName}}错误", + "module_status_range": "介于{{min}}至{{max}}{{unit}}之间", + "mount": "{{side}}安装座", + "na_speed": "目标速度: N/A", + "na_temp": "目标温度: N/A", + "no_deck_fixtures": "无甲板配置", + "no_protocol_runs": "暂无协议运行记录!", + "no_protocols_found": "未找到协议", + "no_recent_runs_description": "运行一些协议后,它们将显示在此处。", + "no_recent_runs": "无最近运行记录", + "num_units": "{{num}}毫米", + "offline_deck_configuration": "工作站必须连接网络才能查看甲板配置", + "offline_instruments_and_modules": "工作站必须连接网络才能查看已连接的设备和模块", + "offline_recent_protocol_runs": "工作站必须连接网络才能查看协议运行情况", + "open_lid": "打开上盖", + "overflow_menu_about": "关于模块", + "overflow_menu_deactivate_block": "停用模块", + "overflow_menu_deactivate_lid": "停用上盖", + "overflow_menu_deactivate_temp": "停用模块", + "overflow_menu_disengage": "模块下降", + "overflow_menu_engage": "设置模块高度", + "overflow_menu_lid_temp": "设置上盖温度", + "overflow_menu_mod_temp": "设置模块温度", + "overflow_menu_set_block_temp": "设置模块温度", + "pipette_cal_recommended": "建议进行移液器校准。", + "pipette_calibrations_differ": "所连接的移液器校准数据差异过大。正确校准后,这些数据应相近。", + "pipette_offset_calibration_needed": "需要进行移液器校准。", + "pipette_recalibration_recommended": "建议重新校准移液器", + "pipette_settings": "{{pipetteName}}设置", + "plunger_positions": "活塞位置", + "power_force": "功率 / 力量", + "protocol": "协议", + "protocol_analysis_failed": "协议APP分析失败。", + "protocol_analysis_stale": "协议分析已过期。", + "protocol_details_page_reanalyze": "前往协议详情页面重新分析。", + "ready_to_run": "运行工作准备完成", + "ready": "准备就绪", + "recalibrate_gripper": "重新校准转板抓手", + "recalibrate_now": "立即重新校准", + "recalibrate_pipette_offset": "重新校准移液器偏移", + "recalibrate_pipette": "重新校准移液器", + "recent_protocol_runs": "最近的协议运行", + "rerun_now": "立即重新运行协议", + "reset_all": "重置全部", + "reset_estop": "重置急停", + "resume_operation": "继续操作", + "right": "右侧", + "robot_control_not_available": "运行过程中某些工作站控制功能不可用", + "robot_initializing": "初始化中...", + "run_a_protocol": "运行协议", + "run_again": "再次运行", + "run_duration": "运行时长", + "run": "运行", + "select_options": "选择选项", + "serial_number": "序列号", + "set_block_temp": "设置温度", + "set_block_temperature": "设置模块温度", + "set_engage_height_and_enter_integer": "为此磁力模块设置启用高度。请输入一个介于{{lower}}和{{higher}}之间的整数。", + "set_engage_height_for_module": "为{{name}}设置启用高度", + "set_engage_height": "设置启用高度", + "set_lid_temperature": "设置上盖温度", + "set_shake_of_hs": "为此模块设置转速。", + "set_shake_speed": "设置震荡速度", + "set_status_heater_shaker": "为{{name}}设置{{part}}", + "set_target_temp_of_hs": "设置目标温度。此模块主动加热,但被动冷却至室温。", + "set_temp_or_shake": "设置{{part}}", + "set_temperature": "设置温度", + "setup_instructions": "设置说明", + "shake_speed": "震荡速度", + "shaker": "震荡仪", + "staging_area_slot": "暂存区板位", + "status": "状态", + "target_speed": "目标转速:{{speed}}rpm", + "target_temp": "目标温度:{{temp}}°C", + "tc_block": "模块", + "tc_lid": "上盖", + "tc_set_temperature_body": "预热或预冷您的热循环模块的{{part}}。请输入介于{{min}}°C 和{{max}}°C之间的一个整数。", + "tc_set_temperature": "为{{name}}设置温度{{part}}", + "tempdeck_slideout_body": "预热或冷却您的{{model}}。输入4°C至96°C之间的一个整数。", + "tempdeck_slideout_title": "为{{name}}设置温度", + "temperature": "温度", + "this_robot_will_restart_with_update": "此工作站需要重启以更新软件。重启会立即终止当前的运行或校准。您仍然要现在更新吗?", + "tip_pickup_drop": "吸头拾取/丢弃", + "to_run_protocol_go_to_protocols_page": "要在该工作站上运行协议,请在协议页面导入协议。", + "trash": "垃圾桶", + "update_now": "立即更新", + "updating_firmware": "正在更新固件...", + "usb_port_not_connected": "USB未连接", + "usb_port": "USB端口-{{port}}", + "version": "版本{{version}}", + "view_pipette_setting": "移液器设置", + "view_run_record": "查看协议运行记录", + "view": "查看", + "waste_chute": "外置垃圾槽", + "welcome_modal_description": "运行协议、管理设备及查看工作站状态的地方。", + "welcome_to_your_dashboard": "欢迎来到您的控制面板!", + "yes_update_now": "是的,现在更新" +} diff --git a/app/src/assets/localization/zh/device_settings.json b/app/src/assets/localization/zh/device_settings.json new file mode 100644 index 00000000000..c69e9e46131 --- /dev/null +++ b/app/src/assets/localization/zh/device_settings.json @@ -0,0 +1,324 @@ +{ + "about_advanced": "关于", + "about_calibration_description": "为了让工作站精确移动,您需要对其进行校准。位置校准分为三部分:甲板校准、移液器偏移校准和吸头长度校准。", + "about_calibration_description_ot3": "为了让工作站精确移动,您需要对其进行校准。移液器和转板抓手校准是一个自动化过程,使用校准探头或销钉。校准完成后,您可以将校准数据以JSON文件的形式保存到计算机中。", + "about_calibration_title": "关于校准", + "advanced": "高级", + "alpha_description": "警告:alpha版本功能完整,但可能包含重大错误。", + "alternative_security_types": "可选的安全类型", + "app_change_in": "应用程序在{{version}}中的更改", + "apply_historic_offsets": "应用耗材偏移校准数据", + "are_you_sure_you_want_to_disconnect": "您确定要断开与{{ssid}}的连接吗?", + "attach_a_pipette_before_calibrating": "在执行校准之前,请安装移液器", + "boot_scripts": "启动脚本", + "both": "两者", + "browse_file_system": "浏览文件系统", + "bug_fixes": "错误修复", + "calibrate_deck": "校准甲板", + "calibrate_deck_description": "适用于没有在甲板上蚀刻十字的2019年之前的工作站。", + "calibrate_deck_to_dots": "根据校准点校准甲板", + "calibrate_gripper": "校准转板抓手", + "calibrate_module": "校准模块", + "calibrate_now": "立即校准", + "calibrate_pipette": "校准移液器偏移", + "calibration": "校准", + "calibration_health_check_description": "检查关键校准点的精度,无需重新校准工作站。", + "calibration_health_check_title": "校准运行状况检查", + "change_network": "更改网络", + "characters_max": "最多17个字符", + "check_for_updates": "检查更新", + "checking_for_updates": "正在检查更新", + "choose": "选择...", + "choose_file": "选择文件", + "choose_network_type": "选择网络类型", + "choose_reset_settings": "选择重置设置", + "clear_all_data": "清除所有数据", + "clear_all_stored_data": "清除所有存储的数据", + "clear_all_stored_data_description": "清除校准、协议和所有设置,保留工作站名称和网络设置。", + "clear_calibration_data": "清除校准数据", + "clear_data_and_restart_robot": "清除数据并重新启动工作站", + "clear_individual_data": "清除单个数据", + "clear_option_authorized_keys": "清除SSH公钥", + "clear_option_boot_scripts": "清除自定义启动脚本", + "clear_option_boot_scripts_description": "清除修改工作站开机行为的脚本", + "clear_option_deck_calibration": "清除甲板校准", + "clear_option_gripper_calibration": "清除转板抓手校准", + "clear_option_gripper_offset_calibrations": "清除转板抓手校准", + "clear_option_module_calibration": "清除模块校准", + "clear_option_pipette_calibrations": "清除移液器校准", + "clear_option_pipette_offset_calibrations": "清除移液器偏移校准", + "clear_option_runs_history": "清除协议运行历史", + "clear_option_runs_history_subtext": "清除所有协议的过往运行信息。点击并应用", + "clear_option_tip_length_calibrations": "清除吸头长度校准", + "cancel_software_update": "取消软件更新", + "complete_and_restart_robot": "完成并重新启动工作站", + "confirm_device_reset_description": "这将永久删除所有协议、校准和其他数据。您需要重新进行初始设置才能再次使用工作站。", + "confirm_device_reset_heading": "您确定要重置您的设备吗?", + "connect": "连接", + "connect_the_estop_to_continue": "连接紧急停止按钮以继续", + "connect_to_wifi_network": "连接到Wi-Fi网络", + "connect_via": "通过{{type}}连接", + "connect_via_usb_description_1": "1. 将 USB A-to-B 连接线连接到工作站的 USB-B 端口。", + "connect_via_usb_description_2": "2. 将电缆连接到计算机上的一个空闲USB端口。", + "connected": "已连接", + "connected_network": "已连接网络", + "connected_to_ssid": "已连接到{{ssid}}", + "connected_via": "通过{{networkInterface}}连接", + "connecting_to": "正在连接到{{ssid}}...", + "connection_description_ethernet": "连接到您实验室的有线网络。", + "connection_description_wifi": "在您的实验室中找到一个网络,或者输入您自己的网络。", + "connection_to_robot_lost": "与工作站的连接中断", + "deck_calibration_description": "新工作站或搬迁工作站后需要校准甲板。重新校准甲板后也将需要重新校准移液器偏移。", + "deck_calibration_missing": "缺少甲板校准", + "deck_calibration_missing_no_pipette": "缺少甲板校准。安装移液器以执行甲板校准。", + "deck_calibration_modal_description": "若同时需要校准甲板及校准移液器偏移,不建议在校准甲板之前校准移液器偏移,因为校准甲板会清除所有其他校准数据。", + "deck_calibration_modal_pipette_description": "您想要继续进行移液器偏移校准吗?", + "deck_calibration_modal_title": "您确定要进行校准吗?", + "deck_calibration_recommended": "建议进行甲板校准", + "deck_calibration_title": "甲板校准", + "dev_tools_description": "访问额外的日志记录和功能标志。", + "device_reset": "设备重置", + "device_reset_description": "将耗材校准、启动脚本和/或工作站校准重置为出厂设置。", + "device_reset_slideout_description": "选择单独的设置以仅清除特定的数据类型。", + "device_resets_cannot_be_undone": "重置无法撤销", + "release_notes": "发行说明", + "directly_connected_to_this_computer": "直接连接到这台计算机。", + "disconnect": "断开连接", + "disconnect_from_ssid": "断开与{{ssid}}的连接", + "disconnect_from_wifi": "断开Wi-Fi连接", + "disconnect_from_wifi_network_failure": "您的工作站无法断开与Wi-Fi网络{{ssid}}的连接。", + "disconnect_from_wifi_network_success": "您的工作站已成功断开与Wi-Fi网络的连接。", + "disconnected_from_wifi": "已断开Wi-Fi连接", + "disconnecting_from_wifi_network": "正在断开与Wi-Fi网络{{ssid}}的连接", + "disengaged": "已解除", + "display_brightness": "屏幕亮度", + "display_led_lights": "状态LED灯", + "display_led_lights_description": "控制工作站前部的指示灯条。", + "display_sleep_settings": "屏幕睡眠设置", + "do_not_turn_off": "这可能需要最多{{minutes}}分钟。请不要关闭工作站。", + "done": "完成", + "download": "下载", + "download_calibration_data": "下载校准日志", + "download_error": "下载错误日志", + "download_logs": "下载日志", + "downloading_logs": "正在下载日志...", + "downloading_software": "正在下载软件...", + "downloading_update": "正在下载更新...", + "e_stop_connected": "紧急停止按钮成功连接", + "e_stop_not_connected": "将紧急停止按钮连接到工作站背面的端口。", + "enable_status_light": "启用状态灯", + "enable_status_light_description": "打开或关闭工作站前部的指示LED灯条。", + "engaged": "已连接", + "enter_factory_password": "输入工厂密码", + "enter_network_name": "输入网络名称", + "enter_password": "输入密码", + "estop": "紧急停止按钮", + "estop_disengaged": "紧急停止按钮已解除", + "estop_engaged": "紧急停止按钮已连接", + "estop_missing": "缺少紧急停止按钮", + "estop_missing_description": "您的紧急停止按钮可能已损坏或脱落。{{robotName}}与紧急停止按钮失去连接,因此取消了协议。连接一个功能正常的紧急停止按钮以继续。", + "estop_pressed": "紧急停止按钮被按下", + "ethernet": "以太网", + "ethernet_connection_description": "将以太网线从工作站背面接口连接到网络交换机或集线器。", + "exit": "退出", + "factory_mode": "工厂模式", + "factory_reset": "工厂重置", + "factory_reset_description": "重置所有设置。在再次使用工作站之前,您必须重新进行初始设置。", + "factory_reset_modal_description": "这些数据以后无法检索。", + "factory_resets_cannot_be_undone": "工厂重置无法撤销。", + "failed_to_connect_to_ssid": "无法连接到{{ssid}}", + "feature_flags": "功能标志", + "finish_setup": "完成设置", + "firmware_version": "固件版本", + "fully_calibrate_before_checking_health": "在检查校准健康之前,请完全校准您的工作站", + "gantry_homing": "重启时归位龙门架", + "gantry_homing_description": "沿Z轴归位龙门架。", + "go_to_advanced_settings": "转到高级应用设置", + "gripper_calibration_description": "使用金属销来确定转板抓手相对于甲板槽上的精密切割方格的确切位置。", + "gripper_calibration_title": "转板抓手校准", + "gripper_serial": "转板抓手序列号", + "health_check": "检查健康状态", + "hide": "隐藏", + "historic_offsets_description": "在设置协议时使用存储的数据。", + "incorrect_password_for_ssid": "哎呀!{{ssid}}的密码不正确", + "install_e_stop": "安装紧急停止按钮", + "installing_software": "正在安装软件...", + "installing_update": "正在安装更新...", + "invalid_password": "无效密码", + "ip_address": "IP地址", + "join_other_network": "加入其他网络", + "join_other_network_error_message": "长度必须为2-32个字符", + "jupyter_notebook": "Jupyter Notebook", + "jupyter_notebook_description": "在网页浏览器中打开运行在此工作站上的Jupyter Notebook。这是一个实验性功能。", + "jupyter_notebook_link": "了解更多关于使用Jupyter Notebook的信息", + "last_calibrated": "最后校准时间:{{date}}", + "last_calibrated_label": "最后校准", + "launch_jupyter_notebook": "启动Jupyter Notebook", + "legacy_settings": "遗留设置", + "mac_address": "MAC地址", + "manage_oem_settings": "管理OEM设置", + "minutes": "{{minute}}分钟", + "missing_calibration": "缺少校准", + "model_and_serial": "移液器型号和序列号", + "module": "模块", + "module_calibration": "模块校准", + "module_calibration_description": "模块校准使用移液器和连接的探头来确定模块相对于甲板的确切位置。", + "mount": "安装", + "name_love_it": "{{name}},喜欢它!", + "name_rule_description": "输入最多17个字符(仅限字母和数字)", + "name_rule_error_exist": "哎呀!名称已被使用。选择一个不同的名称。", + "name_rule_error_name_length": "哎呀!工作站名称必须遵循字符计数和限制。", + "name_rule_error_too_short": "哎呀!太短了。工作站名称至少需要1个字符。", + "name_your_robot": "给您的工作站起个名字", + "name_your_robot_description": "别担心,您可以在设置中随时更改这个名称。", + "need_another_security_type": "需要另一种安全类型吗?", + "network_name": "网络名称", + "network_settings": "网络设置", + "networking": "网络连接", + "never": "从不", + "new_features": "新功能", + "next_step": "下一步", + "no_connection_found": "未找到连接", + "no_gripper_attached": "未连接转板抓手", + "no_modules_attached": "未连接模块", + "no_network_found": "未找到网络", + "no_pipette_attached": "未连接移液器", + "none_description": "不推荐", + "not_calibrated": "尚未校准", + "not_calibrated_short": "未校准", + "not_connected": "未连接", + "not_connected_via_ethernet": "未通过以太网连接", + "not_connected_via_usb": "未通过USB连接", + "not_connected_via_wifi": "未通过Wi-Fi连接", + "not_connected_via_wired_usb": "未通过有线USB连接", + "not_now": "不是现在", + "oem_mode": "OEM模式", + "off": "关闭", + "one_hour": "1小时", + "on": "开启", + "other_networks": "其他网络", + "password": "密码", + "password_error_message": "至少需要8个字符", + "pause_protocol": "当工作站前门打开时暂停协议", + "pause_protocol_description": "启用后,在运行过程中打开工作站前门,工作站会在完成当前动作后暂停。", + "pipette_calibrations_description": "使用校准探头来确定移液器相对于甲板槽上的精密切割方格的确切位置。", + "pipette_calibrations_title": "移液器校准", + "pipette_offset_calibration": "移液器偏移校准", + "pipette_offset_calibration_missing": "缺少移液器偏移校准", + "pipette_offset_calibration_recommended": "建议进行移液器偏移校准", + "pipette_offset_calibrations_history": "查看所有移液器偏移校准历史", + "pipette_offset_calibrations_title": "移液器偏移校准", + "privacy": "隐私", + "problem_during_update": "此次更新耗时比平常要长。", + "proceed_without_updating": "跳过更新以继续", + "protocol_run_history": "协议运行历史", + "recalibrate_deck": "重新校准甲板", + "recalibrate_gripper": "重新校准转板抓手", + "recalibrate_module": "重新校准模块", + "recalibrate_now": "立即重新校准", + "recalibrate_pipette": "重新校准移液器偏移数据", + "recalibrate_tip_and_pipette": "重新校准吸头长度和移液器偏移量", + "recalibration_recommended": "建议重新校准", + "reinstall": "重新安装", + "remind_me_later": "稍后提醒我", + "rename_robot": "重命名工作站", + "rename_robot_input_error": "哎呀!工作站名称必须遵循字符计数和限制。", + "rename_robot_input_limitation_detail": "请输入最多17个字符:字母和数字。", + "rename_robot_prefer_usb_connection": "为确保工作站名称更改的可靠性,请通过USB连接。", + "rename_robot_title": "重命名工作站", + "requires_restarting_the_robot": "更新工作站软件需要重启工作站", + "reset_to_factory_settings": "重置为出厂设置?", + "resets_cannot_be_undone": "重置操作无法撤销", + "restart_now": "现在重启?", + "restart_robot_confirmation_description": "重启{{robotName}}将需要几分钟时间。", + "restart_taking_too_long": "{{robotName}}重启所需时间超出预期。请检查其设置页面中的“高级”选项卡,以确认是否已成功更新。如果工作站无响应,请手动重启。", + "restarting_robot": "安装完成,工作站正在重启...", + "resume_robot_operations": "恢复工作站操作", + "returns_your_device_to_new_state": "这将使您的设备恢复到新的状态。", + "robot_busy_protocol": "当协议正在运行时,此工作站无法更新", + "robot_calibration_data": "工作站校准数据", + "robot_initializing": "正在初始化工作站...", + "robot_name": "工作站名称", + "robot_operating_update_available": "工作站操作系统更新可用", + "robot_serial_number": "工作站序列号", + "robot_server_version": "工作站服务器版本", + "robot_settings": "工作站设置", + "robot_settings_advanced_unknown": "未知", + "robot_successfully_connected": "工作站已成功连接到{{networkName}}。", + "robot_system_version": "工作站系统版本", + "robot_system_version_available": "工作站系统版本{{releaseVersion}}可用", + "robot_up_to_date": "工作站已更新至最新版本", + "robot_up_to_date_description": "您的工作站似乎已经是最新版本,但如果您遇到问题,可以重新应用最新更新。", + "robot_update_available": "工作站更新可用", + "robot_update_success": "工作站软件已成功更新", + "search_again": "再次搜索", + "searching": "搜索中", + "searching_for_networks": "正在搜索网络...", + "security_type": "安全类型", + "select_a_network": "选择一个网络", + "select_a_security_type": "选择一个安全类型", + "select_all_settings": "选择所有设置", + "select_authentication_method": "为您所选的网络选择身份验证方法。", + "sending_software": "正在发送软件...", + "serial": "序列号", + "setup_mode": "设置模式", + "short_trash_bin": "短垃圾桶", + "short_trash_bin_description": "适用于2019年之前带有55mm高垃圾桶的工作站(而非默认的77mm)", + "show": "显示", + "show_password": "显示密码", + "sign_into_wifi": "登录Wi-Fi", + "software_is_up_to_date": "您的软件已经是最新版本!", + "software_update_error": "软件更新错误", + "some_robot_controls_are_not_available": "运行过程中无法使用工作站的控制功能", + "ssh_public_keys": "SSH公钥", + "subnet_mask": "子网掩码", + "successfully_connected": "成功连接!", + "successfully_connected_to_network": "已成功连接到{{ssid}}!", + "supported_protocol_api_versions": "支持的协议API版本", + "text_size": "文本大小", + "text_size_description": "所有屏幕上的文本都会根据您在下方选择的大小进行调整。", + "tip_length_calibrations_history": "查看所有吸头长度校准历史", + "tip_length_calibrations_title": "吸头长度校准", + "tiprack": "吸头架", + "touchscreen_brightness": "触摸屏亮度", + "touchscreen_sleep": "触摸屏休眠", + "troubleshooting": "故障排查", + "try_again": "再试一次", + "try_restarting_the_update": "尝试重新启动更新。", + "up_to_date": "最新的", + "update_available": "有可用更新", + "update_channel_description": "稳定版接收最新的稳定版发布。Beta 版允许您在新功能在稳定版发布前先行试用,但这些新功能尚未完成测试。", + "update_complete": "更新完成!", + "update_found": "发现更新!", + "update_robot_now": "现在更新工作站", + "update_robot_software": "使用本地文件(.zip)手动更新工作站软件", + "updating": "正在更新", + "update_requires_restarting_robot": "更新工作站软件需要重启工作站", + "upload_custom_logo_description": "上传一个Logo,用于工作站启动时显示。", + "upload_custom_logo_dimensions": "Logo必须符合 1024 x 600 的尺寸,且是 PNG 文件(.png)。", + "upload_custom_logo": "上传自定义Logo", + "usage_settings": "使用设置", + "usb": "USB", + "usb_to_ethernet_description": "正在查找 USB-to-Ethernet 适配器信息?", + "use_older_aspirate": "使用旧版吸液动作", + "use_older_aspirate_description": "使用在 3.7.0 版本之前使用的较不准确的体积校准进行吸取。如果需要与 3.7.0 版本之前的结果保持一致,请使用此设置。这仅影响 GEN1 P10S、P10M、P50M 和 P300S 移液器。", + "validating_software": "正在验证软件...", + "view_details": "查看详细信息", + "view_network_details": "查看网络详细信息", + "view_update": "查看更新", + "welcome_description": "在实验室台面上快速运行协议并检查工作站状态。", + "wifi": "Wi-Fi", + "wired_ip": "有线IP", + "wired_mac_address": "有线MAC地址", + "wired_subnet_mask": "有线子网掩码", + "wired_usb": "有线USB", + "wired_usb_description": "了解如何通过USB连接到工作站", + "wireless_ip": "无线IP", + "wireless_mac_address": "无线MAC地址", + "wireless_subnet_mask": "无线子网掩码", + "wpa2_personal": "WPA2个人", + "wpa2_personal_description": "大多数实验室都使用此方法", + "yes_clear_data_and_restart_robot": "是,清除数据并重新启动工作站", + "your_mac_address_is": "您的MAC地址是{{macAddress}}", + "your_robot_is_ready_to_go": "您的工作站已准备就绪。" +} diff --git a/app/src/assets/localization/zh/devices_landing.json b/app/src/assets/localization/zh/devices_landing.json new file mode 100644 index 00000000000..587959751e9 --- /dev/null +++ b/app/src/assets/localization/zh/devices_landing.json @@ -0,0 +1,46 @@ +{ + "active": "激活", + "available": "可用({{count}})", + "check_same_network": "检查电脑和工作站是否连接在同一网络上", + "connect_to_network": "连接到网络", + "connection_troubleshooting_intro": "如果工作站的连接出现问题,请尝试执行以下故障排除任务。首先,仔细检查工作站电源是否打开", + "deck_configuration": "甲板配置", + "devices": "设备", + "disconnect_from_network": "断开网络连接", + "empty": "空", + "forget_unavailable_robot": "忘记无法使用的工作站", + "go_to_run": "去运行", + "home_gantry": "龙门架归位", + "how_to_setup_a_robot": "如何设置新工作站", + "idle": "空闲", + "if_connecting_via_usb": "如果通过USB连接", + "if_connecting_wirelessly": "如果通过无线方式连接", + "if_still_having_issues": "如果您仍然有问题", + "learn_more_about_new_robot_setup": "了解有关设置新工作站的更多信息", + "learn_more_about_troubleshooting_connection": "了解有关连接问题故障排除的更多信息", + "left_mount": "左侧安装支架", + "lights_off": "关闭灯光", + "lights_on": "开启灯光", + "loading": "加载中", + "looking_for_robots": "寻找工作站", + "ninety_six_mount": "左侧+右侧安装支架", + "make_sure_robot_is_connected": "确保工作站已连接到此计算机", + "modules": "模块", + "no_robots_found": "未找到工作站", + "not_available": "不可用({{count}})", + "refresh": "刷新", + "restart_the_app": "重启应用程序", + "restart_the_robot": "重启工作站", + "right_mount": "右侧安装支架", + "robot_settings": "工作站设置", + "run_a_protocol": "运行一个协议", + "running": "运行中", + "see_how_to_setup_new_robot": "查看如何设置新工作站", + "setting_up_new_robot": "了解如何设置新工作站", + "this_robot_has_connected_and_power_on_module": "工作站已连接并开启{{moduleName}}", + "troubleshooting_connection_problems": "了解有关连接问题故障排除的更多信息", + "update_robot_software": "更新工作站软件", + "use_usb_cable_for_new_robot": "设置新工作站时,请使用附带的USB线进行首次连接。", + "wait_after_connecting": "将工作站连接到电脑后等待一会", + "why_is_this_robot_unavailable": "为什么这个工作站不可用?" +} diff --git a/app/src/assets/localization/zh/drop_tip_wizard.json b/app/src/assets/localization/zh/drop_tip_wizard.json new file mode 100644 index 00000000000..8984387755e --- /dev/null +++ b/app/src/assets/localization/zh/drop_tip_wizard.json @@ -0,0 +1,39 @@ +{ + "before_you_begin_do_you_want_to_blowout": "开始前,您是否需要保留已吸取的液体?", + "begin_removal": "开始移除", + "blowout_complete": "吹液完成", + "blowout_liquid": "吹出液体", + "cant_safely_drop_tips": "无法安全丢弃吸头", + "choose_blowout_location": "选择吹液位置", + "choose_drop_tip_location": "选择吸头丢弃位置", + "confirm_blowout_location": "移液器是否位于应吹出液体的位置?", + "confirm_drop_tip_location": "移液器是否位于应丢弃吸头的位置?", + "confirm_removal_and_home": "确认移除并回到原点", + "drop_tip_complete": "吸头丢弃完成", + "drop_tip_failed": "丢弃吸头操作未能完成,请联系技术支持获取帮助。", + "drop_tips": "丢弃吸头", + "error_dropping_tips": "丢弃吸头时发生错误", + "exit_screen_title": "在完成吸头丢弃前退出?", + "getting_ready": "正在准备…", + "go_back": "返回", + "move_to_slot": "移至板位", + "no_proceed_to_drop_tip": "否,继续进行吸头移除", + "position_and_blowout": "确保移液器吸头尖端位于指定位置的正上方并保持水平。如果不是,请使用下面的控制键或键盘微调移液器直到正确位置。", + "position_and_drop_tip": "确保移液器吸头尖端位于指定位置的正上方并保持水平。如果不是,请使用下面的控制键或键盘微调移液器直到正确位置。", + "position_the_pipette": "调整移液器位置", + "remove_the_tips": "在协议中再次使用前,您可能需要从{{mount}}移液器上移除吸头。", + "remove_the_tips_from_pipette": "在协议中再次使用前,您可能需要从移液器上移除吸头。", + "remove_the_tips_manually": "手动移除吸头,然后使龙门架回原点。在拾取吸头的状态下归位可能导致移液器吸入液体并损坏。", + "remove_tips": "移除吸头", + "select_blowout_slot": "您可以将液体吹入实验容器中。在右侧的甲板图上选择您想要吹出液体的板位。确认后龙门架将移动到选定的板位。", + "select_blowout_slot_odd": "您可以将液体吹入耗材中。
龙门架移动到选定的板位后,使用位置控制按键将移液器移动到吹出液体的确切位置。", + "select_drop_tip_slot": "您可以将吸头返回吸头架或丢弃它们。在右侧的甲板图上选择您想丢弃吸头的板位。确认后龙门架将移动到选定的板位。", + "select_drop_tip_slot_odd": "您可以将吸头放回吸头架或丢弃它们。
龙门架移动到选定的板位后,使用位置控制按键将移液器移动到丢弃吸头的确切位置。", + "skip": "跳过", + "stand_back_blowing_out": "请远离,工作站正在吹出液体", + "stand_back_dropping_tips": "请远离,工作站正在丢弃吸头", + "stand_back_robot_in_motion": "请远离,工作站正在移动", + "tips_are_attached": "吸头已拾取", + "tips_may_be_attached": "可能已拾取吸头。", + "yes_blow_out_liquid": "是的,将液体吹入耗材中" +} diff --git a/app/src/assets/localization/zh/error_recovery.json b/app/src/assets/localization/zh/error_recovery.json new file mode 100644 index 00000000000..2383ca8f850 --- /dev/null +++ b/app/src/assets/localization/zh/error_recovery.json @@ -0,0 +1,73 @@ +{ + "are_you_sure_you_want_to_cancel": "您确定要取消吗?", + "at_step": "在步骤", + "back_to_menu": "返回菜单", + "before_you_begin": "开始前", + "begin_removal": "开始移除", + "blowout_failed": "吹出液体失败", + "overpressure_is_usually_caused": "探测器感应到压力过大通常是由吸头碰撞到实验用品、吸头堵塞或吸取/排出粘稠液体速度过快引起。如果问题持续存在,请取消运行并对协议进行必要的修改。", + "cancel_run": "取消运行", + "canceling_run": "正在取消运行", + "change_location": "更改位置", + "change_tip_pickup_location": "更换拾取吸头的位置", + "choose_a_recovery_action": "选择恢复操作", + "confirm": "确认", + "continue": "继续", + "continue_run_now": "现在继续运行", + "continue_to_drop_tip": "继续丢弃吸头", + "error": "错误", + "failed_dispense_step_not_completed": "中断运行的最后一步液体排出失败,恢复程序将不会继续运行这一步骤,请手动完成这一步的移液操作。运行将继续从下一步开始。继续之前,请关闭工作站门。", + "failed_step": "失败步骤", + "go_back": "返回", + "if_tips_are_attached": "如果吸头还在移液器上,您可以在运行终止前选择吹出已吸取的液体并丢弃吸头。", + "ignore_all_errors_of_this_type": "忽略所有此类错误", + "ignore_error_and_skip": "忽略错误并跳到下一步", + "skipping_to_step_succeeded": "跳转到步骤{{step}}成功", + "retrying_step_succeeded": "重试步骤{{step}}成功", + "ignore_only_this_error": "仅忽略此错误", + "ignore_similar_errors_later_in_run": "要在后续的运行中忽略类似错误吗?", + "launch_recovery_mode": "启动恢复模式", + "manually_fill_liquid_in_well": "手动填充孔位{{well}}中的液体", + "manually_fill_well_and_skip": "手动填充孔位并跳到下一步", + "next_step": "下一步", + "no_liquid_detected": "未检测到液体", + "pick_up_tips": "取吸头", + "pipette_overpressure": "移液器超压", + "preserve_aspirated_liquid": "首先,您需要保留已吸取的液体吗?", + "proceed_to_cancel": "继续取消", + "proceed_to_tip_selection": "继续选择吸头", + "recovery_action_failed": "{{action}}失败", + "recovery_mode": "恢复模式", + "recovery_mode_explanation": "恢复模式为您提供运行错误后的手动处理引导。
您可以进行调整以确保发生错误时正在进行的步骤可以完成,或者选择取消协议。当做出调整且未检测到后续错误时,该模式操作完成。根据导致错误的条件,系统将提供相应的调整选项。", + "replace_tips_and_select_location": "建议更换吸头并选择最后一次取吸头的位置。", + "replace_used_tips_in_rack_location": "在吸头板位{{location}}更换已使用的吸头", + "replace_with_new_tip_rack": "更换新的吸头盒", + "first_take_any_necessary_actions": "首先,采取必要的准备操作,工作站将从失败的步骤开始继续运行。然后,在工作站继续运行之前关闭前门。", + "retry_now": "现在重试", + "retry_step": "重试步骤", + "retry_with_new_tips": "使用新吸头重试", + "retry_with_same_tips": "使用相同吸头重试", + "return_to_menu": "返回菜单", + "return_to_the_menu": "返回菜单以选择如何继续。", + "robot_will_not_check_for_liquid": "工作站将不再检查液体。运行将从下一步继续。继续前请关闭工作站前门。", + "robot_will_retry_with_new_tips": "工作站将使用新吸头重试失败的步骤。继续前请关闭工作站前门。", + "robot_will_retry_with_same_tips": "工作站将使用相同的吸头重试失败的步骤。继续前请关闭工作站前门。", + "robot_will_retry_with_tips": "工作站将使用新吸头重试失败的步骤。", + "run_paused": "运行暂停", + "select_tip_pickup_location": "选择取吸头位置", + "skip": "跳过", + "skip_to_next_step": "跳到下一步", + "skip_to_next_step_new_tips": "使用新吸头跳到下一步", + "skip_to_next_step_same_tips": "使用相同吸头跳到下一步", + "stand_back": "请远离,工作站正在运行", + "stand_back_picking_up_tips": "请远离,正在拾取吸头", + "stand_back_resuming": "请远离,正在恢复当前步骤", + "stand_back_retrying": "请远离,正在重试失败步骤", + "stand_back_skipping_to_next_step": "请远离,正在跳到下一步骤", + "tip_drop_failed": "丢弃吸头失败", + "tip_not_detected": "未检测到吸头", + "view_error_details": "查看错误详情", + "view_recovery_options": "查看恢复选项", + "you_can_still_drop_tips": "在继续选择吸头之前,您仍然可以丢弃移液器上现存的吸头。", + "you_may_want_to_remove": "在协议中再次使用之前,您可能需要从{{mount}}移液器上移除吸头。" +} diff --git a/app/src/assets/localization/zh/firmware_update.json b/app/src/assets/localization/zh/firmware_update.json new file mode 100644 index 00000000000..efed676a952 --- /dev/null +++ b/app/src/assets/localization/zh/firmware_update.json @@ -0,0 +1,17 @@ +{ + "firmware_out_of_date": "用于{{mount}}{{instrument}}的固件已过期。在运行使用此设备的协议之前,请更新该固件。", + "gantry_x": "龙门架X轴", + "gantry_y": "龙门架Y轴", + "gripper": "转板抓手", + "head": "头部组件", + "hepa_uv": "HEPA/UV模块", + "pipette_left": "左侧移液器", + "pipette_right": "右侧移液器", + "ready_to_use": "您的{{instrument}}已经准备就绪!", + "rear_panel": "后置面板", + "successful_update": "更新成功!", + "update_failed": "更新失败", + "update_firmware": "更新固件", + "update_needed": "需要更新设备固件", + "updating_firmware": "正在更新{{subsystem}}固件..." +} diff --git a/app/src/assets/localization/zh/gripper_wizard_flows.json b/app/src/assets/localization/zh/gripper_wizard_flows.json new file mode 100644 index 00000000000..6617e15d83a --- /dev/null +++ b/app/src/assets/localization/zh/gripper_wizard_flows.json @@ -0,0 +1,39 @@ +{ + "are_you_sure_exit": "在完成{{flow}}之前,您确定要退出吗?", + "attach_gripper": "安装转板抓手", + "attached_gripper_and_screw_in": "将转板抓手对准接口并压紧固定抓手,以确保安全连接。首先拧紧上方螺丝,接着拧紧下方螺丝。然后轻轻拉动,测试抓手是否稳固安装。", + "before_you_begin": "开始之前", + "begin_calibration": "开始校准", + "calibrate_gripper": "校准转板抓手", + "calibration_pin": "校准钉", + "calibration_pin_touching": "校准钉将触碰甲板{{slot}}中的校准块,以确定其精确位置。", + "complete_calibration": "完成校准", + "continue": "继续", + "continue_calibration": "继续校准", + "detach_gripper": "卸下转板抓手", + "firmware_updating": "需要固件更新,设备正在更新中...", + "firmware_up_to_date": "固件已为最新版本。", + "get_started": "开始操作", + "gripper_calibration": "转板抓手校准", + "gripper_recalibration": "转板抓手重新校准", + "gripper_successfully_attached": "转板抓手已成功安装", + "hex_screwdriver": "2.5mm 六角螺丝刀", + "hold_gripper_and_loosen_screws": "用手扶住转板抓手,首先松开上方螺丝,再松开下方螺丝。(螺丝固定于抓手上,不会脱落。)之后小心卸下抓手。", + "insert_pin_into_front_jaw": "将校准钉插入前夹爪", + "insert_pin_into_rear_jaw": "将校准钉插入后夹爪", + "move_gantry_to_front": "将龙门架移至前端", + "move_pin_from_front_to_rear_jaw": "从前夹爪上取下校准钉并装到后夹爪上。", + "move_pin_from_rear_jaw_to_storage": "从后夹爪上取下校准钉并放回其存放位置。", + "move_pin_from_storage_to_front_jaw": "从存放位置取下校准钉,利用磁性将其放到前夹爪下方的孔洞中。", + "progress_will_be_lost": "{{flow}}的进度将丢失", + "provided_with_robot_use_right_size": "随工作站提供。使用非标准尺寸可能导致仪器螺丝损坏。", + "remove_calibration_pin": "卸下校准钉", + "remove_labware_to_get_started_attaching": "开始前,请清空甲板,移除所有实验器材,以便于安装和校准。同时根据屏幕提示,准备所需工具。校准钉随转板抓手附带,应存放于转板抓手右侧上方。", + "remove_labware_to_get_started_detaching": "开始前,请移除甲板上的所有耗材,并清理工作区,以便于拆卸操作。同时根据屏幕提示,准备相关工具。", + "remove_labware_to_get_started_recalibrating": "开始前,请移除甲板上的所有耗材,并清理工作区,以便于校准。同时根据屏幕提示,准备相关工具。校准钉随转板抓手附带,应存放在转板抓手右侧上方。", + "remove_probe": "解锁校准探头,从移液器喷嘴卸下,并归位存放。", + "return_pin_error": "退出前,请务必将校准钉放回存放位置。 {{error}}", + "stand_back_gripper_is_calibrating": "请远离,转板抓手正在进行校准", + "try_again": "重试", + "unable_to_detect_gripper": "未能检测到转板抓手" +} diff --git a/app/src/assets/localization/zh/heater_shaker.json b/app/src/assets/localization/zh/heater_shaker.json new file mode 100644 index 00000000000..249f1cc6138 --- /dev/null +++ b/app/src/assets/localization/zh/heater_shaker.json @@ -0,0 +1,48 @@ +{ + "back": "返回", + "cannot_open_latch": "模块震荡混匀期间闩锁不能打开", + "cannot_shake": "模块闩锁打开时无法执行震荡混匀命令", + "close_labware_latch": "关闭耗材闩锁", + "close_latch": "关闭闩锁", + "closed_and_locked": "关闭并锁定", + "closed": "已关闭", + "closing": "正在关闭", + "complete": "完成", + "confirm_attachment": "确认已连接", + "confirm_heater_shaker_modal_attachment": "确认热震荡模块已连接", + "continue_shaking_protocol_start_prompt": "协议启动时是否继续执行震荡混匀命令?", + "deactivate_heater": "停止加热", + "deactivate_shaker": "停止震荡混匀", + "deactivate": "停用", + "heater_shaker_in_slot": "在继续前,请在板位{{slotName}}中安装{{moduleName}}", + "heater_shaker_is_shaking": "热震荡模块当前正在震荡混匀", + "keep_shaking_start_run": "继续震荡混匀并开始运行", + "labware_latch": "耗材闩锁", + "labware": "耗材", + "min_max_rpm": "{{min}}-{{max}}rpm", + "module_anchors_extended": "运行开始前,应将模块下方的固定位的漏丝拧紧,确保模块稳固跟甲板结合", + "module_in_slot": "{{moduleName}}位于板位{{slotName}}槽中", + "module_should_have_anchors": "模块应将两个固定锚完全伸出,以确保稳固地连接到甲板上", + "open_labware_latch": "打开耗材闩锁", + "open_latch": "打开闩锁", + "open": "打开", + "opening": "正在打开", + "proceed_to_run": "继续运行", + "set_shake_speed": "设置震荡混匀速度", + "set_temperature": "设置模块温度", + "shake_speed": "震荡混匀速度", + "show_attachment_instructions": "显示固定操作安装说明", + "stop_shaking_start_run": "停止震荡混匀并开始运行", + "stop_shaking": "停止震荡混匀", + "t10_torx_screwdriver": "{{name}}型螺丝刀", + "t10_torx_screwdriver_subtitle": "热震荡模块附带。使用其他尺寸可能会损坏模块螺丝", + "test_shake_banner_information": "测试震荡混匀功能前,如果想往热震荡模块转移耗材,需要在控制页面控制模块开启闩锁", + "test_shake_banner_labware_information": "测试震荡混匀功能前,如果想往热震荡模块转移{{labware}},需要控制模块闩锁", + "test_shake_slideout_banner_info": "测试震荡混匀功能前,如果想往热震荡模块转移耗材,需要在控制页面控制模块开启闩锁", + "test_shake_troubleshooting_slideout_description": "请重新查看模块固定安装及其适配器安装相关说明", + "test_shake": "测试震荡混匀", + "thermal_adapter_attached_to_module": "适配器应已连接到模块上", + "troubleshoot_step_1": "返回步骤1查看模块固定安装相关说明", + "troubleshoot_step_3": "返回步骤3查看模块适配器安装相关说明", + "troubleshooting": "故障排除" +} diff --git a/app/src/assets/localization/zh/incompatible_modules.json b/app/src/assets/localization/zh/incompatible_modules.json new file mode 100644 index 00000000000..7e6da2f253e --- /dev/null +++ b/app/src/assets/localization/zh/incompatible_modules.json @@ -0,0 +1,7 @@ +{ + "incompatible_modules_attached": "检测到不适用模块,", + "remove_before_running_protocol": "运行协议前请移除以下硬件:", + "needs_your_assistance": "{{robot_name}}需要您的协助", + "remove_before_using": "使用此工作站前,请移除不适用模块.", + "is_not_compatible": "{{module_name}}不适用于{{robot_type}}," +} diff --git a/app/src/assets/localization/zh/index.ts b/app/src/assets/localization/zh/index.ts new file mode 100644 index 00000000000..5cb1ab2d10b --- /dev/null +++ b/app/src/assets/localization/zh/index.ts @@ -0,0 +1,63 @@ +import shared from './shared.json' +import anonymous from './anonymous.json' +import app_settings from './app_settings.json' +import branded from './branded.json' +import change_pipette from './change_pipette.json' +import protocol_command_text from './protocol_command_text.json' +import device_details from './device_details.json' +import device_settings from './device_settings.json' +import devices_landing from './devices_landing.json' +import drop_tip_wizard from './drop_tip_wizard.json' +import firmware_update from './firmware_update.json' +import gripper_wizard_flows from './gripper_wizard_flows.json' +import heater_shaker from './heater_shaker.json' +import instruments_dashboard from './instruments_dashboard.json' +import labware_details from './labware_details.json' +import labware_landing from './labware_landing.json' +import labware_position_check from './labware_position_check.json' +import module_wizard_flows from './module_wizard_flows.json' +import pipette_wizard_flows from './pipette_wizard_flows.json' +import protocol_details from './protocol_details.json' +import protocol_info from './protocol_info.json' +import protocol_list from './protocol_list.json' +import protocol_setup from './protocol_setup.json' +import quick_transfer from './quick_transfer.json' +import robot_calibration from './robot_calibration.json' +import robot_controls from './robot_controls.json' +import run_details from './run_details.json' +import top_navigation from './top_navigation.json' +import error_recovery from './error_recovery.json' +import incompatible_modules from './incompatible_modules.json' + +export const zh = { + shared, + anonymous, + app_settings, + branded, + change_pipette, + protocol_command_text, + device_details, + device_settings, + devices_landing, + drop_tip_wizard, + firmware_update, + gripper_wizard_flows, + heater_shaker, + instruments_dashboard, + labware_details, + labware_landing, + labware_position_check, + module_wizard_flows, + pipette_wizard_flows, + protocol_details, + protocol_info, + protocol_list, + protocol_setup, + quick_transfer, + robot_calibration, + robot_controls, + run_details, + top_navigation, + error_recovery, + incompatible_modules, +} diff --git a/app/src/assets/localization/zh/instruments_dashboard.json b/app/src/assets/localization/zh/instruments_dashboard.json new file mode 100644 index 00000000000..f8b6c4f7c29 --- /dev/null +++ b/app/src/assets/localization/zh/instruments_dashboard.json @@ -0,0 +1,10 @@ +{ + "calibrate": "校准", + "detach": "卸载", + "firmware_version": "固件版本", + "last_calibrated": "上次校准", + "no_cal_data": "无校准数据", + "pipette_calibrations_differ": "建议重新校准移液器 已连接的移液器校准数据差异很大。正确校准时,这些数据应该近似。", + "recalibrate": "重新校准", + "serial_number": "序列号" +} diff --git a/app/src/assets/localization/zh/labware_details.json b/app/src/assets/localization/zh/labware_details.json new file mode 100644 index 00000000000..ba1183c2ea4 --- /dev/null +++ b/app/src/assets/localization/zh/labware_details.json @@ -0,0 +1,33 @@ +{ + "all": "全部", + "count": "数量", + "depth": "深度", + "diameter": "直径", + "flat": "平底", + "footprint": "占用空间 (mm)", + "generic": "通用型", + "height": "高度", + "length": "长度", + "manufacturer_number": "制造商/目录编号", + "manufacturer": "制造商", + "max_volume": "最大容量", + "measurements": "尺寸 (mm)", + "na": "不适用", + "shape": "形状", + "spacing": "间距 (mm)", + "tip": "吸头", + "total_length": "总长度", + "tube": "离心管", + "u": "U型底", + "v": "V型底", + "various": "多种", + "well_count": "孔数", + "well": "孔", + "width": "宽度", + "x_offset": "X-初始位置", + "x_size": "X-尺寸", + "x_spacing": "X-间距", + "y_offset": "Y-初始位置", + "y_size": "Y-尺寸", + "y_spacing": "Y-间距" +} diff --git a/app/src/assets/localization/zh/labware_landing.json b/app/src/assets/localization/zh/labware_landing.json new file mode 100644 index 00000000000..386114541f3 --- /dev/null +++ b/app/src/assets/localization/zh/labware_landing.json @@ -0,0 +1,28 @@ +{ + "api_name": "API 名称", + "cancel": "取消", + "cannot-run-python-missing-labware": "工作站缺少耗材定义,无法运行Python协议。", + "category": "类别", + "choose_file_to_upload": "或从您的计算机中选择文件上传。", + "choose_file": "选择文件", + "copied": "已复制!", + "create_new_def": "创建新的自定义耗材", + "custom_def": "自定义耗材", + "date_added": "添加日期", + "def_moved_to_trash": "该耗材将被转移到这台电脑的回收站,可能无法恢复。", + "delete_this_labware": "删除此耗材?", + "delete": "删除", + "duplicate_labware_def": "复制耗材", + "error_importing_file": "{{filename}}导入错误。", + "go_to_def": "前往耗材页面", + "import_custom_def": "导入自定义耗材", + "import": "导入", + "imported": "{{filename}}已导入。", + "invalid_labware_def": "无效耗材", + "labware": "耗材", + "last_updated": "最近更新", + "open_labware_creator": "打开耗材创建工具", + "show_in_folder": "在文件夹中显示", + "unable_to_upload": "无法上传文件", + "yes_delete_def": "是的,删除耗材" +} diff --git a/app/src/assets/localization/zh/labware_position_check.json b/app/src/assets/localization/zh/labware_position_check.json new file mode 100644 index 00000000000..fb92623de2e --- /dev/null +++ b/app/src/assets/localization/zh/labware_position_check.json @@ -0,0 +1,116 @@ +{ + "adapter_in_mod_in_slot": "{{slot}}中{{module}}上的{{adapter}}", + "adapter_in_slot": "{{slot}}上的{{adapter}}", + "adapter_in_tc": "{{module}}上的{{adapter}}", + "all_modules_and_labware_from_protocol": "{{protocol_name}}协议中使用的所有模块与耗材 ", + "applied_offset_data": "已应用的耗材校准数据", + "apply_offset_data": "应用耗材校准数据", + "apply_offsets": "应用校准数据", + "attach_probe": "安装校准探头", + "backmost": "最靠后的", + "calibration_probe": "校准探头", + "check_item_in_location": "检查{{location}}上的{{item}}", + "check_labware_in_slot_title": "检查板位{{slot}}中的耗材{{labware_display_name}}", + "check_remaining_labware_with_primary_pipette_section": "使用{{primary_mount}}移液器和吸头检查剩余耗材", + "check_tip_location": "A1的吸头尖端", + "check_well_location": "耗材上的A1孔", + "clear_all_slots": "清空甲板上所有的耗材,模块原位保留", + "clear_all_slots_odd": "清空甲板上所有的耗材及模块", + "cli_ssh": "命令行界面 (SSH)", + "close_and_apply_offset_data": "关闭并保存耗材校准数据", + "confirm_detached": "确认移除", + "confirm_pick_up_tip_modal_title": "移液器是否成功拾取了吸头?", + "confirm_pick_up_tip_modal_try_again_text": "否,重试", + "confirm_position_and_move": "确认位置,移动到板位{{next_slot}}", + "confirm_position_and_pick_up_tip": "确认位置,取吸头", + "confirm_position_and_return_tip": "确认位置,将吸头返回至板位{{next_slot}}并复位", + "detach_probe": "移除校准探头", + "ensure_nozzle_position_odd": "请检查并确认{{tip_type}}位于{{item_location}}正上方并水平对齐。如果位置不正确,请点击移动移液器,然后微调移液器直至完全对齐。", + "ensure_nozzle_position_desktop": "请检查并确认{{tip_type}}位于{{item_location}}正上方并水平对齐。如果位置不正确,请使用下方的控制按键或键盘微调移液器直至完全对齐。", + "exit_screen_confirm_exit": "不保存耗材校准数据并退出", + "exit_screen_go_back": "返回耗材位置校准", + "exit_screen_subtitle": "如果您现在退出,所有耗材校准数据都将不保留,且无法恢复。", + "exit_screen_title": "确定要在完成耗材位置校准前退出?", + "get_labware_offset_data": "获取耗材校准数据", + "install_probe": "从存储位置取出校准探头,将探头的锁套旋钮按顺时针方向拧松。对准图示位置,将校准探头向上轻推并压到顶部,使探头在{{location}}移液器吸嘴上压紧。随后将锁套旋钮按逆时针方向拧紧,并轻拉确认是否固定稳妥。", + "jog_controls_adjustment": "需要进行调整吗?", + "jupyter_notebook": "Jupyter Notebook", + "labware_display_location_text": "甲板板位{{slot}}", + "labware_offset_data": "耗材校准数据", + "labware_offset": "耗材校准数据", + "labware_offsets_deleted_warning": "一旦开始耗材位置校准,之前创建的耗材校准数据将会丢失。", + "labware_offsets_summary_labware": "耗材", + "labware_offsets_summary_location": "位置", + "labware_offsets_summary_offset": "耗材校准数据", + "labware_offsets_summary_title": "将应用于本次运行的耗材校准数据", + "labware_position_check_description": "耗材位置校准是一个引导式工作流程,为了提高实验中移液器的位置精确度,它会检查甲板上的每一个耗材位置。首先检查吸头盒,然后检查协议中使用到的其它所有耗材。", + "labware_position_check_overview": "耗材位置校准概览", + "labware_position_check_title": "耗材位置校准", + "labware_step_detail_labware_plural": "吸头应位于 {{labware_name}} 第一列正上方,居中对齐,并且与耗材顶部水平对齐。", + "labware_step_detail_labware": "吸头应位于 {{labware_name}} 的A1孔正上方,居中对齐,并且与耗材顶部水平对齐。", + "labware_step_detail_link": "查看如何判断移液器是否居中", + "labware_step_detail_modal_heading": "如何判断移液器是否居中且水平对齐", + "labware_step_detail_modal_nozzle_image_1_text": "从正前方看,似乎已经居中...", + "labware_step_detail_modal_nozzle_image_2_nozzle_text": "移液器吸嘴未居中", + "labware_step_detail_modal_nozzle_image_2_text": "...但从侧面看,需要调整", + "labware_step_detail_modal_nozzle_or_tip_image_1_text": "从俯视角度看,似乎是水平的...", + "labware_step_detail_modal_nozzle_or_tip_image_2_nozzle_text": "移液器吸嘴不水平", + "labware_step_detail_modal_nozzle_or_tip_image_2_text": "...但从水平高度看,需要调整", + "labware_step_detail_modal_nozzle_or_tip_image_3_text": "如遇觉得难以判断,请在吸嘴与吸头之间放入一张常规纸张。当这张纸能刚好卡在两者之间时,可确认高度位置。", + "labware_step_detail_modal_nozzle_or_tip": "为确保吸嘴或吸头与耗材顶部水平对齐,请水平直视进行判断,或在吸嘴与吸头间插入一张纸片辅助判断。", + "labware_step_detail_modal_nozzle": "为确保移液器吸嘴完全居中,请额外从OT-2的另一侧进行检查。", + "labware_step_detail_tiprack_plural": "移液器吸嘴应位于{{tiprack_name}}第一列正上方并居中对齐,并且与吸头顶部水平对齐。", + "labware_step_detail_tiprack": "移液器吸嘴应居中于{{tiprack_name}}的A1位置上方,并且与吸头顶部水平对齐。", + "labware": "耗材", + "learn_more": "了解更多", + "location": "位置", + "lpc_complete_summary_screen_heading": "耗材位置校准完成", + "module_display_location_text": "{{moduleName}}位于甲板板位{{slot}}", + "module_in_slot": "{{module}}位于板位{{slot}}", + "move_pipette": "移动移液器", + "move_to_a1_position": "将移液器移到A1位置并对齐", + "moving_to_slot_title": "正在移动到板位{{slot}}", + "new_labware_offset_data": "新的耗材校准数据", + "ninety_six_probe_location": "A1(左上角)", + "no_labware_offsets": "无耗材校准数据", + "no_offset_data_available": "没有可用的耗材校准数据", + "no_offset_data_on_robot": "这轮运行中此工作站没有可用的耗材校准数据。", + "no_offset_data": "没有可用的校准数据", + "offsets": "校准数据", + "pick_up_tip_from_rack_in_location": "从位于{{location}}的吸头盒上拾取吸头", + "picking_up_tip_title": "在板位{{slot}}拾取吸头", + "pipette_nozzle": "离您最远的移液器吸嘴", + "place_a_full_tip_rack_in_location": "将装满吸头的{{tip_rack}}放入{{location}}", + "place_labware_in_adapter_in_location": "在{{location}}先放置{{adapter}},再放置{{labware}}", + "place_labware_in_location": "将{{labware}}放入{{location}}", + "place_modules": "在甲板上放置模块", + "place_previous_tip_rack_in_location": "将之前使用的{{tip_rack}}放回{{location}}。移液器将把吸头放回原来的位置。", + "position_check_description": "耗材位置校准是一种工作引导流程,旨在对协议中用到的每个耗材在甲板上的具体位置做进一步确认。当检查耗材时,OT-2的移液器吸嘴或连接的吸头将停在A1孔的正中心。如果吸嘴或吸头未居中,您可以用OT-2的调整面板进行调整。此耗材校准数据将全面应用。校准精确度为0.1mm,可调方向包括X、Y和/或Z。", + "prepare_item_in_location": "在{{location}}准备{{item}}", + "primary_pipette_tipracks_section": "使用{{primary_mount}}移液器检查吸头盒并拾取吸头", + "remove_calibration_probe": "移除校准探头", + "remove_probe": "将校准探头解锁,将其从吸嘴上拆下,并放回存储位置。", + "remove_probe_before_exit": "退出前请移除校准探头", + "return_tip_rack_to_location": "将吸头盒放回{{location}}", + "return_tip_section": "放回吸头", + "returning_tip_title": "正在板位{{slot}}放回吸头", + "reveal_jog_controls": "显示调整面板", + "robot_has_no_offsets_from_previous_runs": "耗材校准数据引用自之前运行的协议,以节省您的时间。如果本协议中的所有耗材已在之前的运行中检查过,这些数据将应用于本次运行。 您可以使用耗材位置校准程序的后续步骤来添加耗材校准数据。", + "robot_has_offsets_from_previous_runs": "此工作站具有本协议中所用耗材的校准数据。如果您应用了这些校准数据,仍可通过耗材位置校准程序进行调整。", + "robot_in_motion": "工作站正在运行,请远离。", + "run_labware_position_check": "运行耗材位置校准程序", + "run": "运行", + "secondary_pipette_tipracks_section": "使用{{secondary_mount}}移液器检查吸头盒", + "see_how_offsets_work": "了解耗材校准的工作原理", + "slot_location": "板位位置", + "slot_name": "板位{{slotName}}", + "slot": "板位{{slotName}}", + "start_position_check": "开始耗材位置校准程序,移至板位{{initial_labware_slot}}", + "stored_offset_data": "应用已存储的耗材校准数据?", + "stored_offsets_for_this_protocol": "适用于本协议的已存储耗材校准数据", + "table_view": "表格视图", + "tip_rack": "吸头盒", + "view_current_offsets": "查看当前校准数据", + "view_data": "查看数据", + "what_is_labware_offset_data": "什么是耗材校准数据?" +} diff --git a/app/src/assets/localization/zh/module_wizard_flows.json b/app/src/assets/localization/zh/module_wizard_flows.json new file mode 100644 index 00000000000..b1cd0b086d6 --- /dev/null +++ b/app/src/assets/localization/zh/module_wizard_flows.json @@ -0,0 +1,47 @@ +{ + "attach_probe": "将校准探头安装到移液器", + "begin_calibration": "开始校准", + "calibrate_pipette": "在进行模块校准之前,请先校准移液器", + "calibrate": "校准", + "calibration_adapter_heatershaker": "校准适配器", + "calibration_adapter_temperature": "校准适配器", + "calibration_adapter_thermocycler": "校准适配器", + "calibration_probe_touching_thermocycler": "校准探头将接触探测热循环仪的校准方块,以确定其确切位置", + "calibration_probe_touching": "校准探头将接触探测{{slotNumber}}板位中{{module}}的校准方块,以确定其确切位置", + "calibration_probe": "从存储位置取出校准探头,将探头的锁套旋钮按顺时针方向拧松。对准图示位置,将校准探头向上轻推并压到顶部。随后将锁套旋钮按逆时针方向拧紧,并轻拉确认是否固定稳妥。", + "calibration": "{{module}}校准", + "checking_firmware": "检查{{module}}固件", + "complete_calibration": "完成校准", + "confirm_location": "确认位置", + "confirm_placement": "确认已放置", + "detach_probe_description": "解锁校准探头,将其从吸嘴上卸下并放回原储存位置。", + "detach_probe": "卸下移液器校准探头", + "error_during_calibration": "校准过程中出现错误", + "error_prepping_module": "模块校准出错", + "exit": "退出", + "firmware_up_to_date": "{{module}}固件已是最新版本。", + "firmware_updated": "{{module}}固件已更新!", + "install_adapter": "将校准适配器放置在{{module}}中", + "install_calibration_adapter": "安装校准适配器", + "location_occupied": "当前甲板配置中,在此处的硬件为{{fixture}}", + "module_calibrating": "{{moduleName}}正在校准中,请远离", + "module_calibration": "模块校准", + "module_heating_or_cooling": "模块正在升降温时无法进行模块校准", + "module_secured": "模块必须完全固定在其托架中,然后再安装到甲板对应板位。", + "module_too_hot": "模块温度过高,无法进行模块校准", + "move_gantry_to_front": "将龙门架移至前端", + "next": "下一步", + "pipette_probe": "移液器校准探头", + "place_flush_heater_shaker": "将适配器水平放到模块上。使用热震荡模块专用螺丝和 T10 Torx 螺丝刀将适配器固定到模块上。", + "place_flush_thermocycler": "确保热循环仪盖子已打开,并将适配器水平放置到模块上,即通常放置pcr板的位置。", + "place_flush": "将适配器水平放到模块上。", + "prepping_module": "正在准备{{module}}进行模块校准", + "recalibrate": "重新校准", + "select_location": "选择模块放置位置", + "select_the_slot": "请在右侧的甲板图中选择放置了{{module}}的板位。请确认所选择位置的正确性以确保顺利完成校准。", + "slot_unavailable": "板位不可用", + "stand_back_robot_in_motion": "工作站正在运行,请远离", + "stand_back": "正在进行校准,请远离", + "start_setup": "开始设置", + "successfully_calibrated": "{{module}}已成功校准" +} diff --git a/app/src/assets/localization/zh/pipette_wizard_flows.json b/app/src/assets/localization/zh/pipette_wizard_flows.json new file mode 100644 index 00000000000..1e75fe6223d --- /dev/null +++ b/app/src/assets/localization/zh/pipette_wizard_flows.json @@ -0,0 +1,90 @@ +{ + "align_the_connector": "对准对接孔,将移液器安装到工作站上。使用六角螺丝刀拧紧螺丝以固定移液器。然后手动检查其是否已完全固定。", + "all_pipette_detached": "所有移液器已成功卸下", + "are_you_sure_exit": "您确定要在完成{{flow}}之前退出吗?", + "attach_96_channel_plus_detach": "卸下{{pipetteName}}并安装96通道移液器", + "attach_96_channel": "安装96通道移液器", + "attach_mounting_plate_instructions": "对准对接孔,将移液器安装到工作站上。为了准确对齐,您可能需要调整右侧移液器支架的位置。", + "attach_mounting_plate": "安装固定板", + "attach_pip": "安装移液器", + "attach_pipette": "安装{{mount}}移液器", + "attach_probe": "安装校准探头", + "attach": "正在安装移液器", + "backmost": "最后面的", + "before_you_begin": "开始之前", + "begin_calibration": "开始校准", + "cal_pipette": "校准移液器", + "calibrate_96_channel": "校准96通道移液器", + "calibrate_pipette": "校准{{mount}}移液器", + "calibration_probe_touching": "校准探头将在甲板位{{slotNumber}}中的校准方框侧面进行接触,以确定其精确位置。", + "cancel_attachment": "取消安装", + "cancel_detachment": "取消卸下", + "choose_pipette": "选择要安装的移液器", + "complete_cal": "完成校准", + "connect_96_channel": "连接并安装96通道移液器", + "connect_and_secure_pipette": "连接并固定移液器", + "continue": "继续", + "critical_unskippable_step": "这是关键步骤,不应跳过", + "detach_96_attach_mount": "卸下96通道移液器并安装{{mount}}移液器", + "detach_96_channel": "卸下96通道移液器", + "detach_and_reattach": "卸下并重新安装移液器", + "detach_and_retry": "卸下并重试", + "detach_mount_attach_96": "卸下{{mount}}移液器并安装96通道移液器", + "detach_mounting_plate_instructions": "抓住板子,防止其掉落。拧开移液器固定板的销钉。", + "detach_next_pipette": "卸下下一个移液器", + "detach_pipette_to_attach_96": "卸下{{pipetteName}}并安装96通道移液器", + "detach_pipette": "卸下{{mount}}移液器", + "detach_pipettes_attach_96": "卸下移液器并安装96通道移液器", + "detach_z_axis_screw_again": "在安装96通道移液器前,先拧开Z轴螺丝。", + "detach": "正在卸下移液器", + "exit_cal": "退出校准", + "firmware_updating": "需要固件更新,仪器正在更新中...", + "firmware_up_to_date": "已是最新版本。", + "gantry_empty_for_96_channel_success": "现在两个移液器支架都为空,您可以开始进行96通道移液器的安装。", + "get_started_detach": "开始前,请移除甲板上的实验耗材,清理工作区以便卸载。同时准备好屏幕所示的所需设备。", + "grab_screwdriver": "保持原位不动,用2.5毫米螺丝刀,按照引导动画所示拧紧螺丝。在继续操作前,手动测试移液器安装和固定情况。", + "hold_and_loosen": "用手扶住移液器并拧松移液器螺丝。(螺丝是固定的,不会与移液器分离。)然后小心地卸下移液器。", + "hold_pipette_carefully": "用手扶住移液器防止掉落。对准对接孔,安装移液器。使用螺丝刀拧紧前面的四个螺丝,确保稳固连接。", + "how_to_reattach": "将右侧移液器支架推到Z轴的顶部。然后拧紧移液器支架右上方的固定螺丝。拧紧固定螺丝后,右侧支架应不能再自由上下移动。", + "install_probe": "从存放位置取出校准探头。确保其锁套旋钮已拧松。将校准探头对准移液器{{location}}位置,然后向上轻推并压到顶部。拧紧锁套旋钮。用手测试校准探头是否已固定。", + "loose_detach": "拧松螺丝并卸下", + "move_gantry_to_front": "将龙门架移至前方", + "must_detach_mounting_plate": "在使用其他移液器之前,您必须卸下安装板并重新连接移液器支架z轴板。我们不建议在完成前退出此过程。", + "name_and_volume_detected": "检测到{{name}}移液器", + "next": "下一步", + "ninety_six_channel": "{{ninetySix}}通道移液器", + "ninety_six_detached_success": "{{pipetteName}}移液器成功卸下", + "ninety_six_probe_location": "A1(左后方)", + "pip_cal_failed": "移液器校准失败", + "pip_cal_success": "{{pipetteName}}校准成功", + "pip_recal_success": "{{pipetteName}}重新校准成功", + "pipette_attached": "{{pipetteName}}成功安装", + "pipette_calibrating": "请远离,{{pipetteName}}正在校准", + "pipette_calibration": "移液器校准", + "pipette_detached": "移液器成功卸下", + "pipette_failed_to_attach": "无法检测到移液器", + "pipette_failed_to_detach": "{{pipetteName}}仍未卸下", + "pipette_heavy": "96通道移液器较重({{weight}})。如有需要,可以请其他人员帮忙。", + "please_install_correct_pip": "请改用{{pipetteName}}", + "progress_will_be_lost": "{{flow}}的进度将会丢失", + "reattach_carriage": "重新连接Z轴板", + "recalibrate_pipette": "重新校准{{mount}}移液器", + "remove_cal_probe": "移除校准探头", + "remove_labware_to_get_started": "开始前,请清除甲板上的实验耗材,清理工作区,以便校准。同时收集屏幕显示的所需设备。校准探头随工作站提供,应存放在工作站的右前方支柱上。", + "remove_labware": "开始前,请清除甲板上的实验耗材,清理工作区,以便校准。同时收集屏幕显示的所需设备。校准探头随工作站提供,应存放在工作站的右前方支柱上。", + "remove_probe": "拧松校准探头,将其从喷嘴上拆下,并放回存储位置。", + "replace_pipette": "更换{{mount}}移液器", + "return_probe_error": "退出前,请将校准探头放回其存放位置。 {{error}}", + "single_mount_attached_error": "当此为96通道流程时,请选择单支架移液器", + "single_or_8_channel": "{{single}}或{{eight}}通道移液器", + "stand_back": "请远离,工作站正在移动", + "try_again": "再试一次", + "unable_to_detect_probe": "无法检测到校准探头", + "unscrew_and_detach": "拧松螺丝并卸下安装板", + "unscrew_at_top": "拧松z轴板右上角的固定螺丝。这将解除右侧移液器支架的锁定,使其能够自由上下移动。", + "unscrew_carriage": "拧松Z轴板螺丝", + "waste_chute_error": "在继续之前,请卸下外置垃圾槽。", + "waste_chute_warning": "如果安装了外置垃圾槽,请在继续之前将其卸下。", + "wrong_pip": "安装了错误的设备", + "z_axis_still_attached": "Z轴板螺丝仍然处于锁定状态" +} diff --git a/app/src/assets/localization/zh/protocol_command_text.json b/app/src/assets/localization/zh/protocol_command_text.json new file mode 100644 index 00000000000..81fff4fd220 --- /dev/null +++ b/app/src/assets/localization/zh/protocol_command_text.json @@ -0,0 +1,71 @@ +{ + "adapter_in_mod_in_slot": "{{adapter}}在{{slot}}的{{module}}上", + "adapter_in_slot": "{{adapter}}在{{slot}}上", + "aspirate": "从{{labware_location}}的{{labware}}的{{well_name}}孔中以{{flow_rate}}µL/秒的速度吸液{{volume}}µL", + "aspirate_in_place": "在原位以{{flow_rate}}µL/秒的速度吸液{{volume}}µL", + "blowout": "在{{labware_location}}的{{labware}}的{{well_name}}孔中以{{flow_rate}}µL/秒的速度排空", + "blowout_in_place": "在原位以{{flow_rate}}µL/秒的速度排空", + "closing_tc_lid": "关闭热循环仪热盖", + "comment": "解释", + "configure_for_volume": "配置{{pipette}}以吸液{{volume}}µL", + "configure_nozzle_layout": "配置{{pipette}}以使用{{amount}}个通道", + "confirm_and_resume": "确认并继续", + "deactivate_hs_shake": "停用震荡功能", + "deactivate_temperature_module": "停用温控模块", + "deactivating_hs_heater": "停用加热功能", + "deactivating_tc_block": "停用热循环仪温控功能", + "deactivating_tc_lid": "停用热循环仪热盖功能", + "degrees_c": "{{temp}}°C", + "disengaging_magnetic_module": "下降磁力架模块", + "dispense_push_out": "以{{flow_rate}}µL/秒的速度将{{volume}}µL 排液至{{labware_location}}的{{labware}}的{{well_name}}孔中,并推出{{push_out_volume}}µL", + "dispense": "以{{flow_rate}}µL/秒的速度将{{volume}}µL 排液至{{labware_location}}的{{labware}}的{{well_name}}孔中", + "dispense_in_place": "在原位以{{flow_rate}}µL/秒的速度排液{{volume}}µL", + "drop_tip": "将吸头丢入{{labware}}的{{well_name}}孔中", + "drop_tip_in_place": "在原位丢弃吸头", + "engaging_magnetic_module": "抬升磁力架模块", + "fixed_trash": "垃圾桶", + "home_gantry": "复位所有龙门架、移液器和柱塞轴", + "latching_hs_latch": "在热震荡模块上锁定实验耗材", + "module_in_slot_plural": "{{module}}", + "module_in_slot": "{{module}}在{{slot_name}}号板位", + "move_labware_manually": "手动将{{labware}}从{{old_location}}移动到{{new_location}}", + "move_labware_on": "在{{robot_name}}上移动实验耗材", + "move_labware_using_gripper": "使用转板抓手将{{labware}}从{{old_location}}移动到{{new_location}}", + "move_labware": "移动实验耗材", + "move_relative": "沿{{axis}}轴移动{{distance}}毫米", + "move_to_coordinates": "移动到 (X:{{x}}, Y:{{y}}, Z:{{z}})", + "move_to_slot": "移动到{{slot_name}}号板位", + "move_to_well": "移动到{{labware_location}}的{{labware}}的{{well_name}}孔", + "move_to_addressable_area": "移动到{{addressable_area}}", + "move_to_addressable_area_drop_tip": "移动到{{addressable_area}}", + "notes": "备注", + "off_deck": "甲板外", + "offdeck": "甲板外", + "opening_tc_lid": "打开热循环仪热盖", + "pause_on": "在{{robot_name}}上暂停", + "pause": "暂停", + "pickup_tip": "从{{labware_location}}的{{labware}}的{{well_range}}孔中拾取吸头", + "prepare_to_aspirate": "准备使用{{pipette}}吸液", + "return_tip": "将吸头返回到{{labware_location}}的{{labware}}的{{well_name}}孔中", + "save_position": "保存位置", + "set_and_await_hs_shake": "设置热震荡模块以{{rpm}}rpm 震动并等待达到该转速", + "setting_hs_temp": "将热震荡模块的目标温度设置为{{temp}}", + "setting_temperature_module_temp": "将温控模块设置为{{temp}}(四舍五入到最接近的整数)", + "setting_thermocycler_block_temp": "将热循环仪加热块温度设置为{{temp}},并在达到目标后保持{{hold_time_seconds}}秒", + "setting_thermocycler_lid_temp": "将热循环仪热盖温度设置为{{temp}}", + "slot": "板位{{slot_name}}", + "target_temperature": "目标温度", + "tc_awaiting_for_duration": "等待热循环仪程序完成", + "tc_run_profile_steps": "温度:{{celsius}}°C,时间:{{seconds}}秒", + "tc_starting_profile": "热循环仪开始进行由以下步骤组成的{{repetitions}}次循环:", + "trash_bin_in_slot": "垃圾桶在{{slot_name}}", + "touch_tip": "吸头接触内壁", + "unlatching_hs_latch": "解锁热震荡模块上的实验耗材", + "wait_for_duration": "暂停{{seconds}}秒。{{message}}", + "wait_for_resume": "暂停协议", + "waiting_for_hs_to_reach": "等待热震荡模块达到目标温度", + "waiting_for_tc_block_to_reach": "等待热循环仪加热块达到目标温度并保持指定时间", + "waiting_for_tc_lid_to_reach": "等待热循环仪热盖达到目标温度", + "waiting_to_reach_temp_module": "等待温控模块达到{{temp}}", + "waste_chute": "外置垃圾槽" +} diff --git a/app/src/assets/localization/zh/protocol_details.json b/app/src/assets/localization/zh/protocol_details.json new file mode 100644 index 00000000000..b62bc2bb89a --- /dev/null +++ b/app/src/assets/localization/zh/protocol_details.json @@ -0,0 +1,95 @@ +{ + "add_required_csv_file": "添加所需的CSV文件以继续。", + "author": "作者", + "both_mounts": "两个移液器支架", + "choices": "{{count}}个选择", + "choose_file": "选择文件", + "choose_robot_to_run": "选择要运行的工作站\n{{protocol_name}}", + "clear_and_proceed_to_setup": "清除并继续设置", + "connect_modules_to_see_controls": "连接模块以查看控制", + "connected": "已连接", + "connection_status": "连接状态", + "creation_method": "创建方法", + "csv_file_type_required": "需要CSV文件类型", + "csv_file": "CSV文件", + "csv_required": "该协议需要CSV文件才能继续。", + "deck_view": "甲板视图", + "default_value": "默认值", + "delete_protocol_perm": "{{name}}及其运行历史将被永久删除。", + "delete_protocol": "删除协议", + "delete_this_protocol": "删除此协议?", + "description": "描述", + "extension_mount": "扩展安装支架", + "file_required": "需要文件", + "gripper_pick_up_count_description": "使用转板抓手移动单个耗材的指令。", + "gripper_pick_up_count": "转板次数", + "hardware": "硬件", + "labware_name": "耗材名称", + "labware": "耗材", + "last_analyzed": "上一次分析", + "last_updated": "上一次更新", + "left_mount": "左移液器安装位", + "left_right": "左,右", + "liquid_name": "液体名称", + "liquids_not_in_protocol": "此协议未指定任何液体", + "liquids": "液体", + "listed_values_are_view_only": "列出的值仅供查看", + "location": "位置", + "modules": "模块", + "n_a": "—", + "name": "名称", + "no_available_robots_found": "未找到可用的工作站", + "no_custom_values": "未指定任何自定义值", + "no_parameters": "该协议未指定任何参数", + "no_summary": "没有为此协议指定摘要。", + "not_connected": "未连接", + "not_in_protocol": "该协议未指定{{section}}", + "num_choices": "{{num}}个选择", + "num_options": "{{num}}个选项", + "off": "关闭", + "on_off": "开,关", + "on": "开启", + "org_or_author": "组织/作者", + "parameters": "参数", + "pipette_aspirate_count_description": "每个移液器的单个吸液指令。", + "pipette_aspirate_count": "{{pipette}}吸液次数", + "pipette_dispense_count_description": "每个移液器的单个排液指令。", + "pipette_dispense_count": "{{pipette}}分液次数", + "pipette_pick_up_count_description": "每个移液器的单个拾取吸头指令。", + "pipette_pick_up_count": "{{pipette}}拾取吸头次数", + "proceed_to_setup": "继续进行设置", + "protocol_designer_version": "在线协议编辑器{{version}}", + "protocol_failed_app_analysis": "该协议在应用程序内分析失败。它可能在没有自定义软件配置的工作站上无法使用。", + "protocol_outdated_app_analysis": "该协议的分析已过期。如果您现在运行它,可能会产生不同的结果。", + "python_api_version": "Python API {{version}}", + "quantity": "数量", + "range": "范围", + "read_less": "读取更少", + "read_more": "读取更多", + "requires_csv": "需要CSV", + "requires_upload": "需要上传", + "restore_defaults": "恢复默认值", + "right_mount": "右移液器安装位", + "robot_configuration": "工作站配置", + "robot_is_busy_with_protocol": "{{robotName}}正在运行{{protocolName}},状态为{{runStatus}}。是否要清除并继续?", + "robot_is_busy": "{{robotName}}正在工作", + "robot": "工作站", + "run_protocol": "运行协议", + "select_parameters_for_robot": "选择{{robot_name}}的参数", + "send": "发送", + "sending": "发送中", + "show_in_folder": "在文件夹中显示", + "slot": "{{slotName}}号板位", + "start_setup_customize_values": "开始设置以自定义值", + "start_setup": "开始设置", + "successfully_sent": "发送成功", + "total_volume": "总体积", + "unavailable_or_busy_robot_not_listed_plural": "{{count}}台不可用或工作中的工作站未列出。", + "unavailable_or_busy_robot_not_listed": "{{count}}台不可用或工作中的工作站未列出。", + "unavailable_robot_not_listed_plural": "{{count}}台不可用的工作站未列出。", + "unavailable_robot_not_listed": "{{count}}台不可用的工作站未列出。.", + "unsuccessfully_sent": "发送失败", + "value_out_of_range": "值必须在{{min}}-{{max}}之间", + "view_run_details": "查看运行详情", + "view_unavailable_robots": "在设备页面查看不可用的工作站" +} diff --git a/app/src/assets/localization/zh/protocol_info.json b/app/src/assets/localization/zh/protocol_info.json new file mode 100644 index 00000000000..e279383c495 --- /dev/null +++ b/app/src/assets/localization/zh/protocol_info.json @@ -0,0 +1,86 @@ +{ + "all_protocols": "所有协议", + "browse_protocol_library": "打开在线协议库", + "cancel_run": "取消运行", + "choose_file": "选择文件...", + "choose_snippet_type": "根据目标执行环境选择耗材校准数据的Python代码片段。", + "continue_proceed_to_calibrate": "继续校准", + "continue_verify_calibrations": "验证移液器和耗材的校准", + "create_or_download": "创建或下载新协议", + "creation_method": "创建方法", + "custom_labware_not_supported": "工作站不支持自定义耗材", + "date_added": "添加日期", + "delete_protocol": "删除协议", + "description": "描述", + "drag_file_here": "将协议文件拖放到此处", + "error_analyzing": "尝试分析时出错:{{protocolName}}", + "error_message_no_steps": "此协议没有任何步骤——您的工作站无法执行任何操作!您的协议至少需要一个吸液/排液步骤才能正确导入", + "estimated_run_time": "预计运行时间", + "exit_modal_body": "您确定要关闭此协议吗?", + "exit_modal_exit": "是,现在关闭", + "exit_modal_go_back": "不,返回", + "exit_modal_heading": "确认关闭协议", + "failed_analysis": "分析失败", + "get_labware_offset_data": "获取耗材校准数据", + "import_a_file": "导入协议以开始", + "import_new_protocol": "导入协议", + "import": "导入", + "instrument_cal_data_title": "校准数据", + "instrument_not_attached": "未连接", + "instruments_title": "所需移液器", + "intervention_modal_learn_more": "了解更多关于用户干预的信息", + "invalid_file_type": "无效的文件类型。协议文件必须是“.py”或“.json”格式。", + "invalid_json_file": "无效的JSON文件。", + "labware_cal_description": "以耗材原点的校准数据", + "labware_legacy_definition": "不适用", + "labware_offset_data_title": "耗材校准数据", + "labware_offsets_info": "{{number}}组耗材校准数据", + "labware_position_check_complete_toast_no_offsets": "耗材位置校准完成。无新建的耗材校准数据。", + "labware_position_check_complete_toast_with_offsets_plural": "耗材位置校准完成。新建了{{count}}组耗材校准数据。", + "labware_position_check_complete_toast_with_offsets": "耗材位置校准完成。创建了{{count}}组耗材校准数据。", + "labware_title": "所需耗材", + "last_run": "上次运行", + "last_updated": "最近更新", + "launch_protocol_designer": "打开在线协议编辑器", + "manual_steps_learn_more": "了解更多关于手动步骤的信息", + "modules_title": "所需模块", + "most_recent_updates": "最新更新", + "no_history": "无运行历史", + "no_labware_offset_data": "无耗材校准数据", + "no_protocol_yet": "还没有协议?", + "nothing_here_yet": "没有可显示的协议!", + "oldest_updates": "最早的更新", + "open_a_protocol": "打开一个协议以开始", + "open_api_docs": "打开Python API文档", + "organization_and_author": "组织/作者", + "pin_protocol": "固定协议", + "pinned_protocol": "固定的协议", + "pinned_protocols": "固定的协议", + "protocol_added": "已成功接收{{protocol_name}}", + "protocol_analysis_failed": "协议分析失败", + "protocol_finishing": "在{{robot_name}}上完成协议", + "protocol_loading": "在{{robot_name}}上打开协议", + "protocol_name_title": "协议名称", + "protocol_title": "协议 -{{protocol_name}}", + "protocol_upload_failed": "协议上传失败。请修复错误后重试", + "protocols": "协议", + "quick_transfer": "快速移液", + "required_cal_data_title": "校准数据", + "required_quantity_title": "数量", + "required_type_title": "类型", + "robot_name_last_run": "{{robot_name}}的上次运行", + "robot_type_first": "{{robotType}}协议优先", + "run_again": "再次运行", + "run_protocol": "运行协议", + "run_timestamp_title": "运行时间戳", + "simulation_in_progress": "正在进行模拟", + "timestamp": "+{{index}}", + "too_many_pins_body": "要添加更多协议到您的固定列表,请先移除一个协议。", + "too_many_pins_header": "您已达到最大限额!", + "unpin_protocol": "取消固定协议", + "unpinned_protocol": "取消固定的协议", + "update_robot_for_custom_labware": "您已将自定义耗材保存到您的应用程序中,但需要更新此工作站才能将这些自定义耗材与Python协议一起使用", + "upload": "上传", + "upload_and_simulate": "打开要在{{robot_name}}上运行的协议", + "valid_file_types": "有效的文件类型:Python文件(.py)或在线协议编辑器文件(.json)" +} diff --git a/app/src/assets/localization/zh/protocol_list.json b/app/src/assets/localization/zh/protocol_list.json new file mode 100644 index 00000000000..9c2b3be0f59 --- /dev/null +++ b/app/src/assets/localization/zh/protocol_list.json @@ -0,0 +1,26 @@ +{ + "csv_file_required": "需要CSV文件进行分析。请在运行设置时添加CSV文件。", + "delete_protocol_message": " 并且其运行历史记录将被永久删除。", + "delete_this_protocol": "删除此协议?", + "edit": "编辑", + "go_to_timeline": "转到时间轴", + "last_updated_at": "更新于{{date}}", + "left_mount": "左移液器安装位", + "loading_data": "正在加载数据...", + "modules": "模块", + "no_data": "无数据", + "protocol_analysis_failure": "协议分析失败。", + "protocol_analysis_outdated": "协议分析已过期。", + "protocol_deleted": "协议已删除", + "reanalyze_or_view_error": " 重新分析 协议或查看 错误详情 ", + "reanalyze_to_view": " 重新分析 协议", + "right_mount": "右移液器安装位", + "robot": "工作站", + "send_to_robot_overflow": "发送到{{robot_display_name}}", + "send_to_robot": "将协议发送到{{robot_display_name}}", + "show_in_folder": "在文件夹中显示", + "start_setup": "开始设置", + "this_protocol_will_be_trashed": "该协议将被移至此计算机的回收站,可能无法恢复。", + "updated": "已更新", + "yes_delete_this_protocol": "是的,删除协议" +} diff --git a/app/src/assets/localization/zh/protocol_setup.json b/app/src/assets/localization/zh/protocol_setup.json new file mode 100644 index 00000000000..7e5007e8a2c --- /dev/null +++ b/app/src/assets/localization/zh/protocol_setup.json @@ -0,0 +1,279 @@ +{ + "96_mount": "左+右移液器安装位", + "action_needed": "需要操作", + "adapter_slot_location_module": "{{slotName}}号板位,{{adapterName}}在{{moduleName}}上", + "adapter_slot_location": "{{slotName}}号板位,{{adapterName}}", + "add_fixture": "将{{fixtureName}}添加到{{locationName}}", + "add_this_deck_hardware": "将此硬件添加到您的甲板配置中。它将在协议分析期间被引用。", + "add_to_slot": "添加到{{slotName}}号板位", + "additional_labware": "{{count}}个额外的耗材", + "additional_off_deck_labware": "额外的甲板外耗材", + "attach_gripper_failure_reason": "连接所需的转板抓手以继续", + "attach_gripper": "连接转板抓手", + "attach_module": "校准前连接模块", + "attach_pipette_before_module_calibration": "在进行模块校准前连接移液器", + "attach_pipette_calibration": "连接移液器以查看校准信息", + "attach_pipette_cta": "连接移液器", + "attach_pipette_failure_reason": "连接所需的移液器以继续", + "attach_pipette_tip_length_calibration": "连接移液器以查看吸头长度校准信息", + "attach": "连接", + "back_to_top": "回到顶部", + "cal_all_pip": "首先校准移液器", + "calibrate_deck_failure_reason": "校准甲板以继续", + "calibrate_deck_to_proceed_to_pipette_calibration": "校准甲板以继续进行移液器校准", + "calibrate_deck_to_proceed_to_tip_length_calibration": "校准甲板以继续进行吸头长度校准", + "calibrate_gripper_failure_reason": "校准所需的转板抓手以继续", + "calibrate_module_failure_reason": "校准所需的模块以继续", + "calibrate_now": "现在校准", + "calibrate_pipette_before_module_calibration": "在进行模块校准前校准移液器", + "calibrate_pipette_failure_reason": "校准所需的移液器以继续", + "calibrate_tiprack_failure_reason": "校准所需的吸头长度以继续", + "calibrate": "校准", + "calibrated": "已校准", + "calibration_data_not_available": "一旦运行开始,校准数据不可用", + "calibration_needed": "需要校准", + "calibration_ready": "校准就绪", + "calibration_required_attach_pipette_first": "需要校准,请先连接移液器", + "calibration_required_calibrate_pipette_first": "需要校准,请先校准移液器", + "calibration_required": "需要校准", + "calibration_status": "校准状态", + "calibration": "校准", + "cancel_and_restart_to_edit": "取消运行并重新启动设置以进行编辑", + "choose_enum": "选择{{displayName}}", + "closing": "关闭中...", + "complete_setup_before_proceeding": "完成设置后继续运行", + "configure": "配置", + "configured": "已配置", + "confirm_heater_shaker_module_modal_description": "在开始运行之前,应使模块的两个锚固件完全伸出,以确保牢固连接。导热适配器应连接到模块上。", + "confirm_heater_shaker_module_modal_title": "确认已连接热震荡模块", + "confirm_values": "确认这些值", + "connect_all_hardware": "首先连接并校准所有硬件", + "connect_all_mod": "首先连接所有模块", + "connect_modules_for_controls": "连接模块以查看控制", + "connection_info_not_available": "一旦运行开始,连接信息不可用", + "connection_status": "连接状态", + "csv_file": "CSV 文件", + "currently_configured": "当前已配置", + "currently_unavailable": "当前不可用", + "custom_values": "自定义值", + "deck_cal_description_bullet_1": "在新工作站设置期间执行甲板校准。", + "deck_cal_description_bullet_2": "如果您搬迁了工作站,请重新进行甲板校准。", + "deck_cal_description": "这测量了甲板的 X 和 Y 值相对于门架的值。甲板校准是吸头长度校准和移液器偏移校准的基础。", + "deck_calibration_title": "甲板校准(Deck Calibration)", + "deck_conflict_info_thermocycler": "通过移除位置 A1 和 B1 中的固定装置来更新甲板配置。从甲板配置中移除对应装置或更新协议。", + "deck_conflict_info": "通过移除位置 {{cutout}} 中的 {{currentFixture}} 来更新甲板配置。从甲板配置中移除对应装置或更新协议。", + "deck_conflict": "甲板位置冲突", + "deck_hardware": "甲板硬件", + "deck_map": "甲板布局图", + "default_values": "默认值", + "example": "示例", + "exit_to_deck_configuration": "退出到甲板配置", + "extension_mount": "扩展安装支架", + "extra_attention_warning_title": "在继续运行前固定耗材和模块", + "extra_module_attached": "附加额外模块", + "feedback_form_link": "请告诉我们", + "fixture_name": "装置", + "fixture": "装置", + "fixtures_connected_plural": "已连接{{count}}个装置", + "fixtures_connected": "已连接{{count}}个装置", + "get_labware_offset_data": "获取耗材校准数据", + "hardware_missing": "缺少硬件", + "heater_shaker_extra_attention": "使用闩锁控制,便于放置耗材。", + "heater_shaker_labware_list_view": "要添加耗材,请使用切换键来控制闩锁", + "how_offset_data_works": "耗材校准数据如何工作", + "initial_liquids_num_plural": "{{count}}种初始液体", + "initial_liquids_num": "{{count}}种初始液体", + "initial_location": "初始位置", + "install_modules_and_fixtures": "安装并校准所需的模块。安装所需的装置。", + "install_modules_plural": "安装所需的模块。", + "install_modules": "安装所需的模块。", + "instrument_calibrations_missing_plural": "缺少{{count}}个校准", + "instrument_calibrations_missing": "缺少{{count}}个校准", + "instruments_connected_plural": "已连接{{count}}个硬件", + "instruments_connected": "已连接{{count}}个硬件", + "instruments": "硬件", + "labware_latch_instructions": "使用闩锁控制,便于放置耗材。", + "labware_latch": "耗材闩锁", + "labware_location": "耗材位置", + "labware_name": "耗材名称", + "labware_position_check_not_available_analyzing_on_robot": "在工作站上分析协议时,耗材位置校准不可用", + "labware_position_check_not_available_empty_protocol": "耗材位置校准需要协议加载耗材和移液器", + "labware_position_check_not_available": "运行开始后,耗材位置校准不可用", + "labware_position_check_step_description": "建议的工作流程可帮助您验证每个耗材在甲板上的位置。", + "labware_position_check_step_title": "耗材位置校准", + "labware_position_check_text": "耗材位置校准流程可帮助您验证甲板上每个耗材的位置。在此位置校准过程中,您可以创建耗材校准数据,以调整工作站在 X、Y 和 Z 方向上的移动。", + "labware_position_check": "耗材位置校准", + "labware_setup_step_description": "准备好以下耗材和完整的吸头盒。若不进行耗材位置校准直接运行协议,请将耗材放置在其初始位置并固定。", + "labware_setup_step_title": "耗材", + "labware": "耗材", + "last_calibrated": "最后校准:{{date}}", + "learn_how_it_works": "了解它的工作原理", + "learn_more_about_offset_data": "了解更多关于耗材校准数据的信息", + "learn_more_about_robot_cal_link": "了解更多关于工作站校准的信息", + "learn_more": "了解更多", + "liquid_setup_step_description": "查看液体的起始位置和体积", + "liquid_setup_step_title": "液体", + "liquids_not_in_setup": "此协议未使用液体", + "liquids_not_in_the_protocol": "此协议未指定液体。", + "liquids": "液体", + "list_view": "列表视图", + "loading_data": "加载数据...", + "loading_labware_offsets": "加载耗材校准数据", + "loading_protocol_details": "加载详情...", + "location_conflict": "位置冲突", + "location": "位置", + "lpc_and_offset_data_title": "耗材位置校准和耗材校准数据", + "lpc_disabled_calibration_not_complete": "确保工作站校准完成后再运行耗材位置校准", + "lpc_disabled_modules_and_calibration_not_complete": "确保工作站校准完成并且所有模块已连接后再运行耗材位置校准", + "lpc_disabled_modules_not_connected": "确保所有模块已连接后再运行耗材位置校准", + "lpc_disabled_no_tipracks_loaded": "耗材位置校准需要在协议中加载一个吸头盒", + "lpc_disabled_no_tipracks_used": "耗材位置校准要求协议中至少有一个吸头可供使用", + "map_view": "布局视图", + "missing_gripper": "缺少转板抓手", + "missing_instruments": "缺少{{count}}个", + "missing_pipettes_plural": "缺少{{count}}个移液器", + "missing_pipettes": "缺少{{count}}个移液器", + "missing": "缺少", + "modal_instructions_title": "{{moduleName}}设置说明", + "module_connected": "已连接", + "module_disconnected": "未连接", + "module_instructions_link": "{{moduleName}}设置说明", + "module_mismatch_body": "检查连接到该工作站的模块型号是否正确", + "module_name": "模块", + "module_not_connected": "未连接", + "module_setup_step_title": "甲板硬件", + "module_slot_location": "{{slotName}}号板位,{{moduleName}}", + "module": "模块", + "modules_connected_plural": "连接了{{count}}个模块", + "modules_connected": "连接了{{count}}个模块", + "modules_setup_step_title": "模块设置", + "modules": "模块", + "mount_title": "{{mount}}安装支架:", + "mount": "{{mount}}安装支架", + "multiple_fixtures_missing": "缺少{{count}}个装置", + "multiple_modules_example": "您的协议包含两个温控模块。连接到左侧第一个端口的温控模块对应协议中的第一个温控模块,连接到下一个端口的温控模块对应协议中的第二个温控模块。如果使用集线器,遵循相同的端口排序逻辑。", + "multiple_modules_explanation": "在协议中使用多个相同类型的模块时,首先需要将协议中第一个模块连接到工作站编号最小的USB端口,然后以相同方式连接其他模块。", + "multiple_modules_help_link_title": "查看如何设置相同类型的多个模块", + "multiple_modules_learn_more": "了解更多关于使用相同类型的多个模块的信息", + "multiple_modules_missing_plural": "缺少{{count}}个模块", + "multiple_modules_modal": "设置相同类型的多个模块", + "multiple_modules": "相同类型的多个模块", + "multiple_of_most_modules": "通过以特定顺序连接和加载模块,可以在单个Python协议中使用多种模块类型。无论模块占用哪个甲板板位,工作站都将首先初始化连接到最小编号端口的匹配模块,。", + "must_have_labware_and_pip": "协议中必须加载耗材和移液器", + "n_a": "不可用", + "name": "名称", + "no_custom_values": "未指定自定义值", + "no_data": "无数据", + "no_deck_hardware_specified": "该协议中未指定任何甲板硬件。", + "no_files_found": "未找到文件", + "no_labware_offset_data": "尚无耗材校准数据", + "no_modules_or_fixtures": "该协议中未指定任何模块或装置。", + "no_modules_specified": "该协议中未指定任何模块。", + "no_modules_used_in_this_protocol": "该协议中未使用硬件", + "no_parameters_specified_in_protocol": "协议中未指定任何参数", + "no_parameters_specified": "未指定参数", + "no_tiprack_loaded": "协议中必须加载一个吸头盒", + "no_tiprack_used": "协议中必须拾取一个吸头", + "no_usb_connection_required": "无需USB连接", + "no_usb_port_yet": "尚无USB端口", + "no_usb_required": "无需USB", + "not_calibrated": "尚未校准", + "not_configured": "未配置", + "off_deck": "甲板外", + "off": "关闭", + "offset_data": "偏移校准数据", + "offsets_applied_plural": "应用了{{count}}个偏移校准数据", + "offsets_applied": "应用了{{count}}个偏移校准数据", + "on_adapter_in_mod": "在{{moduleName}}中的{{adapterName}}上", + "on_adapter": "在{{adapterName}}上", + "on_deck": "在甲板上", + "on-deck_labware": "{{count}}个在甲板上的耗材", + "on": "开启", + "opening": "打开中...", + "parameters": "参数", + "pipette_mismatch": "移液器型号不匹配。", + "pipette_missing": "移液器缺失", + "pipette_offset_cal_description_bullet_1": "首次将移液器连接到新安装支架时执行移液器偏移校准。", + "pipette_offset_cal_description_bullet_2": "在执行甲板校准后重新进行移液器偏移校准。", + "pipette_offset_cal_description_bullet_3": "对用于校准移液器的吸头执行吸头长度校准后,重新进行移液器偏移校准。", + "pipette_offset_cal_description": "这会测量移液器相对于移液器安装支架和甲板的X、Y和Z值。移液器偏移校准依赖于甲板校准和吸头长度校准。 ", + "pipette_offset_cal": "移液器偏移校准", + "placement": "放置", + "plug_in_module_to_configure": "插入{{module}}以将其添加到板位", + "plug_in_required_module_plural": "插入并启动所需模块以继续", + "plug_in_required_module": "插入并启动所需模块以继续", + "prepare_to_run": "准备运行", + "proceed_to_labware_position_check": "继续进行耗材位置校准", + "proceed_to_labware_setup_step": "继续进行耗材设置", + "proceed_to_liquid_setup_step": "继续进行液体设置", + "proceed_to_module_setup_step": "继续进行模块设置", + "proceed_to_run": "继续运行", + "protocol_analysis_failed": "协议分析失败", + "protocol_can_be_closed": "该协议现在可以关闭。", + "protocol_run_canceled": "协议运行已取消。", + "protocol_run_complete": "协议运行完成。", + "protocol_run_failed": "协议运行失败。", + "protocol_run_started": "协议运行已开始。", + "protocol_specifies": "协议指定", + "protocol_upload_revamp_feedback": "对此体验有反馈吗?", + "quantity": "数量", + "recalibrate": "重新校准", + "recalibrating_not_available": "无法重新进行吸头长度校准和耗材位置校准。", + "recalibrating_tip_length_not_available": "运行开始后无法重新校准吸头长度", + "recommended": "推荐", + "required_instrument_calibrations": "所需的硬件校准", + "required": "必需", + "required_tip_racks_title": "所需的吸头长度校准", + "reset_parameter_values_body": "这将丢弃您所做的任何更改。所有参数将恢复默认值。", + "reset_parameter_values": "重置参数值?", + "reset_setup": "重新开始设置以进行编辑", + "reset_values": "重置值", + "resolve": "解决", + "restart_setup_and_try": "重新开始设置并尝试使用不同的参数值。", + "restart_setup": "重新开始设置", + "restore_default": "恢复默认值", + "restore_defaults": "恢复默认值", + "robot_cal_description": "工作站校准用于确定其相对于甲板的位置。良好的工作站校准对于成功运行协议至关重要。工作站校准包括3个部分:甲板校准、吸头长度校准和移液器偏移校准。", + "robot_cal_help_title": "工作站校准的工作原理", + "robot_calibration_step_description_pipettes_only": "查看该协议所需的硬件和校准。", + "robot_calibration_step_description": "查看该协议所需的移液器和吸头长度校准。", + "robot_calibration_step_title": "硬件", + "run_disabled_calibration_not_complete": "确保工作站校准完成后再继续运行", + "run_disabled_modules_and_calibration_not_complete": "确保工作站校准完成并且所有模块已连接后再继续运行", + "run_disabled_modules_not_connected": "确保所有模块已连接后再继续运行", + "run_labware_position_check": "运行耗材位置校准", + "run_never_started": "运行未开始", + "run": "运行", + "secure_labware_instructions": "固定耗材说明", + "secure_labware_modal": "将耗材固定到{{name}}", + "secure": "固定", + "setup_for_run": "运行设置", + "setup_instructions": "设置说明", + "setup_is_view_only": "运行开始后设置仅供查看", + "slot_location": "{{slotName}}号板位", + "slot_number": "板位编号", + "status": "状态", + "step": "步骤{{index}}", + "there_are_no_unconfigured_modules": "没有连接{{module}}。请连接一个模块并放置在{{slot}}号板位中。", + "there_are_other_configured_modules": "已有一个{{module}}配置在不同的板位中。退出运行设置,并更新甲板配置以转到已连接的模块。或连接另一个{{module}}继续设置。", + "tip_length_cal_description_bullet": "对移液器上将会用到的每种类型的吸头执行吸头长度校准。", + "tip_length_cal_description": "这将测量吸头底部与移液器喷嘴之间的Z轴距离。如果对用于校准移液器的吸头重新进行吸头长度校准,也需要重新进行移液器偏移校准。", + "tip_length_cal_title": "吸头长度校准", + "tip_length_calibration": "吸头长度校准", + "update_deck_config": "更新甲板配置", + "update_deck": "更新甲板", + "updated": "已更新", + "usb_connected_no_port_info": "USB端口已连接", + "usb_port_connected": "USB端口{{port}}", + "usb_port_number": "USB-{{port}}", + "value_out_of_range_generic": "值必须在范围内", + "value_out_of_range": "值必须在{{min}}-{{max}}之间", + "value": "值", + "values_are_view_only": "值仅供查看", + "view_current_offsets": "查看当前偏移量", + "view_moam": "查看工作站中放置同类型模块的设置说明。", + "view_setup_instructions": "查看设置说明", + "volume": "体积", + "what_labware_offset_is": "耗材偏移校准是一种位置调整类型,用于补偿甲板上耗材整体位置的微小实际差异。耗材偏移校准数据是耗材、甲板位和工作站的特定组合。", + "with_the_chosen_value": "使用选定的值时,发生以下错误:" +} diff --git a/app/src/assets/localization/zh/quick_transfer.json b/app/src/assets/localization/zh/quick_transfer.json new file mode 100644 index 00000000000..651ace8ef4b --- /dev/null +++ b/app/src/assets/localization/zh/quick_transfer.json @@ -0,0 +1,76 @@ +{ + "add_or_remove_columns": "添加或移除列", + "add_or_remove": "添加或移除", + "advanced_settings": "高级设置", + "all": "所有实验耗材", + "always": "每次吸液前", + "aspirate_volume": "每孔吸液体积", + "aspirate_volume_µL": "每孔吸液体积(µL)", + "both_mounts": "左侧+右侧支架", + "change_tip": "更换吸头", + "character_limit_error": "字数超出限制", + "column": "列", + "columns": "列", + "create_new_transfer": "创建新的快速移液命令", + "create_transfer": "创建移液命令", + "destination": "目标", + "destination_labware": "目标实验耗材", + "dispense_volume": "每孔排液体积", + "dispense_volume_µL": "每孔排液体积(µL)", + "enter_characters": "输入最多60个字符", + "exit_quick_transfer": "退出快速移液?", + "grid": "网格", + "grids": "网格", + "learn_more": "了解更多", + "left_mount": "左侧支架", + "lose_all_progress": "您将失去所有此快速移液流程进度.", + "name_your_transfer": "为您的快速移液流程命名", + "number_wells_selected_error_learn_more": "具有多个源孔{{selectionUnits}}的快速移液是可以进行一对一或者多对多移液的(为此移液流程同样选择{{wellCount}}个目标孔位{{selectionUnits}})或进行多对一移液,即合并为单个孔位(选择1个目标孔{{selectionUnit}})。", + "number_wells_selected_error_message": "选择1个或{{wellCount}}个{{selectionUnits}}来进行此移液操作.", + "once": "在移液开始时", + "overview": "概览", + "perDest": "每个目标孔位", + "perSource": "每个源孔位", + "pipette": "移液器", + "quick_transfer_volume": "快速移液{{volume}}µL", + "right_mount": "右侧支架", + "reservoir": "储液槽", + "run_now": "立即运行", + "run_quick_transfer_now": "您想立即运行快速移液流程吗?", + "save": "保存", + "save_to_run_later": "保存您的快速移液流程以备后续运行.", + "save_for_later": "保存备用", + "source": "源", + "select_attached_pipette": "选择已连接的移液器", + "select_by": "按...选择", + "select_dest_labware": "选择目标实验耗材", + "select_dest_wells": "选择目标孔位", + "select_source_labware": "选择源实验耗材", + "select_source_wells": "选择源孔位", + "select_tip_rack": "选择吸头盒", + "set_aspirate_volume": "设置吸液体积", + "set_dispense_volume": "设置排液体积", + "set_transfer_volume": "设置移液体积", + "source_labware": "源实验耗材", + "source_labware_d2": "D2位置的源实验耗材", + "starting_well": "起始孔", + "use_deck_slots": "快速移液将使用板位B2-D2。这些板位将用于放置吸头盒、源实验耗材和目标实验耗材。请确保使用最新的甲板配置,避免碰撞。", + "tip_drop_location": "吸头丢弃位置", + "tip_management": "吸头管理", + "tip_rack": "吸头盒", + "trashBin": "垃圾桶", + "trashBin_location": "位于{{slotName}}的垃圾桶", + "tubeRack": "试管架", + "volume_per_well": "每孔体积", + "volume_per_well_µL": "每孔体积(µL)", + "value_out_of_range": "值必须在{{min}}-{{max}}之间", + "labware": "实验耗材", + "pipette_currently_attached": "快速移液移液器选项取决于当前您工作站上安装的移液器.", + "wasteChute": "外置垃圾槽", + "wasteChute_location": "位于{{slotName}}的外置垃圾槽", + "wellPlate": "孔板", + "well_selection": "孔位选择", + "well_ratio": "快速移液可以一对一或者多对多进行移液的(为此移液操作选择同样数量的{{wells}})或可以多对一,也就是合并为单孔(选择1个目标孔位)。", + "well": "孔", + "wells": "孔" +} diff --git a/app/src/assets/localization/zh/robot_calibration.json b/app/src/assets/localization/zh/robot_calibration.json new file mode 100644 index 00000000000..8f83fb649f8 --- /dev/null +++ b/app/src/assets/localization/zh/robot_calibration.json @@ -0,0 +1,131 @@ +{ + "attached_pipettes": "已连接的移液器校准", + "before_you_begin": "开始之前", + "calibrate": "校准", + "calibrate_deck": "校准甲板", + "calibrate_pipette": "校准移液器", + "calibrate_tip_length": "校准移液器的吸头长度。", + "calibrate_tip_on_block": "在校准块上校准吸头", + "calibrate_tip_on_trash": "在垃圾桶上校准吸头", + "calibrate_xy_axes": "在{{slotName}}号板位中校准x轴和y轴", + "calibrate_z_axis_on_block": "在校准块上校准z轴", + "calibrate_z_axis_on_slot": "在5号板位中校准z轴", + "calibrate_z_axis_on_trash": "在垃圾桶上校准z轴", + "calibration_complete": "校准完成", + "calibration_dashboard": "校准面板", + "calibration_health_check": "校准健康检查", + "calibration_health_check_intro_body": "校准健康检查诊断甲板、吸头长度和移液器偏移校准的问题。您将移液器移动到不同位置,这将与您现有的校准数据进行比较。如果差异很大,系统会提示您重做部分或全部校准。", + "calibration_health_check_results": "校准健康检查结果", + "calibration_recommended": "建议校准", + "calibration_status": "校准状态", + "calibration_status_description": "为了准确和精确的运动,请校准工作站的甲板、移液器偏移数据和吸头长度。", + "calibrations_complete": "校准完成!", + "change_tip_rack": "更换吸头架", + "check_tip_on_block": "在校准块上检查吸头", + "check_tip_on_trash": "在垃圾桶上检查吸头", + "check_xy_axes": "在{{slotName}}号板位中检查x轴和y轴", + "check_z_axis_on_block": "在校准块上检查z轴", + "check_z_axis_on_slot": "在5号板位上检查z轴", + "check_z_axis_on_trash": "在垃圾桶上检查z轴", + "choose_a_tip_rack": "选择一个吸头盒", + "choose_tip_rack": "选择您想要用来校准吸头长度的吸头盒。想要使用这里没有列出的吸头盒吗?请转到实验耗材>导入以添加实验耗材。", + "clear_other_slots": "清除所有其他甲板位", + "confirm_exit_before_completion": "您确定要在完成{{sessionType}}之前退出吗?", + "confirm_placement": "确认放置位置", + "confirm_tip_rack": "确认吸头盒", + "custom": "自定义", + "deck_calibration": "甲板校准", + "deck_calibration_description": "校准工作站甲板的位置。建议用于所有新工作站和搬迁后的工作站。", + "deck_calibration_error_occurred": "尝试启动甲板校准时发生错误", + "deck_calibration_failure": "无法启动甲板校准", + "deck_calibration_intro_body": "甲板校准确保位置准确性,以便您的工作站按预期移动。它将准确建立OT-2的甲板相对于龙门架的方向。", + "deck_calibration_missing": "您还没有校准甲板", + "deck_calibration_redo": "重新校准甲板", + "deck_calibration_spinner": "甲板校准正在进行{{ongoing_action}}", + "deck_invalidates_pipette_offset": "重新校准甲板将清除移液器校准数据", + "definition": "您的OT-2将根据其校准的数据使移液器在内部空间进行移动。了解更多关于OT-2上校准是如何工作的。", + "delete_calibration_data": "删除校准数据", + "did_pipette_pick_up_tip": "移液器是否成功吸取吸头?", + "direction_controls": "方向控制", + "do_you_have_a_cal_block": "您有校准块吗?", + "download_calibration": "下载您的校准数据", + "download_calibration_data_available": "将所有三种类型的校准数据保存为JSON文件。", + "download_calibration_data_unavailable": "无校准数据可用。", + "download_calibration_title": "下载校准数据", + "download_details": "下载详情JSON校准文件查看摘要", + "finish": "完成", + "get_started": "开始", + "good_calibration": "良好校准", + "health_check_button": "检查健康", + "health_check_description": "检查当前校准设置的健康状况。", + "health_check_title": "校准健康检查", + "if_tip_bent_replace_it": "如果吸头发生了弯折,在继续校准前,请确保用未损坏的吸头替换A1位置的吸头。", + "important_to_use_listed_equipment": "使用上述指定的设备进行校准非常重要,因为工作站将根据这些物品的已知数据来确保准确性。", + "jog_controls": "微调控制", + "jog_nozzle_to_block": "微调移液器,直到喷嘴几乎接触(小于0.1毫米){{slotName}}板位中的校准块。", + "jog_nozzle_to_trash": "微调移液器,直到喷嘴几乎接触(小于0.1毫米)垃圾桶的平坦表面。", + "jog_pipette_to_touch_block": "微调移液器,直到吸头几乎接触(小于0.1毫米)6号板位的校准块。", + "jog_pipette_to_touch_cross": "微调移液器,直到吸头精确地位于{{slotName}}板位的十字中心。", + "jog_pipette_to_touch_slot": "微调移液器,直到吸头几乎接触(小于0.1毫米)5号板位的甲板表面。如果移液器在5号板位的编号或边缘处,或较难看清的位置,请切换到x轴和y轴控制,将移液器移过甲板。", + "jog_pipette_to_touch_trash": "微调移液器,直到吸头几乎接触(小于0.1毫米)垃圾桶的平坦表面。", + "jog_too_far_or_bend_tip": "微调过多或吸头发生了弯折?", + "jump_size": "跳跃尺寸", + "large": "大", + "last_calibrated": "上次校准", + "last_completed_on": "上次完成时间{{timestamp}}", + "last_migrated": "上次已知校准迁移", + "launch_calibration": "启动校准", + "launch_calibration_link_text": "前往校准", + "manage_pipettes": "管理移液器", + "missing_calibration_data": "缺少校准数据", + "missing_calibration_data_long": "工作站缺少校准数据", + "need_help": "需要帮助?", + "no_pipette": "没有连接移液器", + "no_tip_length": "校准您的移液器以查看保存的吸头长度", + "pick_up_tip": "拾取吸头", + "pipette_name_and_serial": "{{name}},{{serial}}", + "pipette_offset_calibration": "移液器偏移校准", + "pipette_offset_calibration_intro_body": "校准移液器偏移数据将测量移液器与移液器支架和甲板的相对位置。", + "pipette_offset_calibration_on_mount": "当移液器连接到工作站的{{mount}}支架上时,校准此移液器的偏移数据。", + "pipette_offset_description": "校准移液器拾取默认吸头后的位置。", + "pipette_offset_recalibrate_both_mounts": "两个支架的移液器偏移数据都需要重新校准。", + "pipette_offset_requires_tip_length": "您还没有为此移液器保存吸头长度数据。在校准移液器偏移数据之前,您需要校准吸头长度。", + "pipette_offset_title": "移液器偏移校准", + "place_cal_block": "将校准块放入指定板位", + "place_full_tip_rack": "在8号板位中放置一盒完整的{{tip_rack}}", + "position_pipette_over_tip": "将移液器定位在A1上方", + "prepare_the_space": "预留足够空间", + "progress_will_be_lost": "{{sessionType}}进度将丢失", + "recalibrate": "重新校准", + "recalibrate_pipette": "重新校准移液器", + "recalibrate_warning_body": "执行甲板校准将清除所有移液器偏移数据和吸头长度数据。在完成甲板校准后,您需要重新校准移液器偏移数据和吸头长度。", + "recalibrate_warning_heading": "您确定要重新校准甲板吗?", + "recalibration_recommended": "建议重新校准", + "return_tip": "回收吸头", + "return_tip_and_continue": "回收吸头并继续下一把移液器", + "return_tip_and_exit": "回收吸头并查看校准健康检查结果", + "see_how_robot_calibration_works": "了解工作站校准是如何工作的", + "select_tip_rack": "选择吸头盒", + "serial_number": "序列号", + "small": "小", + "start_over": "重新开始", + "start_over_question": "重新开始?", + "start_with_deck_calibration": "从甲板校准开始,这是其他校准的基础。", + "starting_over_loses_progress": "重新开始将取消您的校准进度。", + "this_is_the_tip_used_in_pipette_offset_cal": "请注意:您必须使用与上面列出的移液器偏移校准中使用的相同吸头。", + "tiny": "微小", + "tip_length": "吸头长度校准", + "tip_length_and_pipette_offset_calibration": "吸头长度和移液器偏移校准", + "tip_length_calibration": "吸头长度校准", + "tip_length_calibration_intro_body": "吸头长度校准将测量吸头底部和移液器喷嘴之间的距离。", + "tip_length_invalidates_pipette_offset": "重新校准吸头长度将清除移液器偏移数据。", + "tip_pick_up_instructions": "使用下方的控制键或键盘,微调移液器,直到最近的喷嘴位于A1位置的正上方,并与吸头顶部平齐。
当移液器正确对齐时,吸取吸头。", + "title": "工作站校准", + "to_check": "检查{{mount}}移液器:", + "unknown_custom_tiprack": "未知的自定义吸头盒", + "use_calibration_block": "使用校准块", + "use_trash_bin": "使用垃圾桶", + "using_current_calibrations": "使用当前校准。", + "you_can_remove_cal_block": "您现在可以从甲板上移除校准块。", + "you_will_need": "您将需要:" +} diff --git a/app/src/assets/localization/zh/robot_controls.json b/app/src/assets/localization/zh/robot_controls.json new file mode 100644 index 00000000000..ecb31d2adf6 --- /dev/null +++ b/app/src/assets/localization/zh/robot_controls.json @@ -0,0 +1,14 @@ +{ + "confirm_location": "确认位置", + "drop_tips": "丢弃吸头", + "home_button": "归位", + "home_description": "将工作站返回起始位置。", + "home_label": "归位所有轴", + "lights_description": "控制灯光", + "lights_label": "灯光", + "recalibrate": "重新校准", + "restart_button": "重新启动", + "restart_description": "重新启动工作站。", + "restart_label": "重新启动工作站", + "title": "工作站控制" +} diff --git a/app/src/assets/localization/zh/run_details.json b/app/src/assets/localization/zh/run_details.json new file mode 100644 index 00000000000..648df8a66e2 --- /dev/null +++ b/app/src/assets/localization/zh/run_details.json @@ -0,0 +1,154 @@ +{ + "analysis_failure_on_robot": "尝试在{{robotName}}上分析{{protocolName}}时发生错误。请修复以下错误,然后再次尝试运行此协议。", + "analyzing_on_robot": "移液工作站分析中", + "anticipated_step": "预期步骤", + "anticipated": "预期步骤", + "apply_stored_data": "应用存储的数据", + "apply_stored_labware_offset_data": "应用已储存的耗材校准数据?", + "cancel_run_alert_info_flex": "该动作将终止本次运行并使移液器归位。", + "cancel_run_alert_info_ot2": "该动作将终止本次运行,已拾取的吸头将被丢弃,移液器将归位。", + "cancel_run_and_restart": "取消运行,重新进行设置以进行编辑", + "cancel_run_modal_back": "否,返回", + "cancel_run_modal_confirm": "是,取消运行", + "cancel_run_modal_heading": "确定要取消吗?", + "cancel_run_module_info": "此外,协议中使用的模块将保持激活状态,直到被禁用。", + "cancel_run": "取消运行", + "canceling_run_dot": "正在取消运行...", + "canceling_run": "正在取消运行", + "clear_protocol_to_make_available": "清除工作站的协议以使其可用", + "clear_protocol": "清除协议", + "close_door_to_resume": "关闭移液工作站的前门以继续运行", + "close_door": "关闭移液工作站前门", + "closing_protocol": "正在关闭协议", + "comment_step": "注释", + "comment": "注释", + "complete_protocol_to_download": "完成协议以下载运行日志", + "current_step_pause_timer": "计时器", + "current_step_pause": "当前步骤 - 用户暂停", + "current_step": "当前步骤", + "current_temperature": "当前:{{temperature}}°C", + "custom_values": "自定义值", + "data_out_of_date": "此数据可能已过期", + "date": "日期", + "door_is_open": "工作站前门已打开", + "door_open_pause": "当前步骤 - 暂停 - 前门已打开", + "download": "下载", + "download_run_log": "下载运行日志", + "downloading_run_log": "正在下载运行日志", + "drop_tip": "在{{labware_location}}内的{{labware}}中的{{well_name}}中丢弃吸头", + "duration": "持续时间", + "end_of_protocol": "协议结束", + "end_step_time": "结束", + "end": "结束", + "error_info": "错误{{errorCode}}:{{errorType}}", + "error_type": "错误:{{errorType}}", + "failed_step": "步骤失败", + "final_step": "最后一步", + "ignore_stored_data": "忽略已存储的数据", + "labware_offset_data": "耗材校准数据", + "labware": "耗材", + "left": "左", + "listed_values": "列出的值仅供查看", + "load_labware_info_protocol_setup_adapter_module": "在{{slot_name}}号板位中的{{module_name}}内加载{{labware}}的{{adapter_name}}", + "load_labware_info_protocol_setup_adapter_off_deck": "在甲板外的{{adapter_name}}上加载{{labware}}", + "load_labware_info_protocol_setup_adapter": "在{{slot_name}}号板位中的{{adapter_name}}中加载{{labware}}", + "load_labware_info_protocol_setup_no_module": "在{{slot_name}}号板位中加载{{labware}}", + "load_labware_info_protocol_setup_off_deck": "在甲板外加载{{labware}}", + "load_labware_info_protocol_setup_plural": "在{{module_name}}中加载{{labware}}", + "load_labware_info_protocol_setup": "在{{slot_name}}号板位中的{{module_name}}中加载{{labware}}", + "load_liquids_info_protocol_setup": "将{{liquid}}加载到{{labware}}中", + "load_module_protocol_setup_plural": "加载{{module}}", + "load_module_protocol_setup": "在{{slot_name}}号板位中加载{{module}}", + "load_pipette_protocol_setup": "在{{mount_name}}安装位上加载{{pipette_name}}", + "loading_protocol": "正在加载协议", + "loading_data": "正在加载数据...", + "location": "位置", + "module_controls": "模块控制", + "module_slot_number": "板位{{slot_number}}", + "move_labware": "移动耗材", + "name": "名称", + "no_files_included": "未包含协议文件", + "no_offsets_available": "无耗材校准数据可用", + "not_available_for_a_completed_run": "不适用于已完成的运行", + "not_available_for_a_run_in_progress": "不适用于正在进行的运行", + "not_started_yet": "未开始", + "off_deck": "甲板外", + "parameters": "参数", + "pause_protocol": "暂停协议", + "pause_run": "暂停运行", + "pause": "暂停", + "paused_for": "暂停原因", + "pickup_tip": "从{{labware_location}}内的{{labware}}中的{{well_name}}孔位拾取吸头", + "plus_more": "+{{count}}更多", + "preview_of_protocol_steps": "这是您的协议步骤预览", + "protocol_analysis_failed": "协议分析失败。", + "protocol_analysis_failure": "协议分析失败", + "protocol_completed": "协议已完成", + "protocol_end": "协议结束", + "protocol_files": "协议文件", + "protocol_paused_for": "协议暂停原因", + "protocol_run_canceled": "协议运行已取消", + "protocol_run_complete": "协议运行已完成", + "protocol_run_failed": "协议运行失败", + "protocol_setup": "协议设置", + "protocol_start": "协议开始", + "protocol_steps": "协议步骤", + "protocol_title": "协议 -{{protocol_name}}", + "resume_run": "恢复运行", + "return_to_dashboard": "返回控制面板", + "right": "右", + "robot_has_previous_offsets": "该移液工作站已存储了之前运行协议的耗材校准数据。您想将这些数据应用于此协议的运行吗?您仍然可以通过实验器具位置检查调整校准数据。", + "robot_was_recalibrated": "在储存此耗材校准数据后,移液工作站已重新校准", + "run_again": "再次运行", + "run_canceled_splash": "运行已取消", + "run_canceled": "运行已取消。", + "run_complete_splash": "运行已完成", + "run_complete": "运行已完成", + "run_completed": "运行已完成。", + "run_cta_disabled": "在开始运行之前,请完成协议选项卡上的所有必要步骤。", + "run_failed_modal_body": "在协议执行{{command}}时发生错误。", + "run_failed_modal_header": "{{errorName}}:{{errorCode}}协议步骤{{count}}", + "run_failed_modal_title": "运行失败", + "run_failed_splash": "运行失败", + "run_failed": "运行失败。", + "run_has_diverged_from_predicted": "运行已偏离预期状态。无法执行新的预期步骤。", + "run_preview": "运行预览", + "run_protocol": "运行协议", + "run_status": "状态:{{status}}", + "run_time": "运行时间", + "run": "运行", + "setup_incomplete": "完成“设置”选项卡中所需的步骤", + "setup": "设置", + "slot": "板位{{slotName}}", + "start_run": "开始运行", + "start_step_time": "开始", + "start_time": "开始时间", + "start": "开始", + "status_awaiting-recovery": "等待恢复", + "status_blocked-by-open-door": "暂停 - 前门打开", + "status_failed": "失败", + "status_finishing": "结束中", + "status_idle": "未开始", + "status_paused": "暂停", + "status_running": "运行中", + "status_stop-requested": "请求停止", + "status_stopped": "已取消", + "status_succeeded": "已完成", + "status": "状态", + "step": "步骤", + "step_failed": "步骤失败", + "step_number": "步骤{{step_number}}:", + "steps_total": "总计{{count}}步", + "stored_labware_offset_data": "已储存适用于此协议的耗材校准数据", + "target_temperature": "目标温度:{{temperature}}°C", + "temperature_not_available": "{{temperature_type}}: n/a", + "thermocycler_error_tooltip": "模块遇到异常,请联系技术支持。", + "total_elapsed_time": "总耗时", + "total_step_count_plural": "总计{{count}}步", + "total_step_count": "总计{{count}}步", + "unable_to_determine_steps": "无法确定步骤", + "view_analysis_error_details": "查看 错误详情", + "view_current_step": "查看当前步骤", + "view_error_details": "查看错误详情", + "view_error": "查看错误" +} diff --git a/app/src/assets/localization/zh/shared.json b/app/src/assets/localization/zh/shared.json new file mode 100644 index 00000000000..f9b0aa47cca --- /dev/null +++ b/app/src/assets/localization/zh/shared.json @@ -0,0 +1,84 @@ +{ + "a_software_update_is_available": "此工作站有可用的软件更新。更新以运行协议。", + "add": "添加", + "alphabetical": "按字母排序", + "back": "返回", + "before_you_begin": "在您开始之前", + "browse": "浏览", + "cancel": "取消", + "change_protocol": "更改协议", + "change_robot": "更换工作站", + "clear_data": "清除数据", + "close_robot_door": "开始运行前请关闭工作站前门。", + "close": "关闭", + "confirm_placement": "确认放置", + "confirm_position": "确认位置", + "confirm_values": "确认这些值", + "confirm": "确认", + "continue_activity": "继续活动", + "continue_to_param": "继续设置参数", + "continue": "继续", + "delete": "删除", + "did_pipette_pick_up_tip": "移液器是否成功拾取吸头?", + "disabled_cannot_connect": "无法连接到工作站", + "disabled_connect_to_robot": "连接到工作站以进行控制", + "disabled_no_pipette_attached": "安装移液器以继续", + "disabled_protocol_is_running": "协议正在运行", + "dont_show_me_again": "不再显示", + "drag_and_drop": "拖放或 浏览 您的文件", + "empty": "清空", + "ending": "结束中", + "error_encountered": "遇到错误", + "error": "错误", + "exit": "退出", + "extension_mount": "扩展安装支架", + "flow_complete": "{{flowName}}完成!", + "get_started": "开始", + "github": "GitHub", + "go_back": "返回", + "instruments": "硬件", + "loading": "加载中...", + "next": "下一步", + "no_data": "无数据", + "no": "否", + "none": "无", + "not_used": "未使用", + "off": "关闭", + "ok": "好的", + "on": "开启", + "open": "打开", + "proceed_to_setup": "继续设置", + "protocol_run_general_error_msg": "无法在工作站上创建协议运行。", + "reanalyze": "重新分析", + "refresh_list": "刷新列表", + "refresh": "刷新", + "remember_my_selection_and_do_not_ask_again": "记住我的选择,不再询问", + "reset_all": "全部重置", + "reset": "重置", + "restart": "重新启动", + "resume": "继续", + "return": "返回", + "reverse": "按字母倒序排序", + "robot_is_analyzing": "工作站正在分析", + "robot_is_busy_no_protocol_run_allowed": "此工作站正忙,无法运行此协议。转到工作站", + "robot_is_busy": "工作站正忙", + "robot_is_reachable_but_not_responding": "此工作站的API服务器未能正确响应IP地址{{hostname}}处的请求", + "robot_was_seen_but_is_unreachable": "最近看到此工作站,但当前无法访问IP地址{{hostname}}", + "save": "保存", + "something_went_wrong": "出现问题", + "sort_by": "排序方式", + "stand_back_robot_is_in_motion": "请离开,工作站在运动", + "start": "开始", + "starting": "启动中", + "step": "步骤{{current}}/{{max}}", + "stop": "停止", + "terminate_activity": "终止活动", + "terminate": "终止远程活动", + "try_again": "重试", + "unknown_error": "发生未知错误", + "unknown": "未知", + "update": "更新", + "view_latest_release_notes": "查看最新发布说明:", + "yes": "是", + "you_will_need": "您将需要:" +} diff --git a/app/src/assets/localization/zh/top_navigation.json b/app/src/assets/localization/zh/top_navigation.json new file mode 100644 index 00000000000..c3906955266 --- /dev/null +++ b/app/src/assets/localization/zh/top_navigation.json @@ -0,0 +1,19 @@ +{ + "all_protocols": "全部协议", + "attached_pipettes_do_not_match": "安装的移液器与加载的协议中指定的移液器不匹配", + "calibrate_deck_to_proceed": "校准甲板以继续", + "deck_setup": "甲板设置", + "devices": "设备", + "instruments": "硬件", + "labware": "耗材", + "modules": "模块", + "pipettes_not_calibrated": "请校准加载的协议中指定的所有移液器以继续", + "pipettes": "移液器", + "please_connect_to_a_robot": "请连接到工作站以继续", + "please_load_a_protocol": "请加载协议以继续", + "protocol_runs": "协议运行", + "protocols": "协议", + "robot_settings": "工作站设置", + "run": "运行", + "settings": "设置" +} diff --git a/app/src/pages/Desktop/AppSettings/AdvancedSettings.tsx b/app/src/pages/Desktop/AppSettings/AdvancedSettings.tsx index eb3515016e8..59439a6a088 100644 --- a/app/src/pages/Desktop/AppSettings/AdvancedSettings.tsx +++ b/app/src/pages/Desktop/AppSettings/AdvancedSettings.tsx @@ -1,5 +1,16 @@ -import { Box, SPACING } from '@opentrons/components' +import { css } from 'styled-components' + +import { + Box, + DIRECTION_COLUMN, + Flex, + RadioGroup, + SPACING, + TYPOGRAPHY, +} from '@opentrons/components' + import { Divider } from '/app/atoms/structure' +import { i18n } from '/app/i18n' import { ClearUnavailableRobots, EnableDevTools, @@ -12,6 +23,7 @@ import { UpdatedChannel, AdditionalCustomLabwareSourceFolder, } from '/app/organisms/Desktop/AdvancedSettings' +import { useFeatureFlag } from '/app/redux/config' export function AdvancedSettings(): JSX.Element { return ( @@ -36,7 +48,36 @@ export function AdvancedSettings(): JSX.Element { + {/* TODO(bh, 2024-09-23): remove when localization setting designs implemented */} + ) } + +function LocalizationSetting(): JSX.Element | null { + const enableLocalization = useFeatureFlag('enableLocalization') + + return enableLocalization ? ( + <> + + + ) => { + void i18n.changeLanguage(event.currentTarget.value) + }} + options={[ + { name: 'EN', value: 'en' }, + { name: 'CN', value: 'zh' }, + ]} + /> + + + ) : null +} diff --git a/app/src/pages/ODD/RobotSettingsDashboard/RobotSettingsList.tsx b/app/src/pages/ODD/RobotSettingsDashboard/RobotSettingsList.tsx index 7473410e42d..5983e957419 100644 --- a/app/src/pages/ODD/RobotSettingsDashboard/RobotSettingsList.tsx +++ b/app/src/pages/ODD/RobotSettingsDashboard/RobotSettingsList.tsx @@ -1,5 +1,6 @@ +import { useContext } from 'react' import { useDispatch, useSelector } from 'react-redux' -import { useTranslation } from 'react-i18next' +import { I18nContext, useTranslation } from 'react-i18next' import { Link } from 'react-router-dom' import { @@ -30,6 +31,7 @@ import { toggleDevInternalFlag, toggleDevtools, toggleHistoricOffsets, + useFeatureFlag, } from '/app/redux/config' import { InlineNotification } from '/app/atoms/InlineNotification' import { getRobotSettings, updateSetting } from '/app/redux/robot-settings' @@ -211,6 +213,8 @@ export function RobotSettingsList(props: RobotSettingsListProps): JSX.Element { onClick={() => dispatch(toggleDevtools())} /> {devToolsOn ? : null} + {/* TODO(bh, 2024-09-23): remove when localization setting designs implemented */} +
) @@ -266,3 +270,21 @@ function FeatureFlags(): JSX.Element { ) } + +function LanguageToggle(): JSX.Element | null { + const enableLocalization = useFeatureFlag('enableLocalization') + + const { i18n } = useContext(I18nContext) + + return enableLocalization ? ( + { + void (i18n.language === 'en' + ? i18n.changeLanguage('zh') + : i18n.changeLanguage('en')) + }} + rightElement={<>} + /> + ) : null +} diff --git a/app/src/redux/config/constants.ts b/app/src/redux/config/constants.ts index f77241acaf3..4cd981093fc 100644 --- a/app/src/redux/config/constants.ts +++ b/app/src/redux/config/constants.ts @@ -6,6 +6,7 @@ export const DEV_INTERNAL_FLAGS: DevInternalFlag[] = [ 'enableRunNotes', 'protocolTimeline', 'enableLabwareCreator', + 'enableLocalization', ] // action type constants diff --git a/app/src/redux/config/schema-types.ts b/app/src/redux/config/schema-types.ts index 4dbc4f97fd1..842fb8c3b80 100644 --- a/app/src/redux/config/schema-types.ts +++ b/app/src/redux/config/schema-types.ts @@ -13,6 +13,7 @@ export type DevInternalFlag = | 'enableRunNotes' | 'protocolTimeline' | 'enableLabwareCreator' + | 'enableLocalization' export type FeatureFlags = Partial> diff --git a/app/src/resources/robot-settings/hooks.ts b/app/src/resources/robot-settings/hooks.ts index 6c712bb57bf..594ea59afe0 100644 --- a/app/src/resources/robot-settings/hooks.ts +++ b/app/src/resources/robot-settings/hooks.ts @@ -10,7 +10,8 @@ import type { RobotSettingsField } from '@opentrons/api-client' * @returns boolean */ export function useIsOEMMode(): boolean { - const { settings } = useRobotSettingsQuery().data ?? {} + // set enabled false to avoid refetch that reinitializes localization provider + const { settings } = useRobotSettingsQuery({ enabled: false }).data ?? {} const isOnDevice = useSelector(getIsOnDevice) const oemModeSetting = From 2fb61add58dc17648900128a817251285e495eff Mon Sep 17 00:00:00 2001 From: Rhyann Clarke <146747548+rclarke0@users.noreply.github.com> Date: Mon, 30 Sep 2024 12:10:49 -0400 Subject: [PATCH 06/17] feat(abr-testing): ABR Testing Plate Reader Documentation (#16329) # Overview Semi documentation of plate readers in ABR Testing ## Test Plan and Hands on Testing ## Changelog Ran ABR data collection scripts to ensure all still worked with added documentation. Added file to hold plate reader specific scripts - like reading hellma plate output, ## Review requests ## Risk assessment --- abr-testing/Pipfile | 2 +- abr-testing/Pipfile.lock | 115 +++++++------- .../automation/google_sheets_tool.py | 4 +- .../data_collection/abr_google_drive.py | 20 ++- .../data_collection/abr_robot_error.py | 11 +- .../data_collection/get_run_logs.py | 2 +- .../data_collection/read_robot_logs.py | 64 ++++++++ .../data_collection/single_run_log_reader.py | 11 +- abr-testing/abr_testing/tools/abr_scale.py | 11 +- abr-testing/abr_testing/tools/plate_reader.py | 144 ++++++++++++++++++ 10 files changed, 319 insertions(+), 65 deletions(-) create mode 100644 abr-testing/abr_testing/tools/plate_reader.py diff --git a/abr-testing/Pipfile b/abr-testing/Pipfile index c046e523a69..0ea9e6f76aa 100644 --- a/abr-testing/Pipfile +++ b/abr-testing/Pipfile @@ -18,7 +18,7 @@ slackclient = "*" slack-sdk = "*" pandas = "*" pandas-stubs = "*" - +numpy = "==1.8.3" [dev-packages] atomicwrites = "==1.4.1" diff --git a/abr-testing/Pipfile.lock b/abr-testing/Pipfile.lock index 05e3c72eeda..08da1926e92 100644 --- a/abr-testing/Pipfile.lock +++ b/abr-testing/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "b82b82f6cc192a520ccaa5f2c94a2dda16993ef31ebd9e140f85f80b6a96cc6a" + "sha256": "f2a4a8a95be01ccb8425c8069a3dc8e3f932c1f9b36f5b12c838ee9cfc26015e" }, "pipfile-spec": 6, "requires": { @@ -394,12 +394,12 @@ }, "google-api-python-client": { "hashes": [ - "sha256:8b84dde11aaccadc127e4846f5cd932331d804ea324e353131595e3f25376e97", - "sha256:d74da1358f3f2d63daf3c6f26bd96d89652051183bc87cf10a56ceb2a70beb50" + "sha256:41f671be10fa077ee5143ee9f0903c14006d39dc644564f4e044ae96b380bf68", + "sha256:b1e62c9889c5ef6022f11d30d7ef23dc55100300f0e8aaf8aa09e8e92540acad" ], "index": "pypi", "markers": "python_version >= '3.7'", - "version": "==2.145.0" + "version": "==2.146.0" }, "google-auth": { "hashes": [ @@ -456,11 +456,11 @@ }, "idna": { "hashes": [ - "sha256:050b4e5baadcd44d760cedbd2b8e639f2ff89bbc7a5730fcc662954303377aac", - "sha256:d838c2c0ed6fced7693d5e8ab8e734d5f8fda53a039c0164afb0b82e771e3603" + "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", + "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3" ], "markers": "python_version >= '3.6'", - "version": "==3.8" + "version": "==3.10" }, "jsonschema": { "hashes": [ @@ -607,6 +607,7 @@ "sha256:f870204a840a60da0b12273ef34f7051e98c3b5961b61b0c2c1be6dfd64fbcd3", "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f" ], + "index": "pypi", "markers": "python_version >= '3.9'", "version": "==1.26.4" }, @@ -709,23 +710,24 @@ }, "protobuf": { "hashes": [ - "sha256:018db9056b9d75eb93d12a9d35120f97a84d9a919bcab11ed56ad2d399d6e8dd", - "sha256:510ed78cd0980f6d3218099e874714cdf0d8a95582e7b059b06cabad855ed0a0", - "sha256:532627e8fdd825cf8767a2d2b94d77e874d5ddb0adefb04b237f7cc296748681", - "sha256:6206afcb2d90181ae8722798dcb56dc76675ab67458ac24c0dd7d75d632ac9bd", - "sha256:66c3edeedb774a3508ae70d87b3a19786445fe9a068dd3585e0cefa8a77b83d0", - "sha256:6d7cc9e60f976cf3e873acb9a40fed04afb5d224608ed5c1a105db4a3f09c5b6", - "sha256:853db610214e77ee817ecf0514e0d1d052dff7f63a0c157aa6eabae98db8a8de", - "sha256:d001a73c8bc2bf5b5c1360d59dd7573744e163b3607fa92788b7f3d5fefbd9a5", - "sha256:dde74af0fa774fa98892209992295adbfb91da3fa98c8f67a88afe8f5a349add", - "sha256:dde9fcaa24e7a9654f4baf2a55250b13a5ea701493d904c54069776b99a8216b", - "sha256:eef7a8a2f4318e2cb2dee8666d26e58eaf437c14788f3a2911d0c3da40405ae8" + "sha256:2c69461a7fcc8e24be697624c09a839976d82ae75062b11a0972e41fd2cd9132", + "sha256:35cfcb15f213449af7ff6198d6eb5f739c37d7e4f1c09b5d0641babf2cc0c68f", + "sha256:52235802093bd8a2811abbe8bf0ab9c5f54cca0a751fdd3f6ac2a21438bffece", + "sha256:59379674ff119717404f7454647913787034f03fe7049cbef1d74a97bb4593f0", + "sha256:5e8a95246d581eef20471b5d5ba010d55f66740942b95ba9b872d918c459452f", + "sha256:87317e9bcda04a32f2ee82089a204d3a2f0d3c8aeed16568c7daf4756e4f1fe0", + "sha256:8ddc60bf374785fb7cb12510b267f59067fa10087325b8e1855b898a0d81d276", + "sha256:a8b9403fc70764b08d2f593ce44f1d2920c5077bf7d311fefec999f8c40f78b7", + "sha256:c0ea0123dac3399a2eeb1a1443d82b7afc9ff40241433296769f7da42d142ec3", + "sha256:ca53faf29896c526863366a52a8f4d88e69cd04ec9571ed6082fa117fac3ab36", + "sha256:eeea10f3dc0ac7e6b4933d32db20662902b4ab81bf28df12218aa389e9c2102d" ], "markers": "python_version >= '3.8'", - "version": "==5.28.0" + "version": "==5.28.2" }, "pyasn1": { "hashes": [ + "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034" ], "markers": "python_version >= '3.8'", @@ -733,6 +735,7 @@ }, "pyasn1-modules": { "hashes": [ + "sha256:49bfa96b45a292b711e986f222502c1c9a5e1f4e568fc30e2574a6c7d07838fd", "sha256:c28e2dbf9c06ad61c71a075c7e0f9fd0f1b0bb2d2ad4377f240d33ac2ab60a7c" ], "markers": "python_version >= '3.8'", @@ -917,11 +920,11 @@ }, "setuptools": { "hashes": [ - "sha256:5f4c08aa4d3ebcb57a50c33b1b07e94315d7fc7230f7115e47fc99776c8ce308", - "sha256:95b40ed940a1c67eb70fc099094bd6e99c6ee7c23aa2306f4d2697ba7916f9c6" + "sha256:35ab7fd3bcd95e6b7fd704e4a1539513edad446c097797f2985e0e4b960772f2", + "sha256:d59a21b17a275fb872a9c3dae73963160ae079f1049ed956880cd7c09b120538" ], "markers": "python_version >= '3.8'", - "version": "==74.1.2" + "version": "==75.1.0" }, "six": { "hashes": [ @@ -933,12 +936,12 @@ }, "slack-sdk": { "hashes": [ - "sha256:af8fc4ef1d1cbcecd28d01acf6955a3bb5b13d56f0a43a1b1c7e3b212cc5ec5b", - "sha256:f35e85f2847e6c25cf7c2d1df206ca0ad75556263fb592457bf03cca68ef64bb" + "sha256:070eb1fb355c149a5f80fa0be6eeb5f5588e4ddff4dd76acf060454435cb037e", + "sha256:853bb55154115d080cae342c4099f2ccb559a78ae8d0f5109b49842401a920fa" ], "index": "pypi", "markers": "python_version >= '3.6'", - "version": "==3.32.0" + "version": "==3.33.0" }, "slackclient": { "hashes": [ @@ -975,11 +978,11 @@ }, "types-pytz": { "hashes": [ - "sha256:6810c8a1f68f21fdf0f4f374a432487c77645a0ac0b31de4bf4690cf21ad3981", - "sha256:8335d443310e2db7b74e007414e74c4f53b67452c0cb0d228ca359ccfba59659" + "sha256:4433b5df4a6fc587bbed41716d86a5ba5d832b4378e506f40d34bc9c81df2c24", + "sha256:a1eebf57ebc6e127a99d2fa2ba0a88d2b173784ef9b3defcc2004ab6855a44df" ], "markers": "python_version >= '3.8'", - "version": "==2024.1.0.20240417" + "version": "==2024.2.0.20240913" }, "typing-extensions": { "hashes": [ @@ -1007,11 +1010,11 @@ }, "urllib3": { "hashes": [ - "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472", - "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168" + "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", + "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9" ], "markers": "python_version >= '3.8'", - "version": "==2.2.2" + "version": "==2.2.3" }, "wrapt": { "hashes": [ @@ -1485,12 +1488,12 @@ }, "google-api-python-client": { "hashes": [ - "sha256:8b84dde11aaccadc127e4846f5cd932331d804ea324e353131595e3f25376e97", - "sha256:d74da1358f3f2d63daf3c6f26bd96d89652051183bc87cf10a56ceb2a70beb50" + "sha256:41f671be10fa077ee5143ee9f0903c14006d39dc644564f4e044ae96b380bf68", + "sha256:b1e62c9889c5ef6022f11d30d7ef23dc55100300f0e8aaf8aa09e8e92540acad" ], "index": "pypi", "markers": "python_version >= '3.7'", - "version": "==2.145.0" + "version": "==2.146.0" }, "google-api-python-client-stubs": { "hashes": [ @@ -1535,11 +1538,11 @@ }, "idna": { "hashes": [ - "sha256:050b4e5baadcd44d760cedbd2b8e639f2ff89bbc7a5730fcc662954303377aac", - "sha256:d838c2c0ed6fced7693d5e8ab8e734d5f8fda53a039c0164afb0b82e771e3603" + "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", + "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3" ], "markers": "python_version >= '3.6'", - "version": "==3.8" + "version": "==3.10" }, "iniconfig": { "hashes": [ @@ -1616,11 +1619,11 @@ }, "platformdirs": { "hashes": [ - "sha256:9e5e27a08aa095dd127b9f2e764d74254f482fef22b0970773bfba79d091ab8c", - "sha256:eb1c8582560b34ed4ba105009a4badf7f6f85768b30126f351328507b2beb617" + "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", + "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb" ], "markers": "python_version >= '3.8'", - "version": "==4.3.2" + "version": "==4.3.6" }, "pluggy": { "hashes": [ @@ -1640,20 +1643,20 @@ }, "protobuf": { "hashes": [ - "sha256:018db9056b9d75eb93d12a9d35120f97a84d9a919bcab11ed56ad2d399d6e8dd", - "sha256:510ed78cd0980f6d3218099e874714cdf0d8a95582e7b059b06cabad855ed0a0", - "sha256:532627e8fdd825cf8767a2d2b94d77e874d5ddb0adefb04b237f7cc296748681", - "sha256:6206afcb2d90181ae8722798dcb56dc76675ab67458ac24c0dd7d75d632ac9bd", - "sha256:66c3edeedb774a3508ae70d87b3a19786445fe9a068dd3585e0cefa8a77b83d0", - "sha256:6d7cc9e60f976cf3e873acb9a40fed04afb5d224608ed5c1a105db4a3f09c5b6", - "sha256:853db610214e77ee817ecf0514e0d1d052dff7f63a0c157aa6eabae98db8a8de", - "sha256:d001a73c8bc2bf5b5c1360d59dd7573744e163b3607fa92788b7f3d5fefbd9a5", - "sha256:dde74af0fa774fa98892209992295adbfb91da3fa98c8f67a88afe8f5a349add", - "sha256:dde9fcaa24e7a9654f4baf2a55250b13a5ea701493d904c54069776b99a8216b", - "sha256:eef7a8a2f4318e2cb2dee8666d26e58eaf437c14788f3a2911d0c3da40405ae8" + "sha256:2c69461a7fcc8e24be697624c09a839976d82ae75062b11a0972e41fd2cd9132", + "sha256:35cfcb15f213449af7ff6198d6eb5f739c37d7e4f1c09b5d0641babf2cc0c68f", + "sha256:52235802093bd8a2811abbe8bf0ab9c5f54cca0a751fdd3f6ac2a21438bffece", + "sha256:59379674ff119717404f7454647913787034f03fe7049cbef1d74a97bb4593f0", + "sha256:5e8a95246d581eef20471b5d5ba010d55f66740942b95ba9b872d918c459452f", + "sha256:87317e9bcda04a32f2ee82089a204d3a2f0d3c8aeed16568c7daf4756e4f1fe0", + "sha256:8ddc60bf374785fb7cb12510b267f59067fa10087325b8e1855b898a0d81d276", + "sha256:a8b9403fc70764b08d2f593ce44f1d2920c5077bf7d311fefec999f8c40f78b7", + "sha256:c0ea0123dac3399a2eeb1a1443d82b7afc9ff40241433296769f7da42d142ec3", + "sha256:ca53faf29896c526863366a52a8f4d88e69cd04ec9571ed6082fa117fac3ab36", + "sha256:eeea10f3dc0ac7e6b4933d32db20662902b4ab81bf28df12218aa389e9c2102d" ], "markers": "python_version >= '3.8'", - "version": "==5.28.0" + "version": "==5.28.2" }, "py": { "hashes": [ @@ -1665,6 +1668,7 @@ }, "pyasn1": { "hashes": [ + "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034" ], "markers": "python_version >= '3.8'", @@ -1672,6 +1676,7 @@ }, "pyasn1-modules": { "hashes": [ + "sha256:49bfa96b45a292b711e986f222502c1c9a5e1f4e568fc30e2574a6c7d07838fd", "sha256:c28e2dbf9c06ad61c71a075c7e0f9fd0f1b0bb2d2ad4377f240d33ac2ab60a7c" ], "markers": "python_version >= '3.8'", @@ -1793,11 +1798,11 @@ }, "urllib3": { "hashes": [ - "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472", - "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168" + "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", + "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9" ], "markers": "python_version >= '3.8'", - "version": "==2.2.2" + "version": "==2.2.3" } } } diff --git a/abr-testing/abr_testing/automation/google_sheets_tool.py b/abr-testing/abr_testing/automation/google_sheets_tool.py index c150eb93f5e..beeb141dc9a 100644 --- a/abr-testing/abr_testing/automation/google_sheets_tool.py +++ b/abr-testing/abr_testing/automation/google_sheets_tool.py @@ -225,8 +225,8 @@ def get_sheet_by_name(self, title: str) -> None: def token_check(self) -> None: """Check if credentials are still valid and refresh if expired.""" - if self.credentials.expired: - self.credentials.refresh() # Refresh the credentials + if self.credentials.access_token_expired: + self.gc.login() # Refresh the credentials def get_row_index_with_value(self, some_string: str, col_num: int) -> Any: """Find row index of string by looking in specific column.""" diff --git a/abr-testing/abr_testing/data_collection/abr_google_drive.py b/abr-testing/abr_testing/data_collection/abr_google_drive.py index f1fe6ad0c8f..4556b62800b 100644 --- a/abr-testing/abr_testing/data_collection/abr_google_drive.py +++ b/abr-testing/abr_testing/data_collection/abr_google_drive.py @@ -7,7 +7,7 @@ from abr_testing.data_collection import read_robot_logs from typing import Set, Dict, Any, Tuple, List, Union from abr_testing.automation import google_drive_tool, google_sheets_tool -from abr_testing.tools import sync_abr_sheet +from abr_testing.tools import sync_abr_sheet, plate_reader def get_modules(file_results: Dict[str, str]) -> Dict[str, Any]: @@ -17,6 +17,7 @@ def get_modules(file_results: Dict[str, str]) -> Dict[str, Any]: "temperatureModuleV2", "magneticBlockV1", "thermocyclerModuleV2", + "absorbanceReaderV1", ) all_modules = {key: "" for key in modList} for module in file_results.get("modules", []): @@ -35,6 +36,7 @@ def create_data_dictionary( issue_url: str, plate: str, accuracy: Any, + hellma_plate_standards: List[Dict[str, Any]], ) -> Tuple[List[List[Any]], List[str], List[List[Any]], List[str]]: """Pull data from run files and format into a dictionary.""" runs_and_robots: List[Any] = [] @@ -113,6 +115,9 @@ def create_data_dictionary( hs_dict = read_robot_logs.hs_commands(file_results) tm_dict = read_robot_logs.temperature_module_commands(file_results) pipette_dict = read_robot_logs.instrument_commands(file_results) + plate_reader_dict = read_robot_logs.plate_reader_commands( + file_results, hellma_plate_standards + ) notes = {"Note1": "", "Jira Link": issue_url} plate_measure = { "Plate Measured": plate, @@ -132,6 +137,7 @@ def create_data_dictionary( **hs_dict, **tm_dict, **tc_dict, + **plate_reader_dict, **pipette_dict, **plate_measure, } @@ -181,6 +187,7 @@ def create_data_dictionary( storage_directory = args.storage_directory[0] google_sheet_name = args.google_sheet_name[0] email = args.email[0] + try: credentials_path = os.path.join(storage_directory, "credentials.json") except FileNotFoundError: @@ -203,13 +210,22 @@ def create_data_dictionary( missing_runs_from_gs = read_robot_logs.get_unseen_run_ids( run_ids_on_gd, run_ids_on_gs ) + # Read Hellma Files + file_values = plate_reader.read_hellma_plate_files(storage_directory, 101934) # Add missing runs to google sheet ( transposed_runs_and_robots, headers, transposed_runs_and_lpc, headers_lpc, - ) = create_data_dictionary(missing_runs_from_gs, storage_directory, "", "", "") + ) = create_data_dictionary( + missing_runs_from_gs, + storage_directory, + "", + "", + "", + hellma_plate_standards=file_values, + ) start_row = google_sheet.get_index_row() + 1 print(start_row) google_sheet.batch_update_cells(transposed_runs_and_robots, "A", start_row, "0") diff --git a/abr-testing/abr_testing/data_collection/abr_robot_error.py b/abr-testing/abr_testing/data_collection/abr_robot_error.py index 9f87f7d4c46..3edf9b315c8 100644 --- a/abr-testing/abr_testing/data_collection/abr_robot_error.py +++ b/abr-testing/abr_testing/data_collection/abr_robot_error.py @@ -13,6 +13,7 @@ import re import pandas as pd from statistics import mean, StatisticsError +from abr_testing.tools import plate_reader def compare_current_trh_to_average( @@ -590,13 +591,21 @@ def get_run_error_info_from_robot( except FileNotFoundError: print("Run file not uploaded.") run_id = os.path.basename(error_run_log).split("_")[1].split(".")[0] + # Get hellma readings + file_values = plate_reader.read_hellma_plate_files(storage_directory, 101934) + ( runs_and_robots, headers, runs_and_lpc, headers_lpc, ) = abr_google_drive.create_data_dictionary( - run_id, error_folder_path, issue_url, "", "" + run_id, + error_folder_path, + issue_url, + "", + "", + hellma_plate_standards=file_values, ) start_row = google_sheet.get_index_row() + 1 diff --git a/abr-testing/abr_testing/data_collection/get_run_logs.py b/abr-testing/abr_testing/data_collection/get_run_logs.py index cc36acd5760..3d8eb851197 100644 --- a/abr-testing/abr_testing/data_collection/get_run_logs.py +++ b/abr-testing/abr_testing/data_collection/get_run_logs.py @@ -47,7 +47,7 @@ def get_run_data(one_run: Any, ip: str) -> Dict[str, Any]: params={"cursor": cursor, "pageLength": page_length}, ) command_data = response.json() - commands.extend(command_data["data"]) + commands.extend(command_data.get("data", "")) run["commands"] = commands response = requests.get( f"http://{ip}:31950/runs/{one_run}", headers={"opentrons-version": "3"} diff --git a/abr-testing/abr_testing/data_collection/read_robot_logs.py b/abr-testing/abr_testing/data_collection/read_robot_logs.py index 96182609c49..3501a330a70 100644 --- a/abr-testing/abr_testing/data_collection/read_robot_logs.py +++ b/abr-testing/abr_testing/data_collection/read_robot_logs.py @@ -14,6 +14,7 @@ import json import requests import sys +from abr_testing.tools import plate_reader def lpc_data( @@ -168,6 +169,69 @@ def instrument_commands(file_results: Dict[str, Any]) -> Dict[str, float]: return pipette_dict +def plate_reader_commands( + file_results: Dict[str, Any], hellma_plate_standards: List[Dict[str, Any]] +) -> Dict[str, object]: + """Plate Reader Command Counts.""" + commandData = file_results.get("commands", "") + move_lid_count: int = 0 + initialize_count: int = 0 + read = "no" + final_result = {} + # Count Number of Reads + read_count, avg_read_time = count_command_in_run_data( + commandData, "absorbanceReader/read", True + ) + # Count Number of Initializations + initialize_count, avg_initialize_time = count_command_in_run_data( + commandData, "absorbanceReader/initialize", True + ) + # Count Number of Lid Movements + for command in commandData: + commandType = command["commandType"] + if ( + commandType == "absorbanceReader/openLid" + or commandType == "absorbanceReader/closeLid" + ): + move_lid_count += 1 + elif commandType == "absorbanceReader/read": + read = "yes" + elif read == "yes" and commandType == "comment": + result = command["params"].get("message", "") + wavelength = result.split("result: {")[1].split(":")[0] + wavelength_str = wavelength + ": " + rest_of_string = result.split(wavelength_str)[1][:-1] + result_dict = eval(rest_of_string) + result_ndarray = plate_reader.convert_read_dictionary_to_array(result_dict) + for item in hellma_plate_standards: + wavelength_of_interest = item["wavelength"] + if str(wavelength) == str(wavelength_of_interest): + error_cells = plate_reader.check_byonoy_data_accuracy( + result_ndarray, item, False + ) + if len(error_cells[0]) > 0: + percent = (96 - len(error_cells)) / 96 * 100 + for cell in error_cells: + print("FAIL: Cell " + str(cell) + " out of accuracy spec.") + else: + percent = 100 + print( + f"PASS: {wavelength_of_interest} meet accuracy specification" + ) + final_result[wavelength] = percent + input("###########################") + read = "no" + plate_dict = { + "Plate Reader # of Reads": read_count, + "Plate Reader Avg Read Time (sec)": avg_read_time, + "Plate Reader # of Initializations": initialize_count, + "Plate Reader Avg Initialize Time (sec)": avg_initialize_time, + "Plate Reader # of Lid Movements": move_lid_count, + "Plate Reader Result": final_result, + } + return plate_dict + + def hs_commands(file_results: Dict[str, Any]) -> Dict[str, float]: """Gets total latch engagements, homes, rotations and total on time (sec) for heater shaker.""" # TODO: modify for cases that have more than 1 heater shaker. diff --git a/abr-testing/abr_testing/data_collection/single_run_log_reader.py b/abr-testing/abr_testing/data_collection/single_run_log_reader.py index 5304842b550..39060529c89 100644 --- a/abr-testing/abr_testing/data_collection/single_run_log_reader.py +++ b/abr-testing/abr_testing/data_collection/single_run_log_reader.py @@ -5,6 +5,7 @@ import csv from abr_testing.data_collection import read_robot_logs from abr_testing.data_collection import abr_google_drive +from abr_testing.tools import plate_reader if __name__ == "__main__": parser = argparse.ArgumentParser(description="Read single run log locally saved.") @@ -25,13 +26,21 @@ sys.exit() # Get Runs from Storage and Read Logs run_ids_in_storage = read_robot_logs.get_run_ids_from_storage(run_log_file_path) + # Get hellma readins + file_values = plate_reader.read_hellma_plate_files(run_log_file_path, 101934) + ( runs_and_robots, header, runs_and_lpc, lpc_headers, ) = abr_google_drive.create_data_dictionary( - run_ids_in_storage, run_log_file_path, "", "", "" + run_ids_in_storage, + run_log_file_path, + "", + "", + "", + hellma_plate_standards=file_values, ) transposed_list = list(zip(*runs_and_robots)) # Adds Run to local csv diff --git a/abr-testing/abr_testing/tools/abr_scale.py b/abr-testing/abr_testing/tools/abr_scale.py index d02bf0acfed..0f6c29c3f69 100644 --- a/abr-testing/abr_testing/tools/abr_scale.py +++ b/abr-testing/abr_testing/tools/abr_scale.py @@ -10,6 +10,7 @@ from typing import Any, Tuple import sys import json +from abr_testing.tools import plate_reader def get_protocol_step_as_int( @@ -141,14 +142,20 @@ def get_most_recent_run_and_record( ) # Record run to google sheets. print(most_recent_run_id) - + # Read Hellma Files + hellma_file_values = plate_reader.read_hellma_plate_files(storage_directory, 101934) ( runs_and_robots, headers, runs_and_lpc, headers_lpc, ) = abr_google_drive.create_data_dictionary( - most_recent_run_id, storage_directory, "", labware, accuracy + most_recent_run_id, + storage_directory, + "", + labware, + accuracy, + hellma_plate_standards=hellma_file_values, ) google_sheet_abr_data = google_sheets_tool.google_sheet( credentials_path, "ABR-run-data", tab_number=0 diff --git a/abr-testing/abr_testing/tools/plate_reader.py b/abr-testing/abr_testing/tools/plate_reader.py new file mode 100644 index 00000000000..3be15fa4268 --- /dev/null +++ b/abr-testing/abr_testing/tools/plate_reader.py @@ -0,0 +1,144 @@ +"""Plate Reader Functions.""" +import argparse +import os +import numpy as np +from typing import Dict, Any, List + + +def convert_read_dictionary_to_array(read_data: Dict[str, Any]) -> np.ndarray: + """Convert a dictionary of read results to an array. + + Converts a dictionary of OD values, as formatted by the Opentrons API's + plate reader read() function, to a 2D numpy.array of shape (8,12) for + further processing. + + read_data: dict + a dictonary of read values with celll numbers for keys, e.g. 'A1' + """ + data = np.empty((8, 12)) + for key, value in read_data.items(): + row_index = ord(key[0]) - ord("A") + column_index = int(key[1:]) - 1 + data[row_index][column_index] = value + + return data + + +def check_byonoy_data_accuracy( + output: Any, cal: Dict[str, np.ndarray], flipped: bool +) -> List[Any]: + """Check multiple OD measurements for accuracy. + + od_list: list of 2D numpy.array of shape (8,12) + a list of multiple plate readings as returned by read_byonoy_directory_to_list() + cal: namedtuple + 2D numpy.array of shape (8,12) of calibration values, and 1D + numpy.array of tolerances, as returned by read_byonoy_file_to_array + flipped: bool + True if reference plate was rotated 180 degrees for measurment + """ + print("entered analysis") + run_error_cells = [] + cal_data = cal["data"] + cal_tolerance = cal["tolerance"] + # Calculate absolute accuracy tolerances for each cell + # The last two columns have a higher tolerance per the Byonoy datasheet + # because OD>2.0 and wavelength>=450nm on the Hellma plate + output_array = np.asarray(output) + accuracy_tols = np.zeros((8, 12)) + accuracy_tols[:, :10] = cal_data[:, :10] * 0.01 + cal_tolerance[:10] + 0.01 + accuracy_tols[:, 10:] = cal_data[:, 10:] * 0.015 + cal_tolerance[10:] + 0.01 + if flipped: + within_tolerance = np.isclose( + output_array, + np.rot90(cal_data, 2), + atol=np.rot90(accuracy_tols, 2), + ) # type: ignore + else: + within_tolerance = np.isclose(output_array, cal_data, atol=accuracy_tols) # type: ignore + errors = np.where(within_tolerance is False) + error_cells = [ + (chr(ord("@") + errors[0][i] + 1) + str(errors[1][i] + 1)) + for i in range(0, len(errors[0])) + ] + run_error_cells.append(error_cells) + return run_error_cells + + +def read_byonoy_file_to_array(filename: str) -> Dict[str, Any]: + """Read a Byonoy endpoint CSV file into a numpy array. + + Returns a named tuple with a 2D numpy array of shape (8,12) of OD values + from a Byonoy endpoint CSV file and a 1D numpy array of tolerances (which + are present in the reference plate calibration data files). + + filename: str + absolute path and filename of the CSV file to be read + """ + wavelength = filename.split("nm.csv")[0].split("_")[-1] + with open(filename, "r") as f: + # print(filename) + + f.seek(0) + file_data = np.genfromtxt( + f, usecols=range(1, 13), skip_header=1, max_rows=8, delimiter="," + ) + # print(file_data.shape, file_data) + + f.seek(0) + file_tolerance = np.genfromtxt( + f, usecols=range(1, 13), skip_header=9, max_rows=1, delimiter="," + ) + # print(file_tolerance.shape, file_tolerance) + + File_Values = { + "wavelength": wavelength, + "data": file_data, + "tolerance": file_tolerance, + } + return File_Values + + +def read_hellma_plate_files( + storage_directory: str, hellma_plate_number: int +) -> List[Any]: + """Read hellma files for the following wavelengths.""" + wavelengths = [405, 450, 490, 650] + file_values = [] + for wave in wavelengths: + file_name = "_".join(["hellma", str(hellma_plate_number), str(wave)]) + file_name_csv = file_name + "nm.csv" + try: + file_path = os.path.join(storage_directory, file_name_csv) + File_Values = read_byonoy_file_to_array(file_path) + file_values.append(File_Values) + except FileNotFoundError: + print( + f"Hellma plate {hellma_plate_number} file at {wave} does not exist in this folder." + ) + continue + return file_values + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Read storage directory with hellma plate files." + ) + parser.add_argument( + "storage_directory", + metavar="STORAGE_DIRECTORY", + type=str, + nargs=1, + help="Path to storage directory for hellma plate files.", + ) + parser.add_argument( + "hellma_plate_number", + metavar="HELLMA_PLATE_NUMBER", + type=int, + nargs=1, + help="Hellma Plate Number.", + ) + args = parser.parse_args() + storage_directory = args.storage_directory[0] + hellma_plate_number = args.hellma_plate_number[0] + read_hellma_plate_files(storage_directory, hellma_plate_number) From f4056d0bf4fffb3e600d114c086fd01e15a38490 Mon Sep 17 00:00:00 2001 From: koji Date: Mon, 30 Sep 2024 12:32:29 -0400 Subject: [PATCH 07/17] chore: update vite version 5.3.2 (#15739) * chore: update vite version 5.3.2 --- .eslintignore | 2 +- .../{vite.config.ts => vite.config.mts} | 0 app-shell/{vite.config.ts => vite.config.mts} | 2 +- app/{vite.config.ts => vite.config.mts} | 0 .../{vite.config.ts => vite.config.mts} | 0 .../{vite.config.ts => vite.config.mts} | 0 .../{vite.config.ts => vite.config.mts} | 0 .../{vite.config.ts => vite.config.mts} | 0 opentrons-ai-client/tsconfig-data.json | 2 +- .../{vite.config.ts => vite.config.mts} | 0 package.json | 4 +- protocol-designer/vite.config.ts | 7 +- shared-data/tsconfig.json | 2 +- .../{vite.config.ts => vite.config.mts} | 0 tsconfig-eslint.json | 1 + vite.config.ts => vite.config.mts | 0 vitest.config.ts | 2 +- yarn.lock | 157 +++++++++++++++++- 18 files changed, 168 insertions(+), 11 deletions(-) rename app-shell-odd/{vite.config.ts => vite.config.mts} (100%) rename app-shell/{vite.config.ts => vite.config.mts} (98%) rename app/{vite.config.ts => vite.config.mts} (100%) rename components/{vite.config.ts => vite.config.mts} (100%) rename discovery-client/{vite.config.ts => vite.config.mts} (100%) rename labware-designer/{vite.config.ts => vite.config.mts} (100%) rename labware-library/{vite.config.ts => vite.config.mts} (100%) rename opentrons-ai-client/{vite.config.ts => vite.config.mts} (100%) rename shared-data/{vite.config.ts => vite.config.mts} (100%) rename vite.config.ts => vite.config.mts (100%) diff --git a/.eslintignore b/.eslintignore index 391b44cdd89..1eb52b86b10 100644 --- a/.eslintignore +++ b/.eslintignore @@ -6,7 +6,7 @@ **/venv/** .opentrons_config **/tsconfig*.json -**/vite.config.ts +**/vite.config.mts # prettier **/package.json **/CHANGELOG.md diff --git a/app-shell-odd/vite.config.ts b/app-shell-odd/vite.config.mts similarity index 100% rename from app-shell-odd/vite.config.ts rename to app-shell-odd/vite.config.mts diff --git a/app-shell/vite.config.ts b/app-shell/vite.config.mts similarity index 98% rename from app-shell/vite.config.ts rename to app-shell/vite.config.mts index 546fe19e23f..63862287fc1 100644 --- a/app-shell/vite.config.ts +++ b/app-shell/vite.config.mts @@ -34,7 +34,7 @@ export default defineConfig( esbuildOptions: { target: 'CommonJs', }, - exclude: ['node_modules'] + exclude: ['node_modules'], }, define: { 'process.env': process.env, diff --git a/app/vite.config.ts b/app/vite.config.mts similarity index 100% rename from app/vite.config.ts rename to app/vite.config.mts diff --git a/components/vite.config.ts b/components/vite.config.mts similarity index 100% rename from components/vite.config.ts rename to components/vite.config.mts diff --git a/discovery-client/vite.config.ts b/discovery-client/vite.config.mts similarity index 100% rename from discovery-client/vite.config.ts rename to discovery-client/vite.config.mts diff --git a/labware-designer/vite.config.ts b/labware-designer/vite.config.mts similarity index 100% rename from labware-designer/vite.config.ts rename to labware-designer/vite.config.mts diff --git a/labware-library/vite.config.ts b/labware-library/vite.config.mts similarity index 100% rename from labware-library/vite.config.ts rename to labware-library/vite.config.mts diff --git a/opentrons-ai-client/tsconfig-data.json b/opentrons-ai-client/tsconfig-data.json index 79a9673faa9..78a16abdc2f 100644 --- a/opentrons-ai-client/tsconfig-data.json +++ b/opentrons-ai-client/tsconfig-data.json @@ -7,6 +7,6 @@ "rootDir": ".", "outDir": "lib" }, - "include": ["src/**/*.json", "fixtures/**/*.json", "vite.config.ts"], + "include": ["src/**/*.json", "fixtures/**/*.json", "vite.config.mts"], "exclude": ["**/*.ts", "**/*.tsx"] } diff --git a/opentrons-ai-client/vite.config.ts b/opentrons-ai-client/vite.config.mts similarity index 100% rename from opentrons-ai-client/vite.config.ts rename to opentrons-ai-client/vite.config.mts diff --git a/package.json b/package.json index 5c088dd44f6..42d21bfc8e9 100755 --- a/package.json +++ b/package.json @@ -137,7 +137,7 @@ "rollup-plugin-terser": "^7.0.2", "script-ext-html-webpack-plugin": "^2.1.4", "semver": "^7.3.8", - "shx": "^0.3.3", + "shx": "^0.3.4", "simple-git": "^3.15.1", "storybook": "^7.6.16", "storybook-addon-pseudo-states": "2.0.0", @@ -149,7 +149,7 @@ "terser-webpack-plugin": "^2.3.5", "typescript": "5.3.3", "url-loader": "^2.1.0", - "vite": "5.0.5", + "vite": "5.3.2", "vitest": "1.2.2", "vitest-when": "0.3.1", "wait-on": "^4.0.2", diff --git a/protocol-designer/vite.config.ts b/protocol-designer/vite.config.ts index 3c5acc81248..6f26d077aa6 100644 --- a/protocol-designer/vite.config.ts +++ b/protocol-designer/vite.config.ts @@ -9,7 +9,7 @@ import lostCss from 'lost' import { versionForProject } from '../scripts/git-version.mjs' import type { UserConfig } from 'vite' -const testAliases: {} | { 'file-saver': string } = +const testAliases: Record | { 'file-saver': string } = process.env.CYPRESS === '1' ? { 'file-saver': @@ -17,6 +17,7 @@ const testAliases: {} | { 'file-saver': string } = } : {} +// eslint-disable-next-line import/no-default-export export default defineConfig( async (): Promise => { const OT_PD_VERSION = await versionForProject('protocol-designer') @@ -71,8 +72,8 @@ export default defineConfig( }, }, server: { - port: 5178 - } + port: 5178, + }, } } ) diff --git a/shared-data/tsconfig.json b/shared-data/tsconfig.json index 03d6a477357..a50e215ee95 100644 --- a/shared-data/tsconfig.json +++ b/shared-data/tsconfig.json @@ -19,6 +19,6 @@ "errors", "liquid/types", "commandAnnotation/types", - "vite.config.ts" + "vite.config.mts" ] } diff --git a/shared-data/vite.config.ts b/shared-data/vite.config.mts similarity index 100% rename from shared-data/vite.config.ts rename to shared-data/vite.config.mts diff --git a/tsconfig-eslint.json b/tsconfig-eslint.json index 4fa07c72874..8c6a70e9c51 100644 --- a/tsconfig-eslint.json +++ b/tsconfig-eslint.json @@ -39,6 +39,7 @@ "usb-bridge/node-client/src", "**/*.js", "**/*.ts", + "**/*.mts", "*.js", ".*.js", "**/*.json" diff --git a/vite.config.ts b/vite.config.mts similarity index 100% rename from vite.config.ts rename to vite.config.mts diff --git a/vitest.config.ts b/vitest.config.ts index d311b1485c2..a485e7536bd 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -3,7 +3,7 @@ /// import path from 'path' import { configDefaults, defineConfig, mergeConfig } from 'vitest/config' -import viteConfig from './vite.config' +import viteConfig from './vite.config.mts' // eslint-disable-next-line import/no-default-export export default mergeConfig( diff --git a/yarn.lock b/yarn.lock index 161b5594197..98d25011f2e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2390,6 +2390,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz#a70f4ac11c6a1dfc18b8bbb13284155d933b9537" integrity sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g== +"@esbuild/aix-ppc64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz#c7184a326533fcdf1b8ee0733e21c713b975575f" + integrity sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ== + "@esbuild/android-arm64@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz#984b4f9c8d0377443cc2dfcef266d02244593622" @@ -2405,6 +2410,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz#db1c9202a5bc92ea04c7b6840f1bbe09ebf9e6b9" integrity sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg== +"@esbuild/android-arm64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz#09d9b4357780da9ea3a7dfb833a1f1ff439b4052" + integrity sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A== + "@esbuild/android-arm@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.18.20.tgz#fedb265bc3a589c84cc11f810804f234947c3682" @@ -2420,6 +2430,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.20.2.tgz#3b488c49aee9d491c2c8f98a909b785870d6e995" integrity sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w== +"@esbuild/android-arm@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz#9b04384fb771926dfa6d7ad04324ecb2ab9b2e28" + integrity sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg== + "@esbuild/android-x64@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.18.20.tgz#35cf419c4cfc8babe8893d296cd990e9e9f756f2" @@ -2435,6 +2450,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.20.2.tgz#3b1628029e5576249d2b2d766696e50768449f98" integrity sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg== +"@esbuild/android-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz#29918ec2db754cedcb6c1b04de8cd6547af6461e" + integrity sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA== + "@esbuild/darwin-arm64@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz#08172cbeccf95fbc383399a7f39cfbddaeb0d7c1" @@ -2450,6 +2470,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz#6e8517a045ddd86ae30c6608c8475ebc0c4000bb" integrity sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA== +"@esbuild/darwin-arm64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz#e495b539660e51690f3928af50a76fb0a6ccff2a" + integrity sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ== + "@esbuild/darwin-x64@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz#d70d5790d8bf475556b67d0f8b7c5bdff053d85d" @@ -2465,6 +2490,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz#90ed098e1f9dd8a9381695b207e1cff45540a0d0" integrity sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA== +"@esbuild/darwin-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz#c13838fa57372839abdddc91d71542ceea2e1e22" + integrity sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw== + "@esbuild/freebsd-arm64@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz#98755cd12707f93f210e2494d6a4b51b96977f54" @@ -2480,6 +2510,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz#d71502d1ee89a1130327e890364666c760a2a911" integrity sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw== +"@esbuild/freebsd-arm64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz#646b989aa20bf89fd071dd5dbfad69a3542e550e" + integrity sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g== + "@esbuild/freebsd-x64@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz#c1eb2bff03915f87c29cece4c1a7fa1f423b066e" @@ -2495,6 +2530,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz#aa5ea58d9c1dd9af688b8b6f63ef0d3d60cea53c" integrity sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw== +"@esbuild/freebsd-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz#aa615cfc80af954d3458906e38ca22c18cf5c261" + integrity sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ== + "@esbuild/linux-arm64@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz#bad4238bd8f4fc25b5a021280c770ab5fc3a02a0" @@ -2510,6 +2550,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz#055b63725df678379b0f6db9d0fa85463755b2e5" integrity sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A== +"@esbuild/linux-arm64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz#70ac6fa14f5cb7e1f7f887bcffb680ad09922b5b" + integrity sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q== + "@esbuild/linux-arm@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz#3e617c61f33508a27150ee417543c8ab5acc73b0" @@ -2525,6 +2570,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz#76b3b98cb1f87936fbc37f073efabad49dcd889c" integrity sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg== +"@esbuild/linux-arm@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz#fc6fd11a8aca56c1f6f3894f2bea0479f8f626b9" + integrity sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA== + "@esbuild/linux-ia32@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz#699391cccba9aee6019b7f9892eb99219f1570a7" @@ -2540,6 +2590,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz#c0e5e787c285264e5dfc7a79f04b8b4eefdad7fa" integrity sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig== +"@esbuild/linux-ia32@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz#3271f53b3f93e3d093d518d1649d6d68d346ede2" + integrity sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg== + "@esbuild/linux-loong64@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz#e6fccb7aac178dd2ffb9860465ac89d7f23b977d" @@ -2555,6 +2610,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz#a6184e62bd7cdc63e0c0448b83801001653219c5" integrity sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ== +"@esbuild/linux-loong64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz#ed62e04238c57026aea831c5a130b73c0f9f26df" + integrity sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg== + "@esbuild/linux-mips64el@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz#eeff3a937de9c2310de30622a957ad1bd9183231" @@ -2570,6 +2630,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz#d08e39ce86f45ef8fc88549d29c62b8acf5649aa" integrity sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA== +"@esbuild/linux-mips64el@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz#e79b8eb48bf3b106fadec1ac8240fb97b4e64cbe" + integrity sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg== + "@esbuild/linux-ppc64@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz#2f7156bde20b01527993e6881435ad79ba9599fb" @@ -2585,6 +2650,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz#8d252f0b7756ffd6d1cbde5ea67ff8fd20437f20" integrity sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg== +"@esbuild/linux-ppc64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz#5f2203860a143b9919d383ef7573521fb154c3e4" + integrity sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w== + "@esbuild/linux-riscv64@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz#6628389f210123d8b4743045af8caa7d4ddfc7a6" @@ -2600,6 +2670,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz#19f6dcdb14409dae607f66ca1181dd4e9db81300" integrity sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg== +"@esbuild/linux-riscv64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz#07bcafd99322d5af62f618cb9e6a9b7f4bb825dc" + integrity sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA== + "@esbuild/linux-s390x@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz#255e81fb289b101026131858ab99fba63dcf0071" @@ -2615,6 +2690,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz#3c830c90f1a5d7dd1473d5595ea4ebb920988685" integrity sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ== +"@esbuild/linux-s390x@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz#b7ccf686751d6a3e44b8627ababc8be3ef62d8de" + integrity sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A== + "@esbuild/linux-x64@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz#c7690b3417af318a9b6f96df3031a8865176d338" @@ -2630,6 +2710,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz#86eca35203afc0d9de0694c64ec0ab0a378f6fff" integrity sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw== +"@esbuild/linux-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz#6d8f0c768e070e64309af8004bb94e68ab2bb3b0" + integrity sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ== + "@esbuild/netbsd-x64@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz#30e8cd8a3dded63975e2df2438ca109601ebe0d1" @@ -2645,6 +2730,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz#e771c8eb0e0f6e1877ffd4220036b98aed5915e6" integrity sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ== +"@esbuild/netbsd-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz#bbe430f60d378ecb88decb219c602667387a6047" + integrity sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg== + "@esbuild/openbsd-x64@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz#7812af31b205055874c8082ea9cf9ab0da6217ae" @@ -2660,6 +2750,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz#9a795ae4b4e37e674f0f4d716f3e226dd7c39baf" integrity sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ== +"@esbuild/openbsd-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz#99d1cf2937279560d2104821f5ccce220cb2af70" + integrity sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow== + "@esbuild/sunos-x64@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz#d5c275c3b4e73c9b0ecd38d1ca62c020f887ab9d" @@ -2675,6 +2770,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz#7df23b61a497b8ac189def6e25a95673caedb03f" integrity sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w== +"@esbuild/sunos-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz#08741512c10d529566baba837b4fe052c8f3487b" + integrity sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg== + "@esbuild/win32-arm64@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz#73bc7f5a9f8a77805f357fab97f290d0e4820ac9" @@ -2690,6 +2790,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz#f1ae5abf9ca052ae11c1bc806fb4c0f519bacf90" integrity sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ== +"@esbuild/win32-arm64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz#675b7385398411240735016144ab2e99a60fc75d" + integrity sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A== + "@esbuild/win32-ia32@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz#ec93cbf0ef1085cc12e71e0d661d20569ff42102" @@ -2705,6 +2810,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz#241fe62c34d8e8461cd708277813e1d0ba55ce23" integrity sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ== +"@esbuild/win32-ia32@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz#1bfc3ce98aa6ca9a0969e4d2af72144c59c1193b" + integrity sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA== + "@esbuild/win32-x64@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz#786c5f41f043b07afb1af37683d7c33668858f6d" @@ -2720,6 +2830,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz#9c907b21e30a52db959ba4f80bb01a0cc403d5cc" integrity sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ== +"@esbuild/win32-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz#acad351d582d157bb145535db2a6ff53dd514b5c" + integrity sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw== + "@eslint-community/eslint-utils@^4.1.2", "@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0": version "4.4.0" resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" @@ -10648,6 +10763,35 @@ esbuild@^0.20.1: "@esbuild/win32-ia32" "0.20.2" "@esbuild/win32-x64" "0.20.2" +esbuild@^0.21.3: + version "0.21.5" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.21.5.tgz#9ca301b120922959b766360d8ac830da0d02997d" + integrity sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw== + optionalDependencies: + "@esbuild/aix-ppc64" "0.21.5" + "@esbuild/android-arm" "0.21.5" + "@esbuild/android-arm64" "0.21.5" + "@esbuild/android-x64" "0.21.5" + "@esbuild/darwin-arm64" "0.21.5" + "@esbuild/darwin-x64" "0.21.5" + "@esbuild/freebsd-arm64" "0.21.5" + "@esbuild/freebsd-x64" "0.21.5" + "@esbuild/linux-arm" "0.21.5" + "@esbuild/linux-arm64" "0.21.5" + "@esbuild/linux-ia32" "0.21.5" + "@esbuild/linux-loong64" "0.21.5" + "@esbuild/linux-mips64el" "0.21.5" + "@esbuild/linux-ppc64" "0.21.5" + "@esbuild/linux-riscv64" "0.21.5" + "@esbuild/linux-s390x" "0.21.5" + "@esbuild/linux-x64" "0.21.5" + "@esbuild/netbsd-x64" "0.21.5" + "@esbuild/openbsd-x64" "0.21.5" + "@esbuild/sunos-x64" "0.21.5" + "@esbuild/win32-arm64" "0.21.5" + "@esbuild/win32-ia32" "0.21.5" + "@esbuild/win32-x64" "0.21.5" + escalade@^3.1.1: version "3.1.2" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.2.tgz#54076e9ab29ea5bf3d8f1ed62acffbb88272df27" @@ -19845,7 +19989,7 @@ shelljs@^0.8.5: interpret "^1.0.0" rechoir "^0.6.2" -shx@^0.3.3: +shx@^0.3.4: version "0.3.4" resolved "https://registry.yarnpkg.com/shx/-/shx-0.3.4.tgz#74289230b4b663979167f94e1935901406e40f02" integrity sha512-N6A9MLVqjxZYcVn8hLmtneQWIJtp8IKzMP4eMnx+nqkvXoqinUPCbUFLp2UcWTEIUONhlk0ewxr/jaVGlc+J+g== @@ -22200,6 +22344,17 @@ vite@5.0.5: optionalDependencies: fsevents "~2.3.3" +vite@5.3.2: + version "5.3.2" + resolved "https://registry.yarnpkg.com/vite/-/vite-5.3.2.tgz#2f0a8531c71060467ed3e0a205a203f269b6d9c8" + integrity sha512-6lA7OBHBlXUxiJxbO5aAY2fsHHzDr1q7DvXYnyZycRs2Dz+dXBWuhpWHvmljTRTpQC2uvGmUFFkSHF2vGo90MA== + dependencies: + esbuild "^0.21.3" + postcss "^8.4.38" + rollup "^4.13.0" + optionalDependencies: + fsevents "~2.3.3" + vite@^5.0, vite@^5.0.0: version "5.2.10" resolved "https://registry.yarnpkg.com/vite/-/vite-5.2.10.tgz#2ac927c91e99d51b376a5c73c0e4b059705f5bd7" From 021d28ebebcbb4a9d4f23158afb42fe62d5e15db Mon Sep 17 00:00:00 2001 From: Max Marrone Date: Mon, 30 Sep 2024 12:54:31 -0400 Subject: [PATCH 08/17] feat(robot-server): Implement storage for "enable/disable error recovery" setting (#16333) --- .../persistence/_migrations/v6_to_v7.py | 1 + .../persistence/file_and_directory_names.py | 2 +- .../persistence/persistence_directory.py | 3 ++ .../persistence/tables/__init__.py | 4 ++ .../persistence/tables/schema_7.py | 28 +++++++++++ .../runs/error_recovery_setting_store.py | 46 +++++++++++++++++++ .../http_api/persistence/test_reset.py | 18 ++------ robot-server/tests/persistence/test_tables.py | 15 +++++- .../runs/test_error_recovery_setting_store.py | 29 ++++++++++++ 9 files changed, 129 insertions(+), 17 deletions(-) create mode 100644 robot-server/robot_server/runs/error_recovery_setting_store.py create mode 100644 robot-server/tests/runs/test_error_recovery_setting_store.py diff --git a/robot-server/robot_server/persistence/_migrations/v6_to_v7.py b/robot-server/robot_server/persistence/_migrations/v6_to_v7.py index 0faee6736e7..faae646e5b7 100644 --- a/robot-server/robot_server/persistence/_migrations/v6_to_v7.py +++ b/robot-server/robot_server/persistence/_migrations/v6_to_v7.py @@ -3,6 +3,7 @@ Summary of changes from schema 6: - Adds a new command_intent to store the commands intent in the commands table +- Adds the `boolean_setting` table. """ import json diff --git a/robot-server/robot_server/persistence/file_and_directory_names.py b/robot-server/robot_server/persistence/file_and_directory_names.py index 781217f6418..7074dd6db2f 100644 --- a/robot-server/robot_server/persistence/file_and_directory_names.py +++ b/robot-server/robot_server/persistence/file_and_directory_names.py @@ -8,7 +8,7 @@ from typing import Final -LATEST_VERSION_DIRECTORY: Final = "7" +LATEST_VERSION_DIRECTORY: Final = "7.1" DECK_CONFIGURATION_FILE: Final = "deck_configuration.json" PROTOCOLS_DIRECTORY: Final = "protocols" diff --git a/robot-server/robot_server/persistence/persistence_directory.py b/robot-server/robot_server/persistence/persistence_directory.py index c4a9675025a..2dd46711697 100644 --- a/robot-server/robot_server/persistence/persistence_directory.py +++ b/robot-server/robot_server/persistence/persistence_directory.py @@ -52,6 +52,9 @@ async def prepare_active_subdirectory(prepared_root: Path) -> Path: v3_to_v4.Migration3to4(subdirectory="4"), v4_to_v5.Migration4to5(subdirectory="5"), v5_to_v6.Migration5to6(subdirectory="6"), + # Subdirectory "7" was previously used on our edge branch for an in-dev + # schema that was never released to the public. It may be present on + # internal robots. v6_to_v7.Migration6to7(subdirectory=LATEST_VERSION_DIRECTORY), ], temp_file_prefix="temp-", diff --git a/robot-server/robot_server/persistence/tables/__init__.py b/robot-server/robot_server/persistence/tables/__init__.py index 097383a0612..006f5356d76 100644 --- a/robot-server/robot_server/persistence/tables/__init__.py +++ b/robot-server/robot_server/persistence/tables/__init__.py @@ -12,8 +12,10 @@ action_table, run_csv_rtp_table, data_files_table, + boolean_setting_table, PrimitiveParamSQLEnum, ProtocolKindSQLEnum, + BooleanSettingKey, ) @@ -28,6 +30,8 @@ "action_table", "run_csv_rtp_table", "data_files_table", + "boolean_setting_table", "PrimitiveParamSQLEnum", "ProtocolKindSQLEnum", + "BooleanSettingKey", ] diff --git a/robot-server/robot_server/persistence/tables/schema_7.py b/robot-server/robot_server/persistence/tables/schema_7.py index b08a447505d..790113ab8e3 100644 --- a/robot-server/robot_server/persistence/tables/schema_7.py +++ b/robot-server/robot_server/persistence/tables/schema_7.py @@ -102,6 +102,7 @@ class ProtocolKindSQLEnum(enum.Enum): PrimitiveParamSQLEnum, values_callable=lambda obj: [e.value for e in obj], create_constraint=True, + # todo(mm, 2024-09-24): Can we add validate_strings=True here? ), nullable=False, ), @@ -263,3 +264,30 @@ class ProtocolKindSQLEnum(enum.Enum): nullable=True, ), ) + + +class BooleanSettingKey(enum.Enum): + """Keys for boolean settings.""" + + DISABLE_ERROR_RECOVERY = "disable_error_recovery" + + +boolean_setting_table = sqlalchemy.Table( + "boolean_setting", + metadata, + sqlalchemy.Column( + "key", + sqlalchemy.Enum( + BooleanSettingKey, + values_callable=lambda obj: [e.value for e in obj], + validate_strings=True, + create_constraint=True, + ), + primary_key=True, + ), + sqlalchemy.Column( + "value", + sqlalchemy.Boolean, + nullable=False, + ), +) diff --git a/robot-server/robot_server/runs/error_recovery_setting_store.py b/robot-server/robot_server/runs/error_recovery_setting_store.py new file mode 100644 index 00000000000..2a892c26eeb --- /dev/null +++ b/robot-server/robot_server/runs/error_recovery_setting_store.py @@ -0,0 +1,46 @@ +# noqa: D100 + + +import sqlalchemy + +from robot_server.persistence.tables import boolean_setting_table, BooleanSettingKey + + +class ErrorRecoverySettingStore: + """Persistently stores settings related to error recovery.""" + + def __init__(self, sql_engine: sqlalchemy.engine.Engine) -> None: + self._sql_engine = sql_engine + + def get_is_disabled(self) -> bool | None: + """Get the value of the "error recovery enabled" setting. + + `None` is the default, i.e. it's never been explicitly set one way or the other. + """ + with self._sql_engine.begin() as transaction: + return transaction.execute( + sqlalchemy.select(boolean_setting_table.c.value).where( + boolean_setting_table.c.key + == BooleanSettingKey.DISABLE_ERROR_RECOVERY + ) + ).scalar_one_or_none() + + def set_is_disabled(self, is_disabled: bool | None) -> None: + """Set the value of the "error recovery enabled" setting. + + `None` means revert to the default. + """ + with self._sql_engine.begin() as transaction: + transaction.execute( + sqlalchemy.delete(boolean_setting_table).where( + boolean_setting_table.c.key + == BooleanSettingKey.DISABLE_ERROR_RECOVERY + ) + ) + if is_disabled is not None: + transaction.execute( + sqlalchemy.insert(boolean_setting_table).values( + key=BooleanSettingKey.DISABLE_ERROR_RECOVERY, + value=is_disabled, + ) + ) diff --git a/robot-server/tests/integration/http_api/persistence/test_reset.py b/robot-server/tests/integration/http_api/persistence/test_reset.py index ffff3aed08e..2d0bf00cb00 100644 --- a/robot-server/tests/integration/http_api/persistence/test_reset.py +++ b/robot-server/tests/integration/http_api/persistence/test_reset.py @@ -30,23 +30,13 @@ def _get_corrupt_persistence_dir() -> Path: async def _assert_reset_was_successful( robot_client: RobotClient, persistence_directory: Path ) -> None: - # It should have no protocols. + # We really want to check that the server's persistence directory has been wiped + # clean, but testing that directly would rely on internal implementation details + # of file layout and tend to be brittle. + # As an approximation, just check that there are no protocols or runs left. assert (await robot_client.get_protocols()).json()["data"] == [] - - # It should have no runs. assert (await robot_client.get_runs()).json()["data"] == [] - # There should be no files except for robot_server.db - # and an empty protocols/ directory. - all_files_and_directories = set(persistence_directory.glob("**/*")) - expected_files_and_directories = { - persistence_directory / "robot_server.db", - persistence_directory / "7", - persistence_directory / "7" / "protocols", - persistence_directory / "7" / "robot_server.db", - } - assert all_files_and_directories == expected_files_and_directories - async def _wait_until_initialization_failed(robot_client: RobotClient) -> None: """Wait until the server returns an "initialization failed" health status.""" diff --git a/robot-server/tests/persistence/test_tables.py b/robot-server/tests/persistence/test_tables.py index 757cdd9a570..bb3a5ece78e 100644 --- a/robot-server/tests/persistence/test_tables.py +++ b/robot-server/tests/persistence/test_tables.py @@ -142,8 +142,20 @@ FOREIGN KEY(file_id) REFERENCES data_files (id) ) """, + """ + CREATE TABLE boolean_setting ( + "key" VARCHAR(22) NOT NULL, + value BOOLEAN NOT NULL, + PRIMARY KEY ("key"), + CONSTRAINT booleansettingkey CHECK ("key" IN ('disable_error_recovery')) + ) + """, ] + +EXPECTED_STATEMENTS_V7 = EXPECTED_STATEMENTS_LATEST + + EXPECTED_STATEMENTS_V6 = [ """ CREATE TABLE protocol ( @@ -257,8 +269,6 @@ ] -EXPECTED_STATEMENTS_V7 = EXPECTED_STATEMENTS_LATEST - EXPECTED_STATEMENTS_V5 = [ """ CREATE TABLE protocol ( @@ -334,6 +344,7 @@ """, ] + EXPECTED_STATEMENTS_V4 = [ """ CREATE TABLE protocol ( diff --git a/robot-server/tests/runs/test_error_recovery_setting_store.py b/robot-server/tests/runs/test_error_recovery_setting_store.py new file mode 100644 index 00000000000..fae8bb76705 --- /dev/null +++ b/robot-server/tests/runs/test_error_recovery_setting_store.py @@ -0,0 +1,29 @@ +"""Tests for error_recovery_setting_store.""" + + +from robot_server.runs.error_recovery_setting_store import ErrorRecoverySettingStore + +import pytest +import sqlalchemy + + +@pytest.fixture +def subject( + sql_engine: sqlalchemy.engine.Engine, +) -> ErrorRecoverySettingStore: + """Return a test subject.""" + return ErrorRecoverySettingStore(sql_engine=sql_engine) + + +def test_error_recovery_setting_store(subject: ErrorRecoverySettingStore) -> None: + """Test `ErrorRecoverySettingStore`.""" + assert subject.get_is_disabled() is None + + subject.set_is_disabled(is_disabled=False) + assert subject.get_is_disabled() is False + + subject.set_is_disabled(is_disabled=True) + assert subject.get_is_disabled() is True + + subject.set_is_disabled(is_disabled=None) + assert subject.get_is_disabled() is None From 61aa51f05242aa1633258d29b56ac58478b0b526 Mon Sep 17 00:00:00 2001 From: Max Marrone Date: Mon, 30 Sep 2024 12:58:17 -0400 Subject: [PATCH 09/17] feat(api): Make gripper errors nonfatal (#16320) --- .../protocol_engine/commands/aspirate.py | 2 +- .../commands/aspirate_in_place.py | 2 +- .../commands/command_unions.py | 2 + .../protocol_engine/commands/dispense.py | 2 +- .../commands/dispense_in_place.py | 2 +- .../protocol_engine/commands/move_labware.py | 93 +++++-- .../execution/labware_movement.py | 3 + .../commands/test_move_labware.py | 242 ++++++++++-------- .../organisms/ErrorRecoveryFlows/constants.ts | 1 + 9 files changed, 223 insertions(+), 126 deletions(-) diff --git a/api/src/opentrons/protocol_engine/commands/aspirate.py b/api/src/opentrons/protocol_engine/commands/aspirate.py index ac0e34424e6..9876ce19bd3 100644 --- a/api/src/opentrons/protocol_engine/commands/aspirate.py +++ b/api/src/opentrons/protocol_engine/commands/aspirate.py @@ -161,7 +161,7 @@ async def execute(self, params: AspirateParams) -> _ExecuteReturn: ) -class Aspirate(BaseCommand[AspirateParams, AspirateResult, ErrorOccurrence]): +class Aspirate(BaseCommand[AspirateParams, AspirateResult, OverpressureError]): """Aspirate command model.""" commandType: AspirateCommandType = "aspirate" diff --git a/api/src/opentrons/protocol_engine/commands/aspirate_in_place.py b/api/src/opentrons/protocol_engine/commands/aspirate_in_place.py index 44dc2e93768..4ae5ec10b97 100644 --- a/api/src/opentrons/protocol_engine/commands/aspirate_in_place.py +++ b/api/src/opentrons/protocol_engine/commands/aspirate_in_place.py @@ -129,7 +129,7 @@ async def execute(self, params: AspirateInPlaceParams) -> _ExecuteReturn: class AspirateInPlace( - BaseCommand[AspirateInPlaceParams, AspirateInPlaceResult, ErrorOccurrence] + BaseCommand[AspirateInPlaceParams, AspirateInPlaceResult, OverpressureError] ): """AspirateInPlace command model.""" diff --git a/api/src/opentrons/protocol_engine/commands/command_unions.py b/api/src/opentrons/protocol_engine/commands/command_unions.py index b586e1f50aa..604fca50e14 100644 --- a/api/src/opentrons/protocol_engine/commands/command_unions.py +++ b/api/src/opentrons/protocol_engine/commands/command_unions.py @@ -144,6 +144,7 @@ ) from .move_labware import ( + GripperMovementError, MoveLabware, MoveLabwareParams, MoveLabwareCreate, @@ -706,6 +707,7 @@ DefinedErrorData[TipPhysicallyMissingError], DefinedErrorData[OverpressureError], DefinedErrorData[LiquidNotFoundError], + DefinedErrorData[GripperMovementError], ] diff --git a/api/src/opentrons/protocol_engine/commands/dispense.py b/api/src/opentrons/protocol_engine/commands/dispense.py index a2d0738b546..ce3ce3cdab1 100644 --- a/api/src/opentrons/protocol_engine/commands/dispense.py +++ b/api/src/opentrons/protocol_engine/commands/dispense.py @@ -121,7 +121,7 @@ async def execute(self, params: DispenseParams) -> _ExecuteReturn: ) -class Dispense(BaseCommand[DispenseParams, DispenseResult, ErrorOccurrence]): +class Dispense(BaseCommand[DispenseParams, DispenseResult, OverpressureError]): """Dispense command model.""" commandType: DispenseCommandType = "dispense" diff --git a/api/src/opentrons/protocol_engine/commands/dispense_in_place.py b/api/src/opentrons/protocol_engine/commands/dispense_in_place.py index 8f52af3284c..1cdbe90e908 100644 --- a/api/src/opentrons/protocol_engine/commands/dispense_in_place.py +++ b/api/src/opentrons/protocol_engine/commands/dispense_in_place.py @@ -107,7 +107,7 @@ async def execute(self, params: DispenseInPlaceParams) -> _ExecuteReturn: class DispenseInPlace( - BaseCommand[DispenseInPlaceParams, DispenseInPlaceResult, ErrorOccurrence] + BaseCommand[DispenseInPlaceParams, DispenseInPlaceResult, OverpressureError] ): """DispenseInPlace command model.""" diff --git a/api/src/opentrons/protocol_engine/commands/move_labware.py b/api/src/opentrons/protocol_engine/commands/move_labware.py index 965143fdbd7..c4381dd6865 100644 --- a/api/src/opentrons/protocol_engine/commands/move_labware.py +++ b/api/src/opentrons/protocol_engine/commands/move_labware.py @@ -1,10 +1,16 @@ """Models and implementation for the ``moveLabware`` command.""" from __future__ import annotations +from opentrons_shared_data.errors.exceptions import ( + FailedGripperPickupError, + LabwareDroppedError, + StallOrCollisionDetectedError, +) from pydantic import BaseModel, Field from typing import TYPE_CHECKING, Optional, Type from typing_extensions import Literal +from opentrons.protocol_engine.resources.model_utils import ModelUtils from opentrons.types import Point from ..types import ( CurrentWell, @@ -18,7 +24,13 @@ ) from ..errors import LabwareMovementNotAllowedError, NotSupportedOnRobotType from ..resources import labware_validation, fixture_validation -from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData +from .command import ( + AbstractCommandImpl, + BaseCommand, + BaseCommandCreate, + DefinedErrorData, + SuccessData, +) from ..errors.error_occurrence import ErrorOccurrence from ..state.update_types import StateUpdate from opentrons_shared_data.gripper.constants import GRIPPER_PADDLE_WIDTH @@ -76,27 +88,41 @@ class MoveLabwareResult(BaseModel): ) -class MoveLabwareImplementation( - AbstractCommandImpl[MoveLabwareParams, SuccessData[MoveLabwareResult, None]] -): +class GripperMovementError(ErrorOccurrence): + """Returned when something physically goes wrong when the gripper moves labware. + + When this error happens, the engine will leave the labware in its original place. + """ + + isDefined: bool = True + + errorType: Literal["gripperMovement"] = "gripperMovement" + + +_ExecuteReturn = ( + SuccessData[MoveLabwareResult, None] | DefinedErrorData[GripperMovementError] +) + + +class MoveLabwareImplementation(AbstractCommandImpl[MoveLabwareParams, _ExecuteReturn]): """The execution implementation for ``moveLabware`` commands.""" def __init__( self, + model_utils: ModelUtils, state_view: StateView, equipment: EquipmentHandler, labware_movement: LabwareMovementHandler, run_control: RunControlHandler, **kwargs: object, ) -> None: + self._model_utils = model_utils self._state_view = state_view self._equipment = equipment self._labware_movement = labware_movement self._run_control = run_control - async def execute( # noqa: C901 - self, params: MoveLabwareParams - ) -> SuccessData[MoveLabwareResult, None]: + async def execute(self, params: MoveLabwareParams) -> _ExecuteReturn: # noqa: C901 """Move a loaded labware to a new location.""" state_update = StateUpdate() @@ -205,16 +231,49 @@ async def execute( # noqa: C901 dropOffset=params.dropOffset or LabwareOffsetVector(x=0, y=0, z=0), ) - # Skips gripper moves when using virtual gripper - await self._labware_movement.move_labware_with_gripper( - labware_id=params.labwareId, - current_location=validated_current_loc, - new_location=validated_new_loc, - user_offset_data=user_offset_data, - post_drop_slide_offset=post_drop_slide_offset, - ) + try: + # Skips gripper moves when using virtual gripper + await self._labware_movement.move_labware_with_gripper( + labware_id=params.labwareId, + current_location=validated_current_loc, + new_location=validated_new_loc, + user_offset_data=user_offset_data, + post_drop_slide_offset=post_drop_slide_offset, + ) + except ( + FailedGripperPickupError, + LabwareDroppedError, + StallOrCollisionDetectedError, + # todo(mm, 2024-09-26): Catch LabwareNotPickedUpError when that exists and + # move_labware_with_gripper() raises it. + ) as exception: + gripper_movement_error: GripperMovementError | None = ( + GripperMovementError( + id=self._model_utils.generate_id(), + createdAt=self._model_utils.get_timestamp(), + errorCode=exception.code.value.code, + detail=exception.code.value.detail, + wrappedErrors=[ + ErrorOccurrence.from_failed( + id=self._model_utils.generate_id(), + createdAt=self._model_utils.get_timestamp(), + error=exception, + ) + ], + ) + ) + else: + gripper_movement_error = None + # All mounts will have been retracted as part of the gripper move. state_update.clear_all_pipette_locations() + + if gripper_movement_error: + return DefinedErrorData( + public=gripper_movement_error, + state_update=state_update, + ) + elif params.strategy == LabwareMovementStrategy.MANUAL_MOVE_WITH_PAUSE: # Pause to allow for manual labware movement await self._run_control.wait_for_resume() @@ -244,7 +303,9 @@ async def execute( # noqa: C901 ) -class MoveLabware(BaseCommand[MoveLabwareParams, MoveLabwareResult, ErrorOccurrence]): +class MoveLabware( + BaseCommand[MoveLabwareParams, MoveLabwareResult, GripperMovementError] +): """A ``moveLabware`` command.""" commandType: MoveLabwareCommandType = "moveLabware" diff --git a/api/src/opentrons/protocol_engine/execution/labware_movement.py b/api/src/opentrons/protocol_engine/execution/labware_movement.py index 5851cacd7b3..b7d03914793 100644 --- a/api/src/opentrons/protocol_engine/execution/labware_movement.py +++ b/api/src/opentrons/protocol_engine/execution/labware_movement.py @@ -177,6 +177,9 @@ async def move_labware_with_gripper( labware_id ) well_bbox = self._state_store.labware.get_well_bbox(labware_id) + # todo(mm, 2024-09-26): This currently raises a lower-level 2015 FailedGripperPickupError. + # Convert this to a higher-level 3001 LabwareDroppedError or 3002 LabwareNotPickedUpError, + # depending on what waypoint we're at, to propagate a more specific error code to users. ot3api.raise_error_if_gripper_pickup_failed( expected_grip_width=labware_bbox.y, grip_width_uncertainty_wider=abs( diff --git a/api/tests/opentrons/protocol_engine/commands/test_move_labware.py b/api/tests/opentrons/protocol_engine/commands/test_move_labware.py index cc6a75bc020..d1309761d8f 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_move_labware.py +++ b/api/tests/opentrons/protocol_engine/commands/test_move_labware.py @@ -1,8 +1,15 @@ """Test the ``moveLabware`` command.""" +from datetime import datetime import inspect import pytest -from decoy import Decoy +from decoy import Decoy, matchers +from opentrons_shared_data.errors.exceptions import ( + EnumeratedError, + FailedGripperPickupError, + LabwareDroppedError, + StallOrCollisionDetectedError, +) from opentrons_shared_data.labware.labware_definition import Parameters, Dimensions from opentrons_shared_data.gripper.constants import GRIPPER_PADDLE_WIDTH @@ -11,6 +18,7 @@ from opentrons.protocols.models import LabwareDefinition from opentrons.protocol_engine import errors, Config from opentrons.protocol_engine.resources import labware_validation +from opentrons.protocol_engine.resources.model_utils import ModelUtils from opentrons.protocol_engine.types import ( CurrentWell, DeckSlotLocation, @@ -24,8 +32,9 @@ AddressableAreaLocation, ) from opentrons.protocol_engine.state.state import StateView -from opentrons.protocol_engine.commands.command import SuccessData +from opentrons.protocol_engine.commands.command import DefinedErrorData, SuccessData from opentrons.protocol_engine.commands.move_labware import ( + GripperMovementError, MoveLabwareParams, MoveLabwareResult, MoveLabwareImplementation, @@ -46,6 +55,24 @@ def patch_mock_labware_validation( monkeypatch.setattr(labware_validation, name, decoy.mock(func=func)) +@pytest.fixture +def subject( + equipment: EquipmentHandler, + labware_movement: LabwareMovementHandler, + state_view: StateView, + run_control: RunControlHandler, + model_utils: ModelUtils, +) -> MoveLabwareImplementation: + """Return a test subject configured to use mocked-out dependencies.""" + return MoveLabwareImplementation( + state_view=state_view, + equipment=equipment, + labware_movement=labware_movement, + run_control=run_control, + model_utils=model_utils, + ) + + @pytest.mark.parametrize( argnames=["strategy", "times_pause_called"], argvalues=[ @@ -55,21 +82,14 @@ def patch_mock_labware_validation( ) async def test_manual_move_labware_implementation( decoy: Decoy, + subject: MoveLabwareImplementation, equipment: EquipmentHandler, - labware_movement: LabwareMovementHandler, state_view: StateView, run_control: RunControlHandler, strategy: LabwareMovementStrategy, times_pause_called: int, ) -> None: """It should execute a pause and return the new offset.""" - subject = MoveLabwareImplementation( - state_view=state_view, - equipment=equipment, - labware_movement=labware_movement, - run_control=run_control, - ) - data = MoveLabwareParams( labwareId="my-cool-labware-id", newLocation=DeckSlotLocation(slotName=DeckSlotName.SLOT_4), @@ -119,19 +139,12 @@ async def test_manual_move_labware_implementation( async def test_move_labware_implementation_on_labware( decoy: Decoy, + subject: MoveLabwareImplementation, equipment: EquipmentHandler, - labware_movement: LabwareMovementHandler, state_view: StateView, run_control: RunControlHandler, ) -> None: """It should execute a pause and return the new offset.""" - subject = MoveLabwareImplementation( - state_view=state_view, - equipment=equipment, - labware_movement=labware_movement, - run_control=run_control, - ) - data = MoveLabwareParams( labwareId="my-cool-labware-id", newLocation=OnLabwareLocation(labwareId="new-labware-id"), @@ -192,18 +205,12 @@ async def test_move_labware_implementation_on_labware( async def test_gripper_move_labware_implementation( decoy: Decoy, + subject: MoveLabwareImplementation, equipment: EquipmentHandler, labware_movement: LabwareMovementHandler, state_view: StateView, - run_control: RunControlHandler, ) -> None: """It should delegate to the equipment handler and return the new offset.""" - subject = MoveLabwareImplementation( - state_view=state_view, - equipment=equipment, - labware_movement=labware_movement, - run_control=run_control, - ) from_location = DeckSlotLocation(slotName=DeckSlotName.SLOT_1) new_location = DeckSlotLocation(slotName=DeckSlotName.SLOT_5) @@ -285,6 +292,101 @@ async def test_gripper_move_labware_implementation( ) +@pytest.mark.parametrize( + "underlying_exception", + [ + FailedGripperPickupError(), + LabwareDroppedError(), + StallOrCollisionDetectedError(), + ], +) +async def test_gripper_error( + decoy: Decoy, + subject: MoveLabwareImplementation, + state_view: StateView, + model_utils: ModelUtils, + labware_movement: LabwareMovementHandler, + underlying_exception: EnumeratedError, +) -> None: + """Test the handling of errors during a gripper movement.""" + labware_id = "labware-id" + labware_namespace = "labware-namespace" + labware_load_name = "load-name" + labware_definition_uri = "opentrons-test/load-name/1" + labware_def = LabwareDefinition.construct( # type: ignore[call-arg] + namespace=labware_namespace, + ) + original_location = DeckSlotLocation(slotName=DeckSlotName.SLOT_A1) + new_location = DeckSlotLocation(slotName=DeckSlotName.SLOT_A2) + error_id = "error-id" + error_created_at = datetime.now() + + # Common MoveLabwareImplementation boilerplate: + decoy.when(state_view.labware.get_definition(labware_id=labware_id)).then_return( + LabwareDefinition.construct(namespace=labware_namespace) # type: ignore[call-arg] + ) + decoy.when(state_view.labware.get(labware_id=labware_id)).then_return( + LoadedLabware( + id=labware_id, + loadName=labware_load_name, + definitionUri=labware_definition_uri, + location=original_location, + offsetId=None, + ) + ) + decoy.when( + state_view.geometry.ensure_valid_gripper_location(original_location) + ).then_return(original_location) + decoy.when( + state_view.geometry.ensure_valid_gripper_location(new_location) + ).then_return(new_location) + decoy.when( + state_view.geometry.ensure_location_not_occupied( + location=new_location, + ) + ).then_return(new_location) + decoy.when(labware_validation.validate_gripper_compatible(labware_def)).then_return( + True + ) + params = MoveLabwareParams( + labwareId=labware_id, + newLocation=new_location, + strategy=LabwareMovementStrategy.USING_GRIPPER, + ) + + # Actual setup for this test: + decoy.when( + await labware_movement.move_labware_with_gripper( + labware_id=labware_id, + current_location=original_location, + new_location=new_location, + user_offset_data=LabwareMovementOffsetData( + pickUpOffset=LabwareOffsetVector(x=0, y=0, z=0), + dropOffset=LabwareOffsetVector(x=0, y=0, z=0), + ), + post_drop_slide_offset=None, + ) + ).then_raise(underlying_exception) + decoy.when(model_utils.get_timestamp()).then_return(error_created_at) + decoy.when(model_utils.generate_id()).then_return(error_id) + + result = await subject.execute(params) + + assert result == DefinedErrorData( + public=GripperMovementError.construct( + id=error_id, + createdAt=error_created_at, + errorCode=underlying_exception.code.value.code, + detail=underlying_exception.code.value.detail, + wrappedErrors=[matchers.Anything()], + ), + state_update=update_types.StateUpdate( + labware_location=update_types.NO_CHANGE, + pipette_location=update_types.CLEAR, + ), + ) + + @pytest.mark.parametrize( ("current_labware_id", "moved_labware_id", "expect_cleared_location"), [ @@ -294,22 +396,13 @@ async def test_gripper_move_labware_implementation( ) async def test_clears_location_if_current_labware_moved_from_under_pipette( decoy: Decoy, - equipment: EquipmentHandler, - labware_movement: LabwareMovementHandler, + subject: MoveLabwareImplementation, state_view: StateView, - run_control: RunControlHandler, current_labware_id: str, moved_labware_id: str, expect_cleared_location: bool, ) -> None: """If it moves the labware that the pipette is currently over, it should clear the location.""" - subject = MoveLabwareImplementation( - state_view=state_view, - equipment=equipment, - labware_movement=labware_movement, - run_control=run_control, - ) - from_location = DeckSlotLocation(slotName=DeckSlotName.SLOT_A1) to_location = DeckSlotLocation(slotName=DeckSlotName.SLOT_A2) @@ -345,18 +438,12 @@ async def test_clears_location_if_current_labware_moved_from_under_pipette( async def test_gripper_move_to_waste_chute_implementation( decoy: Decoy, + subject: MoveLabwareImplementation, equipment: EquipmentHandler, labware_movement: LabwareMovementHandler, state_view: StateView, - run_control: RunControlHandler, ) -> None: """It should drop the labware with a delay added.""" - subject = MoveLabwareImplementation( - state_view=state_view, - equipment=equipment, - labware_movement=labware_movement, - run_control=run_control, - ) from_location = DeckSlotLocation(slotName=DeckSlotName.SLOT_1) new_location = AddressableAreaLocation(addressableAreaName="gripperWasteChute") labware_width = 50 @@ -443,18 +530,11 @@ async def test_gripper_move_to_waste_chute_implementation( async def test_move_labware_raises_for_labware_or_module_not_found( decoy: Decoy, + subject: MoveLabwareImplementation, equipment: EquipmentHandler, - labware_movement: LabwareMovementHandler, - run_control: RunControlHandler, state_view: StateView, ) -> None: """It should raise an error when specified labware/ module is not found.""" - subject = MoveLabwareImplementation( - state_view=state_view, - labware_movement=labware_movement, - equipment=equipment, - run_control=run_control, - ) move_non_existent_labware_params = MoveLabwareParams( labwareId="my-cool-labware-id", newLocation=DeckSlotLocation(slotName=DeckSlotName.SLOT_5), @@ -499,19 +579,12 @@ async def test_move_labware_raises_for_labware_or_module_not_found( async def test_move_labware_raises_if_movement_obstructed( decoy: Decoy, + subject: MoveLabwareImplementation, equipment: EquipmentHandler, labware_movement: LabwareMovementHandler, state_view: StateView, - run_control: RunControlHandler, ) -> None: """It should execute a pause and return the new offset.""" - subject = MoveLabwareImplementation( - state_view=state_view, - equipment=equipment, - labware_movement=labware_movement, - run_control=run_control, - ) - data = MoveLabwareParams( labwareId="my-cool-labware-id", newLocation=DeckSlotLocation(slotName=DeckSlotName.SLOT_5), @@ -551,18 +624,10 @@ async def test_move_labware_raises_if_movement_obstructed( async def test_move_labware_raises_when_location_occupied( decoy: Decoy, - equipment: EquipmentHandler, - labware_movement: LabwareMovementHandler, + subject: MoveLabwareImplementation, state_view: StateView, - run_control: RunControlHandler, ) -> None: """It should raise an error when trying to move labware to non-empty location.""" - subject = MoveLabwareImplementation( - state_view=state_view, - labware_movement=labware_movement, - equipment=equipment, - run_control=run_control, - ) move_labware_params = MoveLabwareParams( labwareId="my-cool-labware-id", newLocation=DeckSlotLocation(slotName=DeckSlotName.SLOT_5), @@ -589,19 +654,10 @@ async def test_move_labware_raises_when_location_occupied( async def test_move_labware_raises_when_moving_adapter_with_gripper( decoy: Decoy, - equipment: EquipmentHandler, - labware_movement: LabwareMovementHandler, + subject: MoveLabwareImplementation, state_view: StateView, - run_control: RunControlHandler, ) -> None: """It should raise an error when trying to move an adapter with a gripper.""" - subject = MoveLabwareImplementation( - state_view=state_view, - equipment=equipment, - labware_movement=labware_movement, - run_control=run_control, - ) - data = MoveLabwareParams( labwareId="my-cool-labware-id", newLocation=DeckSlotLocation(slotName=DeckSlotName.SLOT_4), @@ -639,19 +695,10 @@ async def test_move_labware_raises_when_moving_adapter_with_gripper( async def test_move_labware_raises_when_moving_labware_with_gripper_incompatible_quirk( decoy: Decoy, - equipment: EquipmentHandler, - labware_movement: LabwareMovementHandler, + subject: MoveLabwareImplementation, state_view: StateView, - run_control: RunControlHandler, ) -> None: """It should raise an error when trying to move an adapter with a gripper.""" - subject = MoveLabwareImplementation( - state_view=state_view, - equipment=equipment, - labware_movement=labware_movement, - run_control=run_control, - ) - data = MoveLabwareParams( labwareId="my-cool-labware-id", newLocation=DeckSlotLocation(slotName=DeckSlotName.SLOT_4), @@ -687,18 +734,10 @@ async def test_move_labware_raises_when_moving_labware_with_gripper_incompatible async def test_move_labware_with_gripper_raises_on_ot2( decoy: Decoy, - equipment: EquipmentHandler, - labware_movement: LabwareMovementHandler, + subject: MoveLabwareImplementation, state_view: StateView, - run_control: RunControlHandler, ) -> None: """It should raise an error when using a gripper with robot type of OT2.""" - subject = MoveLabwareImplementation( - state_view=state_view, - equipment=equipment, - labware_movement=labware_movement, - run_control=run_control, - ) data = MoveLabwareParams( labwareId="my-cool-labware-id", newLocation=DeckSlotLocation(slotName=DeckSlotName.SLOT_4), @@ -728,19 +767,10 @@ async def test_move_labware_with_gripper_raises_on_ot2( async def test_move_labware_raises_when_moving_fixed_trash_labware( decoy: Decoy, - equipment: EquipmentHandler, - labware_movement: LabwareMovementHandler, + subject: MoveLabwareImplementation, state_view: StateView, - run_control: RunControlHandler, ) -> None: """It should raise an error when trying to move a fixed trash.""" - subject = MoveLabwareImplementation( - state_view=state_view, - equipment=equipment, - labware_movement=labware_movement, - run_control=run_control, - ) - data = MoveLabwareParams( labwareId="my-cool-labware-id", newLocation=DeckSlotLocation(slotName=DeckSlotName.FIXED_TRASH), diff --git a/app/src/organisms/ErrorRecoveryFlows/constants.ts b/app/src/organisms/ErrorRecoveryFlows/constants.ts index f7cea32eb03..f9903ccac9c 100644 --- a/app/src/organisms/ErrorRecoveryFlows/constants.ts +++ b/app/src/organisms/ErrorRecoveryFlows/constants.ts @@ -18,6 +18,7 @@ export const DEFINED_ERROR_TYPES = { OVERPRESSURE: 'overpressure', LIQUID_NOT_FOUND: 'liquidNotFound', TIP_PHYSICALLY_MISSING: 'tipPhysicallyMissing', + GRIPPER_MOVEMENT: 'gripperMovement', } // Client-defined error-handling flows. From 141b2fedc879bece5a61c82798facea63eb8070e Mon Sep 17 00:00:00 2001 From: Max Marrone Date: Mon, 30 Sep 2024 13:12:54 -0400 Subject: [PATCH 10/17] feat(robot-server): Add `PATCH /errorRecovery/settings` endpoint with `enableErrorRecovery` setting (#16355) --- .../robot_server/error_recovery/__init__.py | 5 ++ .../error_recovery/settings/__init__.py | 1 + .../error_recovery/settings/models.py | 46 ++++++++++ .../error_recovery/settings/router.py | 70 +++++++++++++++ .../settings/store.py} | 28 ++++-- .../persistence/tables/schema_7.py | 2 +- robot-server/robot_server/router.py | 7 ++ .../service/legacy/routers/settings.py | 11 ++- robot-server/tests/error_recovery/__init__.py | 1 + .../tests/error_recovery/settings/__init__.py | 1 + .../error_recovery/settings/test_store.py | 29 ++++++ robot-server/tests/integration/conftest.py | 5 ++ .../http_api/error_recovery/__init__.py | 0 .../error_recovery/test_settings.tavern.yaml | 88 +++++++++++++++++++ .../tests/integration/robot_client.py | 7 ++ robot-server/tests/persistence/test_tables.py | 4 +- .../runs/test_error_recovery_setting_store.py | 29 ------ 17 files changed, 293 insertions(+), 41 deletions(-) create mode 100644 robot-server/robot_server/error_recovery/__init__.py create mode 100644 robot-server/robot_server/error_recovery/settings/__init__.py create mode 100644 robot-server/robot_server/error_recovery/settings/models.py create mode 100644 robot-server/robot_server/error_recovery/settings/router.py rename robot-server/robot_server/{runs/error_recovery_setting_store.py => error_recovery/settings/store.py} (55%) create mode 100644 robot-server/tests/error_recovery/__init__.py create mode 100644 robot-server/tests/error_recovery/settings/__init__.py create mode 100644 robot-server/tests/error_recovery/settings/test_store.py create mode 100644 robot-server/tests/integration/http_api/error_recovery/__init__.py create mode 100644 robot-server/tests/integration/http_api/error_recovery/test_settings.tavern.yaml delete mode 100644 robot-server/tests/runs/test_error_recovery_setting_store.py diff --git a/robot-server/robot_server/error_recovery/__init__.py b/robot-server/robot_server/error_recovery/__init__.py new file mode 100644 index 00000000000..3adc4a5a851 --- /dev/null +++ b/robot-server/robot_server/error_recovery/__init__.py @@ -0,0 +1,5 @@ +"""The implementation of the `/errorRecovery` routes. + +Note that much of the code for the overall error recovery feature lives elsewhere, +e.g. in `robot_server.runs`. +""" diff --git a/robot-server/robot_server/error_recovery/settings/__init__.py b/robot-server/robot_server/error_recovery/settings/__init__.py new file mode 100644 index 00000000000..da3fed7d8d8 --- /dev/null +++ b/robot-server/robot_server/error_recovery/settings/__init__.py @@ -0,0 +1 @@ +"""The implementation of the `/errorRecovery/settings` routes.""" diff --git a/robot-server/robot_server/error_recovery/settings/models.py b/robot-server/robot_server/error_recovery/settings/models.py new file mode 100644 index 00000000000..c6cf8c99618 --- /dev/null +++ b/robot-server/robot_server/error_recovery/settings/models.py @@ -0,0 +1,46 @@ +"""HTTP request/response models for error recovery settings.""" + + +import textwrap +from typing import Annotated +import pydantic + + +class ResponseData(pydantic.BaseModel): + """Response body data from the `/errorRecovery/settings` endpoints.""" + + enabled: Annotated[ + bool, + pydantic.Field( + description=textwrap.dedent( + """\ + Whether error recovery mode is globally enabled. + See `PATCH /errorRecovery/settings`. + """ + ) + ), + ] + + +class RequestData(pydantic.BaseModel): + """Request body data for `PATCH /errorRecovery/settings`.""" + + enabled: Annotated[ + bool | None, + pydantic.Field( + description=textwrap.dedent( + """\ + If provided, globally enables or disables error recovery mode. + + If this is `true`, a run (see the `/runs` endpoints) will *potentially* + enter recovery mode when an error happens, depending on the details of + the error and depending on `/runs/{runId}/errorRecoveryPolicy`. + + If this is `false`, a run will just fail if it encounters an error. + + The default is `true`. This currently only has an effect on Flex robots. + On OT-2s, error recovery is not supported. + """ + ) + ), + ] = None diff --git a/robot-server/robot_server/error_recovery/settings/router.py b/robot-server/robot_server/error_recovery/settings/router.py new file mode 100644 index 00000000000..1a302b05582 --- /dev/null +++ b/robot-server/robot_server/error_recovery/settings/router.py @@ -0,0 +1,70 @@ +"""FastAPI endpoint functions to implement `/errorRecovery/settings`.""" + + +from typing import Annotated + +import fastapi + +from robot_server.service.json_api import PydanticResponse, RequestModel, SimpleBody +from .models import RequestData, ResponseData +from .store import ErrorRecoverySettingStore, get_error_recovery_setting_store + + +router = fastapi.APIRouter() +_PATH = "/errorRecovery/settings" + + +@PydanticResponse.wrap_route( + router.get, + path=_PATH, + summary="Get current error recovery settings", +) +async def get_error_recovery_settings( # noqa: D103 + store: Annotated[ + ErrorRecoverySettingStore, fastapi.Depends(get_error_recovery_setting_store) + ] +) -> PydanticResponse[SimpleBody[ResponseData]]: + return await _get_current_response(store) + + +@PydanticResponse.wrap_route( + router.patch, + path=_PATH, + summary="Set error recovery settings", +) +async def patch_error_recovery_settings( # noqa: D103 + request_body: RequestModel[RequestData], + store: Annotated[ + ErrorRecoverySettingStore, fastapi.Depends(get_error_recovery_setting_store) + ], +) -> PydanticResponse[SimpleBody[ResponseData]]: + if request_body.data.enabled is not None: + store.set_is_enabled(request_body.data.enabled) + return await _get_current_response(store) + + +@PydanticResponse.wrap_route( + router.delete, + path=_PATH, + summary="Reset error recovery settings to defaults", +) +async def delete_error_recovery_settings( # noqa: D103 + store: Annotated[ + ErrorRecoverySettingStore, fastapi.Depends(get_error_recovery_setting_store) + ], +) -> PydanticResponse[SimpleBody[ResponseData]]: + store.set_is_enabled(None) + return await _get_current_response(store) + + +async def _get_current_response( + store: ErrorRecoverySettingStore, +) -> PydanticResponse[SimpleBody[ResponseData]]: + is_enabled = store.get_is_enabled() + if is_enabled is None: + # todo(mm, 2024-09-30): This defaulting will probably need to move down a layer + # when we connect this setting to `POST /runs`. + is_enabled = True + return await PydanticResponse.create( + SimpleBody.construct(data=ResponseData.construct(enabled=is_enabled)) + ) diff --git a/robot-server/robot_server/runs/error_recovery_setting_store.py b/robot-server/robot_server/error_recovery/settings/store.py similarity index 55% rename from robot-server/robot_server/runs/error_recovery_setting_store.py rename to robot-server/robot_server/error_recovery/settings/store.py index 2a892c26eeb..6cef66aae2e 100644 --- a/robot-server/robot_server/runs/error_recovery_setting_store.py +++ b/robot-server/robot_server/error_recovery/settings/store.py @@ -1,8 +1,12 @@ # noqa: D100 +from typing import Annotated + +import fastapi import sqlalchemy +from robot_server.persistence.fastapi_dependencies import get_sql_engine from robot_server.persistence.tables import boolean_setting_table, BooleanSettingKey @@ -12,7 +16,7 @@ class ErrorRecoverySettingStore: def __init__(self, sql_engine: sqlalchemy.engine.Engine) -> None: self._sql_engine = sql_engine - def get_is_disabled(self) -> bool | None: + def get_is_enabled(self) -> bool | None: """Get the value of the "error recovery enabled" setting. `None` is the default, i.e. it's never been explicitly set one way or the other. @@ -21,11 +25,11 @@ def get_is_disabled(self) -> bool | None: return transaction.execute( sqlalchemy.select(boolean_setting_table.c.value).where( boolean_setting_table.c.key - == BooleanSettingKey.DISABLE_ERROR_RECOVERY + == BooleanSettingKey.ENABLE_ERROR_RECOVERY ) ).scalar_one_or_none() - def set_is_disabled(self, is_disabled: bool | None) -> None: + def set_is_enabled(self, is_enabled: bool | None) -> None: """Set the value of the "error recovery enabled" setting. `None` means revert to the default. @@ -34,13 +38,23 @@ def set_is_disabled(self, is_disabled: bool | None) -> None: transaction.execute( sqlalchemy.delete(boolean_setting_table).where( boolean_setting_table.c.key - == BooleanSettingKey.DISABLE_ERROR_RECOVERY + == BooleanSettingKey.ENABLE_ERROR_RECOVERY ) ) - if is_disabled is not None: + if is_enabled is not None: transaction.execute( sqlalchemy.insert(boolean_setting_table).values( - key=BooleanSettingKey.DISABLE_ERROR_RECOVERY, - value=is_disabled, + key=BooleanSettingKey.ENABLE_ERROR_RECOVERY, + value=is_enabled, ) ) + + +async def get_error_recovery_setting_store( + sql_engine: Annotated[sqlalchemy.engine.Engine, fastapi.Depends(get_sql_engine)] +) -> ErrorRecoverySettingStore: + """A FastAPI dependency to return the server's ErrorRecoverySettingStore.""" + # Since the store itself has no state, and no asyncio.Locks or anything, + # instances are fungible and disposable, and we can use a fresh one for each + # request instead of having to maintain a global singleton. + return ErrorRecoverySettingStore(sql_engine) diff --git a/robot-server/robot_server/persistence/tables/schema_7.py b/robot-server/robot_server/persistence/tables/schema_7.py index 790113ab8e3..cf1e2d8d717 100644 --- a/robot-server/robot_server/persistence/tables/schema_7.py +++ b/robot-server/robot_server/persistence/tables/schema_7.py @@ -269,7 +269,7 @@ class ProtocolKindSQLEnum(enum.Enum): class BooleanSettingKey(enum.Enum): """Keys for boolean settings.""" - DISABLE_ERROR_RECOVERY = "disable_error_recovery" + ENABLE_ERROR_RECOVERY = "enable_error_recovery" boolean_setting_table = sqlalchemy.Table( diff --git a/robot-server/robot_server/router.py b/robot-server/robot_server/router.py index ff60286b4f9..63ae4e5ab43 100644 --- a/robot-server/robot_server/router.py +++ b/robot-server/robot_server/router.py @@ -8,6 +8,7 @@ from .client_data.router import router as client_data_router from .commands.router import commands_router from .deck_configuration.router import router as deck_configuration_router +from .error_recovery.settings.router import router as error_recovery_settings_router from .health.router import health_router from .instruments.router import instruments_router from .maintenance_runs.router import maintenance_runs_router @@ -89,6 +90,12 @@ dependencies=[Depends(check_version_header)], ) +router.include_router( + router=error_recovery_settings_router, + tags=["Error Recovery Settings"], + dependencies=[Depends(check_version_header)], +) + router.include_router( router=modules_router, tags=["Attached Modules"], diff --git a/robot-server/robot_server/service/legacy/routers/settings.py b/robot-server/robot_server/service/legacy/routers/settings.py index b5caa233724..f1c7c77dc72 100644 --- a/robot-server/robot_server/service/legacy/routers/settings.py +++ b/robot-server/robot_server/service/legacy/routers/settings.py @@ -65,7 +65,7 @@ # TODO: (ba, 2024-04-11): We should have a proper IPC mechanism to talk between # the servers instead of one off endpoint calls like these. -async def set_oem_mode_request(enable): +async def _set_oem_mode_request(enable: bool) -> int: """PUT request to set the OEM Mode for the system server.""" async with aiohttp.ClientSession() as session: async with session.put( @@ -94,7 +94,14 @@ async def post_settings( try: # send request to system server if this is the enableOEMMode setting if update.id == "enableOEMMode" and robot_type == RobotTypeEnum.FLEX: - resp = await set_oem_mode_request(update.value) + resp = await _set_oem_mode_request( + # Unlike opentrons.advanced_settings, system-server cannot store + # `None`/`null` to restore to default. Storing `False` instead is close + # enough. + update.value + if update.value is not None + else False + ) if resp != 200: # TODO: raise correct error here raise Exception(f"Something went wrong setting OEM Mode. err: {resp}") diff --git a/robot-server/tests/error_recovery/__init__.py b/robot-server/tests/error_recovery/__init__.py new file mode 100644 index 00000000000..6e031999e7b --- /dev/null +++ b/robot-server/tests/error_recovery/__init__.py @@ -0,0 +1 @@ +# noqa: D104 diff --git a/robot-server/tests/error_recovery/settings/__init__.py b/robot-server/tests/error_recovery/settings/__init__.py new file mode 100644 index 00000000000..6e031999e7b --- /dev/null +++ b/robot-server/tests/error_recovery/settings/__init__.py @@ -0,0 +1 @@ +# noqa: D104 diff --git a/robot-server/tests/error_recovery/settings/test_store.py b/robot-server/tests/error_recovery/settings/test_store.py new file mode 100644 index 00000000000..cc69f5d307f --- /dev/null +++ b/robot-server/tests/error_recovery/settings/test_store.py @@ -0,0 +1,29 @@ +"""Tests for the error recovery settings store.""" + + +from robot_server.error_recovery.settings.store import ErrorRecoverySettingStore + +import pytest +import sqlalchemy + + +@pytest.fixture +def subject( + sql_engine: sqlalchemy.engine.Engine, +) -> ErrorRecoverySettingStore: + """Return a test subject.""" + return ErrorRecoverySettingStore(sql_engine=sql_engine) + + +def test_error_recovery_setting_store(subject: ErrorRecoverySettingStore) -> None: + """Test `ErrorRecoverySettingStore`.""" + assert subject.get_is_enabled() is None + + subject.set_is_enabled(is_enabled=False) + assert subject.get_is_enabled() is False + + subject.set_is_enabled(is_enabled=True) + assert subject.get_is_enabled() is True + + subject.set_is_enabled(is_enabled=None) + assert subject.get_is_enabled() is None diff --git a/robot-server/tests/integration/conftest.py b/robot-server/tests/integration/conftest.py index 2caad065d10..0f96913a8f4 100644 --- a/robot-server/tests/integration/conftest.py +++ b/robot-server/tests/integration/conftest.py @@ -131,6 +131,7 @@ async def _clean_server_state_async() -> None: await _delete_all_sessions(robot_client) await _reset_deck_configuration(robot_client) + await _reset_error_recovery_settings(robot_client) await _delete_client_data(robot_client) @@ -174,3 +175,7 @@ async def _delete_client_data(robot_client: RobotClient) -> None: async def _reset_deck_configuration(robot_client: RobotClient) -> None: await robot_client.post_setting_reset_options({"deckConfiguration": True}) + + +async def _reset_error_recovery_settings(robot_client: RobotClient) -> None: + await robot_client.delete_error_recovery_settings() diff --git a/robot-server/tests/integration/http_api/error_recovery/__init__.py b/robot-server/tests/integration/http_api/error_recovery/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/robot-server/tests/integration/http_api/error_recovery/test_settings.tavern.yaml b/robot-server/tests/integration/http_api/error_recovery/test_settings.tavern.yaml new file mode 100644 index 00000000000..225fd16964a --- /dev/null +++ b/robot-server/tests/integration/http_api/error_recovery/test_settings.tavern.yaml @@ -0,0 +1,88 @@ +test_name: Test the /errorRecovery/settings endpoints + +marks: + - usefixtures: + - ot3_server_base_url + +stages: + - name: Get default settings + request: + method: GET + url: '{ot3_server_base_url}/errorRecovery/settings' + response: + status_code: 200 + json: &initial_get_settings_response + data: + enabled: true + + - name: Change settings + request: + method: PATCH + url: '{ot3_server_base_url}/errorRecovery/settings' + json: + data: + enabled: false + response: + status_code: 200 + json: &patch_settings_response + data: + enabled: false + + - name: Get the settings again and make sure they're still changed + request: + method: GET + url: '{ot3_server_base_url}/errorRecovery/settings' + response: + status_code: 200 + json: *patch_settings_response + + - name: Restore defaults + request: + method: DELETE + url: '{ot3_server_base_url}/errorRecovery/settings' + response: + status_code: 200 + json: *initial_get_settings_response + + - name: Get the settings again and make sure they're still the defaults + request: + method: GET + url: '{ot3_server_base_url}/errorRecovery/settings' + response: + status_code: 200 + json: *initial_get_settings_response + +--- +test_name: Test no-op PATCH requests + +marks: + - usefixtures: + - ot3_server_base_url + - parametrize: + key: enabled + vals: + - true + - false + +stages: + - name: Set initial settings + request: + method: PATCH + url: '{ot3_server_base_url}/errorRecovery/settings' + json: + data: + enabled: '{enabled}' + response: + save: + json: + initial_response: data + + - name: Send a no-op PATCH and make sure it doesn't change anything + request: + method: PATCH + url: '{ot3_server_base_url}/errorRecovery/settings' + json: + data: {} + response: + json: + data: !force_original_structure '{initial_response}' diff --git a/robot-server/tests/integration/robot_client.py b/robot-server/tests/integration/robot_client.py index 3c0af8c8bf0..9db9409d90a 100644 --- a/robot-server/tests/integration/robot_client.py +++ b/robot-server/tests/integration/robot_client.py @@ -377,6 +377,13 @@ async def delete_all_client_data(self) -> Response: response.raise_for_status() return response + async def delete_error_recovery_settings(self) -> Response: + response = await self.httpx_client.delete( + url=f"{self.base_url}/errorRecovery/settings" + ) + response.raise_for_status() + return response + async def poll_until_run_completes( robot_client: RobotClient, run_id: str, poll_interval: float = _RUN_POLL_INTERVAL diff --git a/robot-server/tests/persistence/test_tables.py b/robot-server/tests/persistence/test_tables.py index bb3a5ece78e..a970bb86d89 100644 --- a/robot-server/tests/persistence/test_tables.py +++ b/robot-server/tests/persistence/test_tables.py @@ -144,10 +144,10 @@ """, """ CREATE TABLE boolean_setting ( - "key" VARCHAR(22) NOT NULL, + "key" VARCHAR(21) NOT NULL, value BOOLEAN NOT NULL, PRIMARY KEY ("key"), - CONSTRAINT booleansettingkey CHECK ("key" IN ('disable_error_recovery')) + CONSTRAINT booleansettingkey CHECK ("key" IN ('enable_error_recovery')) ) """, ] diff --git a/robot-server/tests/runs/test_error_recovery_setting_store.py b/robot-server/tests/runs/test_error_recovery_setting_store.py deleted file mode 100644 index fae8bb76705..00000000000 --- a/robot-server/tests/runs/test_error_recovery_setting_store.py +++ /dev/null @@ -1,29 +0,0 @@ -"""Tests for error_recovery_setting_store.""" - - -from robot_server.runs.error_recovery_setting_store import ErrorRecoverySettingStore - -import pytest -import sqlalchemy - - -@pytest.fixture -def subject( - sql_engine: sqlalchemy.engine.Engine, -) -> ErrorRecoverySettingStore: - """Return a test subject.""" - return ErrorRecoverySettingStore(sql_engine=sql_engine) - - -def test_error_recovery_setting_store(subject: ErrorRecoverySettingStore) -> None: - """Test `ErrorRecoverySettingStore`.""" - assert subject.get_is_disabled() is None - - subject.set_is_disabled(is_disabled=False) - assert subject.get_is_disabled() is False - - subject.set_is_disabled(is_disabled=True) - assert subject.get_is_disabled() is True - - subject.set_is_disabled(is_disabled=None) - assert subject.get_is_disabled() is None From d9655d6ef3afc1b80f3c3bfb1a8a7bd4125c853b Mon Sep 17 00:00:00 2001 From: Jethary Alcid <66035149+jerader@users.noreply.github.com> Date: Mon, 30 Sep 2024 15:48:37 -0400 Subject: [PATCH 11/17] fix(labware-library): hide the descriptive icons for now (#16388) closes RQA-3251 --- .../src/components/ui/TableTitle.tsx | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/labware-library/src/components/ui/TableTitle.tsx b/labware-library/src/components/ui/TableTitle.tsx index 1ea3178ba82..c360b3aef5b 100644 --- a/labware-library/src/components/ui/TableTitle.tsx +++ b/labware-library/src/components/ui/TableTitle.tsx @@ -1,8 +1,7 @@ // Table Title with expandable measurement diagrams -import * as React from 'react' import cx from 'classnames' import { LabelText, LABEL_LEFT } from './LabelText' -import { ClickableIcon } from './ClickableIcon' +// import { ClickableIcon } from './ClickableIcon' import styles from './styles.module.css' interface TableTitleProps { @@ -11,30 +10,32 @@ interface TableTitleProps { } export function TableTitle(props: TableTitleProps): JSX.Element { - const [guideVisible, setGuideVisible] = React.useState(false) - const toggleGuide = (): void => { - setGuideVisible(!guideVisible) - } + // TODO(ja, 9/30/24): fix the actual bug. temporarily commenting it out for now since the images + // rendered by the toggleGuide were not being copied into the build folder + // see https://opentrons.atlassian.net/browse/AUTH-885 for more info + // const [guideVisible, setGuideVisible] = React.useState(false) + // const toggleGuide = (): void => { + // setGuideVisible(!guideVisible) + // } + // const iconClassName = cx(styles.info_button, { + // [styles.active]: guideVisible, + // }) const { label, diagram } = props - const iconClassName = cx(styles.info_button, { - [styles.active]: guideVisible, - }) - const contentClassName = cx(styles.expandable_content, { - [styles.open]: guideVisible, + [styles.open]: false, }) return ( <>
{label} - + /> */}
{diagram}
From 3300cb1f63ff8c0f45cd495a5a75a577cdec9539 Mon Sep 17 00:00:00 2001 From: koji Date: Tue, 1 Oct 2024 09:32:26 -0400 Subject: [PATCH 12/17] fix(protocol-designer): fix liquids button in deck setup (#16380) * fix(protocol-designer): fix liquids button in deck setup --- components/src/icons/icon-data.ts | 5 +++++ .../pages/Designer/__tests__/Designer.test.tsx | 4 ++-- protocol-designer/src/pages/Designer/index.tsx | 16 ++++++---------- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/components/src/icons/icon-data.ts b/components/src/icons/icon-data.ts index c7e1f135a52..502e75cabfe 100644 --- a/components/src/icons/icon-data.ts +++ b/components/src/icons/icon-data.ts @@ -761,6 +761,11 @@ export const ICON_DATA_BY_NAME = { 'M12,20A6,6 0 0,1 6,14C6,10 12,3.25 12,3.25C12,3.25 18,10 18,14A6,6 0 0,1 12,20Z', viewBox: '0 0 24 24', }, + 'water-drop': { + path: + 'M12 22C9.72 22 7.81 21.22 6.29 19.65C4.76 18.08 4 16.13 4 13.8C4 12.13 4.66 10.32 5.99 8.36C7.32 6.4 9.32 4.28 12 2C14.68 4.28 16.69 6.4 18.01 8.36C19.33 10.32 20 12.13 20 13.8C20 16.13 19.24 18.08 17.71 19.65C16.18 21.22 14.28 22 12 22ZM12 20C13.73 20 15.17 19.41 16.3 18.24C17.43 17.07 18 15.59 18 13.8C18 12.58 17.5 11.21 16.49 9.67C15.48 8.14 13.99 6.46 12 4.64C10.02 6.46 8.52 8.13 7.51 9.67C6.5 11.2 6 12.58 6 13.8C6 15.58 6.57 17.06 7.7 18.24C8.83 19.42 10.27 20 12 20Z M8.55 11.42C7.95 12.38 7.63 13.29 7.63 14.12C7.63 15.4 8.05 16.46 8.88 17.32C9.71 18.18 10.75 18.6 12 18.6C13.25 18.6 14.29 18.17 15.12 17.32C15.95 16.46 16.37 15.4 16.37 14.12C16.37 13.29 16.05 12.39 15.45 11.42H8.55Z', + viewBox: '0 0 24 24', + }, wifi: { path: 'M3.16848 9.22683C4.91915 7.43693 7.33915 6.33359 9.99973 6.33359C12.6604 6.33359 15.0804 7.43697 16.8311 9.22693L17.9996 8.03818C15.9522 5.95529 13.1239 4.66699 9.99973 4.66699C6.87563 4.66699 4.0473 5.95525 2 8.03809L3.16848 9.22683ZM6.1685 12.2783C7.15141 11.2696 8.51069 10.6495 9.99953 10.6495C11.4886 10.6495 12.848 11.2698 13.8309 12.2787L14.9994 11.0899C13.7199 9.78811 11.9521 8.98291 9.99953 8.98291C8.04712 8.98291 6.27954 9.78795 5 11.0895L6.1685 12.2783ZM10.0002 14.9654C9.6831 14.9654 9.38403 15.1024 9.16876 15.3306L8.00012 14.1417C8.51196 13.6209 9.2191 13.2988 10.0002 13.2988C10.7811 13.2988 11.4881 13.6208 11.9999 14.1414L10.8313 15.3303C10.6161 15.1023 10.3171 14.9654 10.0002 14.9654Z', diff --git a/protocol-designer/src/pages/Designer/__tests__/Designer.test.tsx b/protocol-designer/src/pages/Designer/__tests__/Designer.test.tsx index dfb7f7ea3cd..2b496cbe5e1 100644 --- a/protocol-designer/src/pages/Designer/__tests__/Designer.test.tsx +++ b/protocol-designer/src/pages/Designer/__tests__/Designer.test.tsx @@ -77,14 +77,14 @@ describe('Designer', () => { screen.getByText('Edit protocol') screen.getByText('Protocol steps') screen.getByText('Protocol starting deck') - screen.getByText('Liquids') + screen.getByTestId('water-drop') fireEvent.click(screen.getByRole('button', { name: 'Done' })) expect(mockNavigate).toHaveBeenCalledWith('/overview') }) it('renders the liquids button overflow menu', () => { render() - fireEvent.click(screen.getByText('Liquids')) + fireEvent.click(screen.getByTestId('water-drop')) screen.getByText('mock LiquidsOverflowMenu') }) diff --git a/protocol-designer/src/pages/Designer/index.tsx b/protocol-designer/src/pages/Designer/index.tsx index e830c59f9b9..4bf18d80326 100644 --- a/protocol-designer/src/pages/Designer/index.tsx +++ b/protocol-designer/src/pages/Designer/index.tsx @@ -5,16 +5,15 @@ import { useNavigate } from 'react-router-dom' import { ALIGN_CENTER, ALIGN_END, + Btn, COLORS, DIRECTION_COLUMN, Flex, INFO_TOAST, Icon, JUSTIFY_SPACE_BETWEEN, - PrimaryButton, SPACING, SecondaryButton, - StyledText, Tabs, ToggleGroup, useOnClickOutside, @@ -169,18 +168,15 @@ export function Designer(): JSX.Element { - { showLiquidOverflowMenu(true) }} > - - - - {t('liquids')} - - - + + { if (hasTrashEntity) { From d7ae1cd1d9643c7a99e4ef1e564d99662f63068c Mon Sep 17 00:00:00 2001 From: koji Date: Tue, 1 Oct 2024 09:38:47 -0400 Subject: [PATCH 13/17] fix(protocol-designer): fix deck map hardware rendering issue (#16354) * fix(protocol-designer): fix deck map hardware rendering issue --- .../localization/en/starting_deck_state.json | 1 + .../__tests__/ProtocolMetadataNav.test.tsx | 54 ++++ .../organisms/ProtocolMetadataNav/index.tsx | 20 +- .../Designer/DeckSetup/DeckSetupTools.tsx | 304 +++++++++--------- .../src/pages/Designer/index.tsx | 6 +- 5 files changed, 228 insertions(+), 157 deletions(-) create mode 100644 protocol-designer/src/organisms/ProtocolMetadataNav/__tests__/ProtocolMetadataNav.test.tsx diff --git a/protocol-designer/src/assets/localization/en/starting_deck_state.json b/protocol-designer/src/assets/localization/en/starting_deck_state.json index 43b6499b218..8fb6ecb1e73 100644 --- a/protocol-designer/src/assets/localization/en/starting_deck_state.json +++ b/protocol-designer/src/assets/localization/en/starting_deck_state.json @@ -2,6 +2,7 @@ "adapter_compatible_lab": "Adapter compatible labware", "adapter": "Adapter", "add_fixture": "Add a fixture", + "add_hardware_labware": "Add hardware/labware", "add_hw_lw": "Add hardware/labware", "add_labware": "Add labware", "add_liquid": "Add liquid", diff --git a/protocol-designer/src/organisms/ProtocolMetadataNav/__tests__/ProtocolMetadataNav.test.tsx b/protocol-designer/src/organisms/ProtocolMetadataNav/__tests__/ProtocolMetadataNav.test.tsx new file mode 100644 index 00000000000..4ce46f1d7c2 --- /dev/null +++ b/protocol-designer/src/organisms/ProtocolMetadataNav/__tests__/ProtocolMetadataNav.test.tsx @@ -0,0 +1,54 @@ +import { screen } from '@testing-library/react' +import { describe, it, beforeEach, vi } from 'vitest' +import { renderWithProviders } from '../../../__testing-utils__' +import { i18n } from '../../../assets/localization' +import { getFileMetadata } from '../../../file-data/selectors' + +import { ProtocolMetadataNav } from '..' + +vi.mock('../../../file-data/selectors') + +const render = (props: React.ComponentProps) => { + return renderWithProviders(, { + i18nInstance: i18n, + }) +} + +describe('ProtocolMetadataNav', () => { + let props: React.ComponentProps + beforeEach(() => { + props = { + isAddingHardwareOrLabware: false, + } + vi.mocked(getFileMetadata).mockReturnValue({ + protocolName: 'mockProtocolName', + created: 123, + }) + }) + + it('should render protocol name and edit protocol - protocol name', () => { + render(props) + screen.getByText('mockProtocolName') + screen.getByText('Edit protocol') + }) + it('should render protocol name and edit protocol - no protocol name', () => { + vi.mocked(getFileMetadata).mockReturnValue({}) + render(props) + screen.getByText('Untitled protocol') + screen.getByText('Edit protocol') + }) + + it('should render protocol name and add hardware/labware - protocol name', () => { + props = { isAddingHardwareOrLabware: true } + render(props) + screen.getByText('mockProtocolName') + screen.getByText('Add hardware/labware') + }) + it('should render protocol name and add hardware/labware - no protocol name', () => { + props = { isAddingHardwareOrLabware: true } + vi.mocked(getFileMetadata).mockReturnValue({}) + render(props) + screen.getByText('Untitled protocol') + screen.getByText('Add hardware/labware') + }) +}) diff --git a/protocol-designer/src/organisms/ProtocolMetadataNav/index.tsx b/protocol-designer/src/organisms/ProtocolMetadataNav/index.tsx index c9cab202149..a828642d29d 100644 --- a/protocol-designer/src/organisms/ProtocolMetadataNav/index.tsx +++ b/protocol-designer/src/organisms/ProtocolMetadataNav/index.tsx @@ -5,11 +5,18 @@ import { DIRECTION_COLUMN, Flex, JUSTIFY_CENTER, + JUSTIFY_FLEX_START, StyledText, } from '@opentrons/components' import { getFileMetadata } from '../../file-data/selectors' -export function ProtocolMetadataNav(): JSX.Element { +interface ProtocolMetadataNavProps { + isAddingHardwareOrLabware?: boolean +} + +export function ProtocolMetadataNav({ + isAddingHardwareOrLabware = false, +}: ProtocolMetadataNavProps): JSX.Element { const metadata = useSelector(getFileMetadata) const { t } = useTranslation('starting_deck_state') @@ -20,9 +27,16 @@ export function ProtocolMetadataNav(): JSX.Element { ? metadata?.protocolName : t('untitled_protocol')} - + - {t('edit_protocol')} + {isAddingHardwareOrLabware + ? t('add_hardware_labware') + : t('edit_protocol')} diff --git a/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupTools.tsx b/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupTools.tsx index 51fdfdf49da..77311f2e3b2 100644 --- a/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupTools.tsx +++ b/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupTools.tsx @@ -3,8 +3,8 @@ import { useTranslation } from 'react-i18next' import { useDispatch, useSelector } from 'react-redux' import { ALIGN_CENTER, - DIRECTION_COLUMN, DeckInfoLabel, + DIRECTION_COLUMN, Flex, ModuleIcon, RadioButton, @@ -15,9 +15,9 @@ import { } from '@opentrons/components' import { FLEX_ROBOT_TYPE, - OT2_ROBOT_TYPE, getModuleDisplayName, getModuleType, + OT2_ROBOT_TYPE, } from '@opentrons/shared-data' import { getRobotType } from '../../../file-data/selectors' @@ -30,12 +30,12 @@ import { getDeckSetupForActiveItem } from '../../../top-selectors/labware-locati import { createContainer, deleteContainer, + editSlotInfo, selectFixture, selectLabware, selectModule, - editSlotInfo, - selectZoomedIntoSlot, selectNestedLabware, + selectZoomedIntoSlot, } from '../../../labware-ingred/actions' import { getEnableAbsorbanceReader, @@ -80,15 +80,15 @@ export function DeckSetupTools(props: DeckSetupToolsProps): JSX.Element | null { selectedNestedLabwareDefUri, } = selectedSlotInfo const { slot, cutout } = selectedSlot - const [selectedHardware, setHardware] = useState< + const [selectedHardware, setSelectedHardware] = useState< ModuleModel | Fixture | null >(null) // initialize the previously selected hardware because for some reason it does not // work initiating it in the above useState useEffect(() => { - if (selectedModuleModel || selectedFixture) { - setHardware(selectedModuleModel ?? selectedFixture ?? null) + if (selectedModuleModel !== null || selectedFixture != null) { + setSelectedHardware(selectedModuleModel ?? selectedFixture ?? null) } }, [selectedModuleModel, selectedFixture]) @@ -182,7 +182,7 @@ export function DeckSetupTools(props: DeckSetupToolsProps): JSX.Element | null { } } handleResetToolbox() - setHardware(null) + setSelectedHardware(null) } const handleConfirm = (): void => { @@ -267,161 +267,159 @@ export function DeckSetupTools(props: DeckSetupToolsProps): JSX.Element | null { }} confirmButtonText={t('done')} > - + {slot !== 'offDeck' ? : null} {tab === 'hardware' ? ( - <> - - - - {t('add_module')} - - - {moduleModels?.map(model => { - const modelSomewhereOnDeck = Object.values( - deckSetupModules - ).filter( - module => module.model === model && module.slot !== slot - ) - const typeSomewhereOnDeck = Object.values( - deckSetupModules - ).filter( - module => - module.type === getModuleType(model) && module.slot !== slot - ) - const moamModels = enableMoam - ? MOAM_MODELS - : MOAM_MODELS_WITH_FF + + + + {t('add_module')} + + + {moduleModels?.map(model => { + const modelSomewhereOnDeck = Object.values( + deckSetupModules + ).filter( + module => module.model === model && module.slot !== slot + ) + const typeSomewhereOnDeck = Object.values( + deckSetupModules + ).filter( + module => + module.type === getModuleType(model) && + module.slot !== slot + ) + const moamModels = enableMoam + ? MOAM_MODELS + : MOAM_MODELS_WITH_FF - const collisionError = getDeckErrors({ - modules: deckSetupModules, - selectedSlot: slot, - selectedModel: model, - labware: deckSetupLabware, - robotType: robotType, - }) + const collisionError = getDeckErrors({ + modules: deckSetupModules, + selectedSlot: slot, + selectedModel: model, + labware: deckSetupLabware, + robotType, + }) - return ( - { - if (onDeckProps?.setHoveredModule != null) { - onDeckProps.setHoveredModule(null) - } - }} - setHovered={() => { - if (onDeckProps?.setHoveredModule != null) { - onDeckProps.setHoveredModule(model) - } - }} - largeDesktopBorderRadius - buttonLabel={ - - - - {getModuleDisplayName(model)} - - - } - key={`${model}_${slot}`} - buttonValue={model} - onChange={() => { - if ( - modelSomewhereOnDeck.length === 1 && - !moamModels.includes(model) && - robotType === FLEX_ROBOT_TYPE - ) { - makeSnackbar( - t('one_item', { - hardware: getModuleDisplayName(model), - }) as string - ) - } else if ( - typeSomewhereOnDeck.length > 0 && - robotType === OT2_ROBOT_TYPE - ) { - makeSnackbar( - t('one_item', { - hardware: t( - `shared:${getModuleType(model).toLowerCase()}` - ), - }) as string - ) - } else if (collisionError != null) { - makeSnackbar(t(`${collisionError}`) as string) - } else { - setHardware(model) - dispatch(selectModule({ moduleModel: model })) - dispatch(selectLabware({ labwareDefUri: null })) - dispatch( - selectNestedLabware({ nestedLabwareDefUri: null }) - ) + return ( + { + if (onDeckProps?.setHoveredModule != null) { + onDeckProps.setHoveredModule(null) + } + }} + setHovered={() => { + if (onDeckProps?.setHoveredModule != null) { + onDeckProps.setHoveredModule(model) + } + }} + largeDesktopBorderRadius + buttonLabel={ + + + + {getModuleDisplayName(model)} + + } - }} - isSelected={model === selectedHardware} - /> - ) - })} + key={`${model}_${slot}`} + buttonValue={model} + onChange={() => { + if ( + modelSomewhereOnDeck.length === 1 && + !moamModels.includes(model) && + robotType === FLEX_ROBOT_TYPE + ) { + makeSnackbar( + t('one_item', { + hardware: getModuleDisplayName(model), + }) as string + ) + } else if ( + typeSomewhereOnDeck.length > 0 && + robotType === OT2_ROBOT_TYPE + ) { + makeSnackbar( + t('one_item', { + hardware: t( + `shared:${getModuleType(model).toLowerCase()}` + ), + }) as string + ) + } else if (collisionError != null) { + makeSnackbar(t(`${collisionError}`) as string) + } else { + setSelectedHardware(model) + dispatch(selectFixture({ fixture: null })) + dispatch(selectModule({ moduleModel: model })) + dispatch(selectLabware({ labwareDefUri: null })) + dispatch( + selectNestedLabware({ nestedLabwareDefUri: null }) + ) + } + }} + isSelected={model === selectedHardware} + /> + ) + })} + {robotType === OT2_ROBOT_TYPE || fixtures.length === 0 ? null : ( - - - - {t('add_fixture')} - + + + {t('add_fixture')} + + + {fixtures.map(fixture => ( + { + if (onDeckProps?.setHoveredFixture != null) { + onDeckProps.setHoveredFixture(null) + } + }} + setHovered={() => { + if (onDeckProps?.setHoveredFixture != null) { + onDeckProps.setHoveredFixture(fixture) + } + }} + largeDesktopBorderRadius + buttonLabel={t(`shared:${fixture}`)} + key={`${fixture}_${slot}`} + buttonValue={fixture} + onChange={() => { + // delete this when multiple trash bins are supported + if (fixture === 'trashBin' && hasTrash) { + makeSnackbar( + t('one_item', { + hardware: t('shared:trashBin'), + }) as string + ) + } else { + setSelectedHardware(fixture) + dispatch(selectModule({ moduleModel: null })) + dispatch(selectFixture({ fixture })) + dispatch(selectLabware({ labwareDefUri: null })) + dispatch( + selectNestedLabware({ nestedLabwareDefUri: null }) + ) + } + }} + isSelected={fixture === selectedHardware} + /> + ))} - {fixtures.map(fixture => ( - { - if (onDeckProps?.setHoveredFixture != null) { - onDeckProps.setHoveredFixture(null) - } - }} - setHovered={() => { - if (onDeckProps?.setHoveredFixture != null) { - onDeckProps.setHoveredFixture(fixture) - } - }} - largeDesktopBorderRadius - buttonLabel={t(`shared:${fixture}`)} - key={`${fixture}_${slot}`} - buttonValue={fixture} - onChange={() => { - // delete this when multiple trash bins are supported - if (fixture === 'trashBin' && hasTrash) { - makeSnackbar( - t('one_item', { - hardware: t('shared:trashBin'), - }) as string - ) - } else { - setHardware(fixture) - dispatch(selectFixture({ fixture: fixture })) - dispatch(selectLabware({ labwareDefUri: null })) - dispatch( - selectNestedLabware({ nestedLabwareDefUri: null }) - ) - } - }} - isSelected={fixture === selectedHardware} - /> - ))} )} - + ) : ( )} diff --git a/protocol-designer/src/pages/Designer/index.tsx b/protocol-designer/src/pages/Designer/index.tsx index 4bf18d80326..6beba5c1534 100644 --- a/protocol-designer/src/pages/Designer/index.tsx +++ b/protocol-designer/src/pages/Designer/index.tsx @@ -165,7 +165,11 @@ export function Designer(): JSX.Element { {zoomIn.slot != null ? null : ( )} - + From 88d72377efab5e48f9df94a042f47871e8c9794f Mon Sep 17 00:00:00 2001 From: Jethary Alcid <66035149+jerader@users.noreply.github.com> Date: Tue, 1 Oct 2024 09:55:33 -0400 Subject: [PATCH 14/17] feat(protocol-designer): step edit form opens on edit button (#16378) closes Auth-879 --- .../localization/en/protocol_steps.json | 2 + .../Timeline/ConnectedStepInfo.tsx | 31 +++- .../ProtocolSteps/Timeline/StepContainer.tsx | 22 ++- .../Timeline/StepOverflowMenu.tsx | 157 ++++++++++++------ .../Timeline/TimelineToolbox.tsx | 2 +- .../__tests__/StepOverflowMenu.test.tsx | 42 ++++- .../src/ui/steps/actions/actions.ts | 30 ++++ 7 files changed, 210 insertions(+), 76 deletions(-) diff --git a/protocol-designer/src/assets/localization/en/protocol_steps.json b/protocol-designer/src/assets/localization/en/protocol_steps.json index fd663b8d6c4..f4eeaf8ff57 100644 --- a/protocol-designer/src/assets/localization/en/protocol_steps.json +++ b/protocol-designer/src/assets/localization/en/protocol_steps.json @@ -1,8 +1,10 @@ { + "add_details": "Add step details", "change_tips": "Change tips", "default_tip_option": "Default - get next tip", "delete": "Delete step", "duplicate": "Duplicate step", + "edit_step": "Edit step", "final_deck_state": "Final deck state", "heater_shaker_settings": "Heater-shaker settings", "module": "Module", diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/ConnectedStepInfo.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/ConnectedStepInfo.tsx index fde2e8348e1..d2b2e8e0f32 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/ConnectedStepInfo.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/ConnectedStepInfo.tsx @@ -23,9 +23,9 @@ import { StepContainer } from './StepContainer' import type { ThunkDispatch } from 'redux-thunk' import type { HoverOnStepAction } from '../../../../ui/steps' -import type { DeleteModalType } from '../../../../components/modals/ConfirmDeleteModal' import type { StepIdType } from '../../../../form-types' import type { BaseState, ThunkAction } from '../../../../types' +import type { DeleteModalType } from '../../../../components/modals/ConfirmDeleteModal' export interface ConnectedStepInfoProps { stepId: StepIdType @@ -63,27 +63,31 @@ export function ConnectedStepInfo(props: ConnectedStepInfoProps): JSX.Element { const selected: boolean = multiSelectItemIds?.length ? multiSelectItemIds.includes(stepId) : selectedStepId === stepId - const currentFormIsPresaved = useSelector( stepFormSelectors.getCurrentFormIsPresaved ) const singleEditFormHasUnsavedChanges = useSelector( stepFormSelectors.getCurrentFormHasUnsavedChanges ) - const selectStep = (): ThunkAction => + dispatch(stepsActions.resetSelectStep(stepId)) + const selectStepOnDoubleClick = (): ThunkAction => dispatch(stepsActions.selectStep(stepId)) const highlightStep = (): HoverOnStepAction => dispatch(stepsActions.hoverOnStep(stepId)) const unhighlightStep = (): HoverOnStepAction => dispatch(stepsActions.hoverOnStep(null)) - const handleStepItemSelection = (): void => { - selectStep() - } - + const { + confirm: confirmDoubleClick, + showConfirmation: showConfirmationDoubleClick, + cancel: cancelDoubleClick, + } = useConditionalConfirm( + selectStepOnDoubleClick, + currentFormIsPresaved || singleEditFormHasUnsavedChanges + ) const { confirm, showConfirmation, cancel } = useConditionalConfirm( - handleStepItemSelection, + selectStep, currentFormIsPresaved || singleEditFormHasUnsavedChanges ) @@ -94,10 +98,20 @@ export function ConnectedStepInfo(props: ConnectedStepInfoProps): JSX.Element { return CLOSE_STEP_FORM_WITH_CHANGES } } + const iconName = stepIconsByType[step.stepType] return ( <> + {/* TODO: update this modal */} + {showConfirmationDoubleClick && ( + + )} + {/* TODO: update this modal */} {showConfirmation && ( void + onDoubleClick?: (event: React.MouseEvent) => void onMouseEnter?: (event: React.MouseEvent) => void onMouseLeave?: (event: React.MouseEvent) => void selected?: boolean @@ -46,6 +44,7 @@ export function StepContainer(props: StepContainerProps): JSX.Element { const { stepId, iconName, + onDoubleClick, onMouseEnter, onMouseLeave, selected, @@ -56,7 +55,6 @@ export function StepContainer(props: StepContainerProps): JSX.Element { hasError = false, isStepAfterError = false, } = props - const formData = useSelector(getUnsavedForm) const [top, setTop] = React.useState(0) const menuRootRef = React.useRef(null) const [stepOverflowMenu, setStepOverflowMenu] = React.useState(false) @@ -121,10 +119,11 @@ export function StepContainer(props: StepContainerProps): JSX.Element { }} > {iconName && ( )} - {formData != null ? null : ( - - {capitalizeFirstLetterAfterNumber(title)} - - )} + + {capitalizeFirstLetterAfterNumber(title)} + - {selected && !isStartingOrEndingState && formData == null ? ( + {selected && !isStartingOrEndingState ? ( top: number + setStepOverflowMenu: React.Dispatch> } export function StepOverflowMenu(props: StepOverflowMenuProps): JSX.Element { - const { stepId, menuRootRef, top } = props + const { stepId, menuRootRef, top, setStepOverflowMenu } = props const { t } = useTranslation('protocol_steps') const dispatch = useDispatch>() const deleteStep = (stepId: StepIdType): void => { dispatch(steplistActions.deleteStep(stepId)) } + const formData = useSelector(getUnsavedForm) + const currentFormIsPresaved = useSelector(getCurrentFormIsPresaved) + const singleEditFormHasUnsavedChanges = useSelector( + getCurrentFormHasUnsavedChanges + ) const duplicateStep = ( stepId: StepIdType ): ReturnType => dispatch(stepsActions.duplicateStep(stepId)) + const handleStepItemSelection = (): void => { + dispatch(populateForm(stepId)) + setStepOverflowMenu(false) + } + const handleDelete = (): void => { + if (stepId != null) { + deleteStep(stepId) + } else { + console.warn( + 'something went wrong, cannot delete a step without a step id' + ) + } + } + + const { confirm, showConfirmation, cancel } = useConditionalConfirm( + handleStepItemSelection, + currentFormIsPresaved || singleEditFormHasUnsavedChanges + ) + + const { + confirm: confirmDelete, + showConfirmation: showDeleteConfirmation, + cancel: cancelDelete, + } = useConditionalConfirm(handleDelete, true) + + const getModalType = (): DeleteModalType => { + if (currentFormIsPresaved) { + return CLOSE_UNSAVED_STEP_FORM + } else { + return CLOSE_STEP_FORM_WITH_CHANGES + } + } + return ( - { - e.preventDefault() - e.stopPropagation() - }} - > - { - console.log('wire this up') - }} - > - {t('rename')} - - { - console.log('wire this up') - }} - > - {t('view_commands')} - - { - duplicateStep(stepId) - }} - > - {t('duplicate')} - - - { - deleteStep(stepId) + <> + {/* TODO: update this modal */} + {showConfirmation && ( + + )} + {/* TODO: update this modal */} + {showDeleteConfirmation && ( + + )} + { + e.preventDefault() + e.stopPropagation() }} > - {t('delete')} - - + {formData != null ? null : ( + {t('edit_step')} + )} + { + console.log('wire this up') + }} + > + {t('view_commands')} + + { + duplicateStep(stepId) + }} + > + {t('duplicate')} + + + {t('delete')} + + ) } diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/TimelineToolbox.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/TimelineToolbox.tsx index caf1c19f740..7aeabd53802 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/TimelineToolbox.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/TimelineToolbox.tsx @@ -61,7 +61,7 @@ export const TimelineToolbox = (): JSX.Element => { return ( { + const actual = await importOriginal() + return { + ...actual, + useConditionalConfirm: vi.fn(() => ({ + confirm: mockConfirm, + showConfirmation: true, + cancel: mockCancel, + })), + } +}) const render = (props: React.ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, @@ -24,17 +45,24 @@ describe('StepOverflowMenu', () => { stepId: 'mockId', top: 0, menuRootRef: { current: null }, + setStepOverflowMenu: vi.fn(), } + vi.mocked(getCurrentFormIsPresaved).mockReturnValue(false) + vi.mocked(getCurrentFormHasUnsavedChanges).mockReturnValue(false) + vi.mocked(getUnsavedForm).mockReturnValue(null) }) it('renders each button and clicking them calls the action', () => { render(props) - fireEvent.click(screen.getByText('Delete step')) - expect(vi.mocked(deleteStep)).toHaveBeenCalled() + fireEvent.click(screen.getAllByText('Delete step')[0]) + screen.getByText('Are you sure you want to delete this step?') + fireEvent.click(screen.getByText('delete step')) + expect(mockConfirm).toHaveBeenCalled() fireEvent.click(screen.getByText('Duplicate step')) expect(vi.mocked(duplicateStep)).toHaveBeenCalled() + fireEvent.click(screen.getByText('Edit step')) + expect(mockConfirm).toHaveBeenCalled() fireEvent.click(screen.getByText('View commands')) - fireEvent.click(screen.getByText('Rename step')) - // TODO: wire up view commands and rename + // TODO: wire up view commands }) }) diff --git a/protocol-designer/src/ui/steps/actions/actions.ts b/protocol-designer/src/ui/steps/actions/actions.ts index bdd41e323ae..7fecbcf0de1 100644 --- a/protocol-designer/src/ui/steps/actions/actions.ts +++ b/protocol-designer/src/ui/steps/actions/actions.ts @@ -101,6 +101,35 @@ export const clearWellSelectionLabwareKey = (): ClearWellSelectionLabwareKeyActi type: 'CLEAR_WELL_SELECTION_LABWARE_KEY', payload: null, }) +export const resetSelectStep = (stepId: StepIdType): ThunkAction => ( + dispatch: ThunkDispatch, + getState: GetState +) => { + const selectStepAction: SelectStepAction = { + type: 'SELECT_STEP', + payload: stepId, + } + dispatch(selectStepAction) + dispatch({ + type: 'POPULATE_FORM', + payload: null, + }) + resetScrollElements() +} + +export const populateForm = (stepId: StepIdType): ThunkAction => ( + dispatch: ThunkDispatch, + getState: GetState +) => { + const state = getState() + const formData = { ...stepFormSelectors.getSavedStepForms(state)[stepId] } + dispatch({ + type: 'POPULATE_FORM', + payload: formData, + }) + resetScrollElements() +} + export const selectStep = (stepId: StepIdType): ThunkAction => ( dispatch: ThunkDispatch, getState: GetState @@ -118,6 +147,7 @@ export const selectStep = (stepId: StepIdType): ThunkAction => ( }) resetScrollElements() } + // NOTE(sa, 2020-12-11): this is a thunk so that we can populate the batch edit form with things later export const selectMultipleSteps = ( stepIds: StepIdType[], From 680ee12f8408498c0d6ce39ad8b7a44eaff42d80 Mon Sep 17 00:00:00 2001 From: koji Date: Tue, 1 Oct 2024 10:38:00 -0400 Subject: [PATCH 15/17] fix(components, protocol-designer): Fix text wrap issues in PD (#16385) * fix(components, protocol-designer): Fix text wrap issues in PD --- .../ListItemChildren/ListItemDescriptor.tsx | 8 +- .../src/molecules/DropdownMenu/index.tsx | 15 ++-- .../ProtocolOverview/LiquidDefinitions.tsx | 71 +++++++++++++++++ .../__tests__/LiquidDefinitions.test.tsx | 78 +++++++++++++++++++ .../__tests__/ProtocolOverview.test.tsx | 8 +- .../src/pages/ProtocolOverview/index.tsx | 47 ++--------- 6 files changed, 179 insertions(+), 48 deletions(-) create mode 100644 protocol-designer/src/pages/ProtocolOverview/LiquidDefinitions.tsx create mode 100644 protocol-designer/src/pages/ProtocolOverview/__tests__/LiquidDefinitions.test.tsx diff --git a/components/src/atoms/ListItem/ListItemChildren/ListItemDescriptor.tsx b/components/src/atoms/ListItem/ListItemChildren/ListItemDescriptor.tsx index 7560bf25e5e..51d9ca9e181 100644 --- a/components/src/atoms/ListItem/ListItemChildren/ListItemDescriptor.tsx +++ b/components/src/atoms/ListItem/ListItemChildren/ListItemDescriptor.tsx @@ -1,6 +1,6 @@ import { Flex } from '../../../primitives' import { - ALIGN_CENTER, + ALIGN_FLEX_START, DIRECTION_ROW, FLEX_AUTO, JUSTIFY_SPACE_BETWEEN, @@ -22,7 +22,7 @@ export const ListItemDescriptor = ( flexDirection={DIRECTION_ROW} gridGap={SPACING.spacing8} width="100%" - alignItems={ALIGN_CENTER} + alignItems={ALIGN_FLEX_START} justifyContent={type === 'mini' ? JUSTIFY_SPACE_BETWEEN : 'none'} padding={ type === 'mini' @@ -36,7 +36,9 @@ export const ListItemDescriptor = ( > {description} - {content} + + {content} + ) } diff --git a/components/src/molecules/DropdownMenu/index.tsx b/components/src/molecules/DropdownMenu/index.tsx index d82aa20b4f8..30a02209121 100644 --- a/components/src/molecules/DropdownMenu/index.tsx +++ b/components/src/molecules/DropdownMenu/index.tsx @@ -9,7 +9,6 @@ import { DIRECTION_COLUMN, DIRECTION_ROW, JUSTIFY_SPACE_BETWEEN, - NO_WRAP, OVERFLOW_AUTO, OVERFLOW_HIDDEN, POSITION_ABSOLUTE, @@ -235,12 +234,9 @@ export function DropdownMenu(props: DropdownMenuProps): JSX.Element { font-weight: ${dropdownType === 'rounded' ? TYPOGRAPHY.pSemiBold : TYPOGRAPHY.pRegular}; - white-space: ${NO_WRAP}; - overflow: ${OVERFLOW_HIDDEN}; - text-overflow: ellipsis; `} > - + {currentOption.name} @@ -311,3 +307,12 @@ export function DropdownMenu(props: DropdownMenuProps): JSX.Element { ) } + +const MENU_TEXT_STYLE = css` + display: -webkit-box; + -webkit-box-orient: vertical; + overflow: ${OVERFLOW_HIDDEN}; + text-overflow: ellipsis; + word-wrap: break-word; + -webkit-line-clamp: 1; +` diff --git a/protocol-designer/src/pages/ProtocolOverview/LiquidDefinitions.tsx b/protocol-designer/src/pages/ProtocolOverview/LiquidDefinitions.tsx new file mode 100644 index 00000000000..03c274ce104 --- /dev/null +++ b/protocol-designer/src/pages/ProtocolOverview/LiquidDefinitions.tsx @@ -0,0 +1,71 @@ +import { useTranslation } from 'react-i18next' +import { css } from 'styled-components' +import { + ALIGN_CENTER, + DIRECTION_COLUMN, + Flex, + InfoScreen, + LiquidIcon, + ListItem, + ListItemDescriptor, + OVERFLOW_HIDDEN, + SPACING, + StyledText, +} from '@opentrons/components' + +import type { AllIngredGroupFields } from '../../labware-ingred/types' + +interface LiquidDefinitionsProps { + allIngredientGroupFields: AllIngredGroupFields +} + +export function LiquidDefinitions({ + allIngredientGroupFields, +}: LiquidDefinitionsProps): JSX.Element { + const { t } = useTranslation('protocol_overview') + return ( + + + {t('liquid_defs')} + + + {Object.keys(allIngredientGroupFields).length > 0 ? ( + Object.values(allIngredientGroupFields).map((liquid, index) => ( + + + + + {liquid.name} + + + } + content={liquid.description ?? t('na')} + /> + + )) + ) : ( + + )} + + + ) +} + +const LIQUID_DEFINITION_TEXT = css` + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 3; + overflow: ${OVERFLOW_HIDDEN}; + text-overflow: ellipsis; +` diff --git a/protocol-designer/src/pages/ProtocolOverview/__tests__/LiquidDefinitions.test.tsx b/protocol-designer/src/pages/ProtocolOverview/__tests__/LiquidDefinitions.test.tsx new file mode 100644 index 00000000000..832cea4d800 --- /dev/null +++ b/protocol-designer/src/pages/ProtocolOverview/__tests__/LiquidDefinitions.test.tsx @@ -0,0 +1,78 @@ +import { describe, it, vi, beforeEach } from 'vitest' +import { screen } from '@testing-library/react' + +import { renderWithProviders } from '../../../__testing-utils__' +import { i18n } from '../../../assets/localization' +import { LiquidDefinitions } from '../LiquidDefinitions' + +import type { ComponentProps } from 'react' +import type { InfoScreen } from '@opentrons/components' + +vi.mock('@opentrons/components', async importOriginal => { + const actual = await importOriginal() + return { + ...actual, + InfoScreen: () =>
mock InfoScreen
, + } +}) + +const mockAllIngredientGroupFields = { + '0': { + name: 'EtOH', + displayColor: '#b925ff', + description: 'Immer fisch Hergestllter EtOH', + serialize: false, + liquidGroupId: '0', + }, + '1': { + name: '10mM Tris pH8,5', + displayColor: '#ffd600', + description: null, + serialize: false, + liquidGroupId: '1', + }, + '2': { + name: 'Amplicon PCR sample + AMPure XP beads', + displayColor: '#9dffd8', + description: '25µl Amplicon PCR + 20 µl AMPure XP beads', + serialize: false, + liquidGroupId: '2', + }, +} + +const render = (props: ComponentProps) => { + return renderWithProviders(, { + i18nInstance: i18n, + }) +} + +describe('LiquidDefinitions', () => { + let props: ComponentProps + + beforeEach(() => { + props = { + allIngredientGroupFields: {}, + } + }) + + it('should render text and InfoScreen if no liquid', () => { + render(props) + screen.getByText('Liquid Definitions') + screen.getByText('mock InfoScreen') + }) + + it('should render liquid information if there are liquids', () => { + props = { + allIngredientGroupFields: mockAllIngredientGroupFields, + } + render(props) + screen.getByText('EtOH') + screen.getByText('Immer fisch Hergestllter EtOH') + + screen.getByText('10mM Tris pH8,5') + screen.getByText('N/A') + + screen.getByText('Amplicon PCR sample + AMPure XP beads') + screen.getByText('25µl Amplicon PCR + 20 µl AMPure XP beads') + }) +}) diff --git a/protocol-designer/src/pages/ProtocolOverview/__tests__/ProtocolOverview.test.tsx b/protocol-designer/src/pages/ProtocolOverview/__tests__/ProtocolOverview.test.tsx index e77515eaba0..471898802f6 100644 --- a/protocol-designer/src/pages/ProtocolOverview/__tests__/ProtocolOverview.test.tsx +++ b/protocol-designer/src/pages/ProtocolOverview/__tests__/ProtocolOverview.test.tsx @@ -16,6 +16,7 @@ import { selectors as labwareIngredSelectors } from '../../../labware-ingred/sel import { ProtocolOverview } from '../index' import { DeckThumbnail } from '../DeckThumbnail' import { OffDeckThumbnail } from '../OffdeckThumbnail' +import { LiquidDefinitions } from '../LiquidDefinitions' import type { NavigateFunction } from 'react-router-dom' @@ -27,6 +28,8 @@ vi.mock('../../../organisms/MaterialsListModal') vi.mock('../../../labware-ingred/selectors') vi.mock('../../../organisms') vi.mock('../../../labware-ingred/selectors') +vi.mock('../LiquidDefinitions') + const mockNavigate = vi.fn() vi.mock('react-router-dom', async importOriginal => { @@ -72,6 +75,9 @@ describe('ProtocolOverview', () => { vi.mocked(OffDeckThumbnail).mockReturnValue(
mock OffdeckThumbnail
) + vi.mocked(LiquidDefinitions).mockReturnValue( +
mock LiquidDefinitions
+ ) }) it('renders each section with text', () => { @@ -101,7 +107,7 @@ describe('ProtocolOverview', () => { screen.getByText('Right pipette') screen.getByText('Extension mount') // liquids - screen.getByText('Liquid Definitions') + screen.getByText('mock LiquidDefinitions') // steps screen.getByText('Protocol steps') }) diff --git a/protocol-designer/src/pages/ProtocolOverview/index.tsx b/protocol-designer/src/pages/ProtocolOverview/index.tsx index 92c2ec27724..0009f4cf624 100644 --- a/protocol-designer/src/pages/ProtocolOverview/index.tsx +++ b/protocol-designer/src/pages/ProtocolOverview/index.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react' +import { Fragment, useState, useEffect } from 'react' import { useTranslation } from 'react-i18next' import { useNavigate } from 'react-router-dom' import { useDispatch, useSelector } from 'react-redux' @@ -16,7 +16,6 @@ import { JUSTIFY_FLEX_END, JUSTIFY_SPACE_BETWEEN, LargeButton, - LiquidIcon, ListItem, ListItemDescriptor, Modal, @@ -58,6 +57,7 @@ import { import { DeckThumbnail } from './DeckThumbnail' import { OffDeckThumbnail } from './OffdeckThumbnail' import { getWarningContent } from './UnusedModalContent' +import { LiquidDefinitions } from './LiquidDefinitions' import type { CreateCommand, PipetteName } from '@opentrons/shared-data' import type { DeckSlot } from '@opentrons/step-generation' @@ -223,8 +223,9 @@ export function ProtocolOverview(): JSX.Element { const cancelModal = (): void => { setShowExportWarningModal(false) } + return ( - <> + {showEditMetadataModal ? ( { @@ -449,41 +450,9 @@ export function ProtocolOverview(): JSX.Element { ) : null} - - - {t('liquid_defs')} - - - {Object.keys(allIngredientGroupFields).length > 0 ? ( - Object.values(allIngredientGroupFields).map( - (liquid, index) => ( - - - - - {liquid.name} - - - } - content={liquid.description ?? t('na')} - /> - - ) - ) - ) : ( - - )} - - + @@ -562,7 +531,7 @@ export function ProtocolOverview(): JSX.Element { - + ) } From 79b6d665c754df5c8b20155417e2ab7efc0218f2 Mon Sep 17 00:00:00 2001 From: Ryan Howard Date: Tue, 1 Oct 2024 10:58:48 -0400 Subject: [PATCH 16/17] feat(hardware): add a new can message to batch read sensor data (#16370) # Overview In a first step to streamline tool_sensors.py we want to batch read sensor data, this will speed up the process and let us stream alot better ## Test Plan and Hands on Testing ## Changelog ## Review requests ## Risk assessment --- .../backends/flex_protocol.py | 4 ++ .../backends/ot3controller.py | 9 +++ api/src/opentrons/hardware_control/ot3api.py | 6 ++ .../firmware_bindings/constants.py | 1 + .../firmware_bindings/messages/fields.py | 8 +++ .../messages/message_definitions.py | 11 ++++ .../firmware_bindings/messages/payloads.py | 9 +++ .../hardware_control/tool_sensors.py | 55 ++++++++++++++++++- .../sensors/sensor_driver.py | 14 +++++ 9 files changed, 116 insertions(+), 1 deletion(-) diff --git a/api/src/opentrons/hardware_control/backends/flex_protocol.py b/api/src/opentrons/hardware_control/backends/flex_protocol.py index dfa6fd99ce4..6f3299cf92d 100644 --- a/api/src/opentrons/hardware_control/backends/flex_protocol.py +++ b/api/src/opentrons/hardware_control/backends/flex_protocol.py @@ -54,6 +54,10 @@ async def get_serial_number(self) -> Optional[str]: def restore_system_constraints(self) -> AsyncIterator[None]: ... + @asynccontextmanager + def grab_pressure(self, channels: int, mount: OT3Mount) -> AsyncIterator[None]: + ... + def update_constraints_for_gantry_load(self, gantry_load: GantryLoad) -> None: ... diff --git a/api/src/opentrons/hardware_control/backends/ot3controller.py b/api/src/opentrons/hardware_control/backends/ot3controller.py index ced7540d4df..84c95c8fbc4 100644 --- a/api/src/opentrons/hardware_control/backends/ot3controller.py +++ b/api/src/opentrons/hardware_control/backends/ot3controller.py @@ -161,6 +161,7 @@ capacitive_pass, liquid_probe, check_overpressure, + grab_pressure, ) from opentrons_hardware.hardware_control.rear_panel_settings import ( get_door_state, @@ -369,6 +370,14 @@ async def restore_system_constraints(self) -> AsyncIterator[None]: self._move_manager.update_constraints(old_system_constraints) log.debug(f"Restore previous system constraints: {old_system_constraints}") + @asynccontextmanager + async def grab_pressure( + self, channels: int, mount: OT3Mount + ) -> AsyncIterator[None]: + tool = axis_to_node(Axis.of_main_tool_actuator(mount)) + async with grab_pressure(channels, tool, self._messenger): + yield + def update_constraints_for_calibration_with_gantry_load( self, gantry_load: GantryLoad, diff --git a/api/src/opentrons/hardware_control/ot3api.py b/api/src/opentrons/hardware_control/ot3api.py index e43eb674e12..cdd69fc2f90 100644 --- a/api/src/opentrons/hardware_control/ot3api.py +++ b/api/src/opentrons/hardware_control/ot3api.py @@ -307,6 +307,12 @@ async def restore_system_constrants(self) -> AsyncIterator[None]: async with self._backend.restore_system_constraints(): yield + @contextlib.asynccontextmanager + async def grab_pressure(self, mount: OT3Mount) -> AsyncIterator[None]: + instrument = self._pipette_handler.get_pipette(mount) + async with self._backend.grab_pressure(instrument.channels, mount): + yield + def _update_door_state(self, door_state: DoorState) -> None: mod_log.info(f"Updating the window switch status: {door_state}") self.door_state = door_state diff --git a/hardware/opentrons_hardware/firmware_bindings/constants.py b/hardware/opentrons_hardware/firmware_bindings/constants.py index 960419d334c..923d8eb0725 100644 --- a/hardware/opentrons_hardware/firmware_bindings/constants.py +++ b/hardware/opentrons_hardware/firmware_bindings/constants.py @@ -244,6 +244,7 @@ class MessageId(int, Enum): max_sensor_value_request = 0x70 max_sensor_value_response = 0x71 + batch_read_sensor_response = 0x81 read_sensor_request = 0x82 write_sensor_request = 0x83 baseline_sensor_request = 0x84 diff --git a/hardware/opentrons_hardware/firmware_bindings/messages/fields.py b/hardware/opentrons_hardware/firmware_bindings/messages/fields.py index 6294f7bb541..d8838d52027 100644 --- a/hardware/opentrons_hardware/firmware_bindings/messages/fields.py +++ b/hardware/opentrons_hardware/firmware_bindings/messages/fields.py @@ -206,6 +206,14 @@ class FirmwareUpdateDataField(utils.BinaryFieldBase[bytes]): FORMAT = f"{NUM_BYTES}s" +class BatchSensorDataField(utils.BinaryFieldBase[bytes]): + """The data field for many sensor data results.""" + + # 14 4-byte data points + NUM_BYTES = 56 + FORMAT = f"{NUM_BYTES}s" + + class ErrorSeverityField(utils.UInt16Field): """A field for error severity.""" diff --git a/hardware/opentrons_hardware/firmware_bindings/messages/message_definitions.py b/hardware/opentrons_hardware/firmware_bindings/messages/message_definitions.py index 12f65f80f81..79eb8e279bf 100644 --- a/hardware/opentrons_hardware/firmware_bindings/messages/message_definitions.py +++ b/hardware/opentrons_hardware/firmware_bindings/messages/message_definitions.py @@ -593,6 +593,17 @@ class ReadFromSensorResponse(BaseMessage): # noqa: D101 message_id: Literal[MessageId.read_sensor_response] = MessageId.read_sensor_response +@dataclass +class BatchReadFromSensorResponse(BaseMessage): # noqa: D101 + payload: payloads.BatchReadFromSensorResponsePayload + payload_type: Type[ + payloads.BatchReadFromSensorResponsePayload + ] = payloads.BatchReadFromSensorResponsePayload + message_id: Literal[ + MessageId.batch_read_sensor_response + ] = MessageId.batch_read_sensor_response + + @dataclass class SetSensorThresholdRequest(BaseMessage): # noqa: D101 payload: payloads.SetSensorThresholdRequestPayload diff --git a/hardware/opentrons_hardware/firmware_bindings/messages/payloads.py b/hardware/opentrons_hardware/firmware_bindings/messages/payloads.py index 2e00aa2d9a7..5b493bf47a9 100644 --- a/hardware/opentrons_hardware/firmware_bindings/messages/payloads.py +++ b/hardware/opentrons_hardware/firmware_bindings/messages/payloads.py @@ -31,6 +31,7 @@ GearMotorIdField, OptionalRevisionField, MotorUsageTypeField, + BatchSensorDataField, ) from .. import utils @@ -444,6 +445,14 @@ class ReadFromSensorResponsePayload(SensorPayload): sensor_data: utils.Int32Field +@dataclass(eq=False) +class BatchReadFromSensorResponsePayload(SensorPayload): + """A response for a batch of sensor responses.""" + + data_length: utils.UInt8Field + sensor_data: BatchSensorDataField + + @dataclass(eq=False) class SetSensorThresholdRequestPayload(SensorPayload): """A request to set the threshold value of a sensor.""" diff --git a/hardware/opentrons_hardware/hardware_control/tool_sensors.py b/hardware/opentrons_hardware/hardware_control/tool_sensors.py index eb966ccebd4..173a8c2738b 100644 --- a/hardware/opentrons_hardware/hardware_control/tool_sensors.py +++ b/hardware/opentrons_hardware/hardware_control/tool_sensors.py @@ -10,12 +10,13 @@ Callable, AsyncContextManager, Optional, + AsyncIterator, ) from logging import getLogger from numpy import float64 from math import copysign from typing_extensions import Literal - +from contextlib import asynccontextmanager from opentrons_hardware.firmware_bindings.constants import ( NodeId, SensorId, @@ -663,3 +664,55 @@ def _drain() -> Iterator[float]: break return list(_drain()) + + +@asynccontextmanager +async def grab_pressure( + channels: int, tool: NodeId, messenger: CanMessenger +) -> AsyncIterator[None]: + """Run some task and log the pressure.""" + sensor_driver = SensorDriver() + sensor_id = SensorId.BOTH if channels > 1 else SensorId.S0 + sensors: List[SensorId] = [] + if sensor_id == SensorId.BOTH: + sensors.append(SensorId.S0) + sensors.append(SensorId.S1) + else: + sensors.append(sensor_id) + + for sensor in sensors: + pressure_sensor = PressureSensor.build( + sensor_id=sensor, + node_id=tool, + ) + num_baseline_reads = 10 + # TODO: RH log this baseline and remove noqa + pressure_baseline = await sensor_driver.get_baseline( # noqa: F841 + messenger, pressure_sensor, num_baseline_reads + ) + await messenger.ensure_send( + node_id=tool, + message=BindSensorOutputRequest( + payload=BindSensorOutputRequestPayload( + sensor=SensorTypeField(SensorType.pressure), + sensor_id=SensorIdField(sensor), + binding=SensorOutputBindingField(SensorOutputBinding.report), + ) + ), + expected_nodes=[tool], + ) + try: + yield + finally: + for sensor in sensors: + await messenger.ensure_send( + node_id=tool, + message=BindSensorOutputRequest( + payload=BindSensorOutputRequestPayload( + sensor=SensorTypeField(SensorType.pressure), + sensor_id=SensorIdField(sensor), + binding=SensorOutputBindingField(SensorOutputBinding.none), + ) + ), + expected_nodes=[tool], + ) diff --git a/hardware/opentrons_hardware/sensors/sensor_driver.py b/hardware/opentrons_hardware/sensors/sensor_driver.py index 84780695e66..611bc091970 100644 --- a/hardware/opentrons_hardware/sensors/sensor_driver.py +++ b/hardware/opentrons_hardware/sensors/sensor_driver.py @@ -278,6 +278,20 @@ def __call__( self.response_queue.put_nowait(data) current_time = round((time.time() - self.start_time), 3) self.csv_writer.writerow([current_time, data]) # type: ignore + if isinstance(message, message_definitions.BatchReadFromSensorResponse): + data_length = message.payload.data_length.value + data_bytes = message.payload.sensor_data.value + data_ints = [ + int.from_bytes(data_bytes[i * 4 : i * 4 + 4]) + for i in range(data_length) + ] + for d in data_ints: + data = sensor_types.SensorDataType.build( + d, message.payload.sensor + ).to_float() + self.response_queue.put_nowait(data) + current_time = round((time.time() - self.start_time), 3) + self.csv_writer.writerow([current_time, data]) if isinstance(message, message_definitions.Acknowledgement): if ( self.event is not None From e799e9b96266b0ec1c4234e9110ec3a750cbaf8d Mon Sep 17 00:00:00 2001 From: Ryan Howard Date: Tue, 1 Oct 2024 11:58:48 -0400 Subject: [PATCH 17/17] fix(api): don't lpd for each step in a mix (#16310) # Overview If an instrument is set to have Liquid Presence Detection on and their is a call to instrument.mix then the pipette will exit the liquid, do a LPD and then re-enter for each cycle. This change makes it so that it saves the state of the instrument, does LPD on the first aspirate and then turns LPD off. Once Mix is complete then LPD is toggled back to whatever state it was in before. ## Test Plan and Hands on Testing ## Changelog ## Review requests ## Risk assessment Low: If an error somehow occurs during the mix cycle and the command is bypassed then the instrument may be left with LPD off. --- .../protocol_api/instrument_context.py | 28 ++++- .../protocol_api/test_instrument_context.py | 115 ++++++++++++++++++ 2 files changed, 137 insertions(+), 6 deletions(-) diff --git a/api/src/opentrons/protocol_api/instrument_context.py b/api/src/opentrons/protocol_api/instrument_context.py index e11ffcc78c2..b158ff8c75f 100644 --- a/api/src/opentrons/protocol_api/instrument_context.py +++ b/api/src/opentrons/protocol_api/instrument_context.py @@ -540,12 +540,12 @@ def mix( ), ): self.aspirate(volume, location, rate) - while repetitions - 1 > 0: - self.dispense(volume, rate=rate, **dispense_kwargs) - self.aspirate(volume, rate=rate) - repetitions -= 1 - self.dispense(volume, rate=rate) - + with AutoProbeDisable(self): + while repetitions - 1 > 0: + self.dispense(volume, rate=rate, **dispense_kwargs) + self.aspirate(volume, rate=rate) + repetitions -= 1 + self.dispense(volume, rate=rate) return self @requires_version(2, 0) @@ -2192,6 +2192,22 @@ def _raise_if_configuration_not_supported_by_pipette( # SINGLE, QUADRANT and ALL are supported by all pipettes +class AutoProbeDisable: + """Use this class to temporarily disable automatic liquid presence detection.""" + + def __init__(self, instrument: InstrumentContext): + self.instrument = instrument + + def __enter__(self) -> None: + if self.instrument.api_version >= APIVersion(2, 21): + self.auto_presence = self.instrument.liquid_presence_detection + self.instrument.liquid_presence_detection = False + + def __exit__(self, *args: Any, **kwargs: Any) -> None: + if self.instrument.api_version >= APIVersion(2, 21): + self.instrument.liquid_presence_detection = self.auto_presence + + def _raise_if_has_end_or_front_right_or_back_left( style: NozzleLayout, end: Optional[str], diff --git a/api/tests/opentrons/protocol_api/test_instrument_context.py b/api/tests/opentrons/protocol_api/test_instrument_context.py index 3478ceb9a86..4478c250b8c 100644 --- a/api/tests/opentrons/protocol_api/test_instrument_context.py +++ b/api/tests/opentrons/protocol_api/test_instrument_context.py @@ -89,6 +89,20 @@ def mock_instrument_core(decoy: Decoy) -> InstrumentCore: """Get a mock instrument implementation core.""" instrument_core = decoy.mock(cls=InstrumentCore) decoy.when(instrument_core.get_mount()).then_return(Mount.LEFT) + + # we need to add this for the mock of liquid_presence detection to actually work + # this replaces the mock with a a property again + instrument_core._liquid_presence_detection = False # type: ignore[attr-defined] + + def _setter(enable: bool) -> None: + instrument_core._liquid_presence_detection = enable # type: ignore[attr-defined] + + def _getter() -> bool: + return instrument_core._liquid_presence_detection # type: ignore[attr-defined, no-any-return] + + instrument_core.get_liquid_presence_detection = _getter # type: ignore[method-assign] + instrument_core.set_liquid_presence_detection = _setter # type: ignore[method-assign] + return instrument_core @@ -1476,3 +1490,104 @@ def test_96_tip_config_invalid( decoy.when(mock_instrument_core.get_nozzle_map()).then_return(nozzle_map) decoy.when(mock_instrument_core.get_active_channels()).then_return(96) assert subject._96_tip_config_valid() is True + + +@pytest.mark.parametrize("api_version", [APIVersion(2, 21)]) +def test_mix_no_lpd( + decoy: Decoy, + mock_instrument_core: InstrumentCore, + subject: InstrumentContext, + mock_protocol_core: ProtocolCore, +) -> None: + """It should aspirate/dispense to a well several times.""" + mock_well = decoy.mock(cls=Well) + + bottom_location = Location(point=Point(1, 2, 3), labware=mock_well) + input_location = Location(point=Point(2, 2, 2), labware=None) + last_location = Location(point=Point(9, 9, 9), labware=None) + + decoy.when(mock_protocol_core.get_last_location(Mount.LEFT)).then_return( + last_location + ) + decoy.when( + mock_validation.validate_location( + location=input_location, last_location=last_location + ) + ).then_return(WellTarget(well=mock_well, location=None, in_place=False)) + decoy.when( + mock_validation.validate_location(location=None, last_location=last_location) + ).then_return(WellTarget(well=mock_well, location=None, in_place=False)) + decoy.when(mock_well.bottom(z=1.0)).then_return(bottom_location) + decoy.when(mock_instrument_core.get_aspirate_flow_rate(1.23)).then_return(5.67) + decoy.when(mock_instrument_core.get_dispense_flow_rate(1.23)).then_return(5.67) + decoy.when(mock_instrument_core.has_tip()).then_return(True) + decoy.when(mock_instrument_core.get_current_volume()).then_return(0.0) + + subject.mix(repetitions=10, volume=10.0, location=input_location, rate=1.23) + decoy.verify( + mock_instrument_core.aspirate(), # type: ignore[call-arg] + ignore_extra_args=True, + times=10, + ) + decoy.verify( + mock_instrument_core.dispense(), # type: ignore[call-arg] + ignore_extra_args=True, + times=10, + ) + + decoy.verify( + mock_instrument_core.liquid_probe_with_recovery(), # type: ignore[call-arg] + ignore_extra_args=True, + times=0, + ) + + +@pytest.mark.ot3_only +@pytest.mark.parametrize("api_version", [APIVersion(2, 21)]) +def test_mix_with_lpd( + decoy: Decoy, + mock_instrument_core: InstrumentCore, + subject: InstrumentContext, + mock_protocol_core: ProtocolCore, +) -> None: + """It should aspirate/dispense to a well several times and do 1 lpd.""" + mock_well = decoy.mock(cls=Well) + bottom_location = Location(point=Point(1, 2, 3), labware=mock_well) + input_location = Location(point=Point(2, 2, 2), labware=None) + last_location = Location(point=Point(9, 9, 9), labware=None) + + decoy.when(mock_protocol_core.get_last_location(Mount.LEFT)).then_return( + last_location + ) + decoy.when( + mock_validation.validate_location( + location=input_location, last_location=last_location + ) + ).then_return(WellTarget(well=mock_well, location=None, in_place=False)) + decoy.when( + mock_validation.validate_location(location=None, last_location=last_location) + ).then_return(WellTarget(well=mock_well, location=None, in_place=False)) + decoy.when(mock_well.bottom(z=1.0)).then_return(bottom_location) + decoy.when(mock_instrument_core.get_aspirate_flow_rate(1.23)).then_return(5.67) + decoy.when(mock_instrument_core.get_dispense_flow_rate(1.23)).then_return(5.67) + decoy.when(mock_instrument_core.has_tip()).then_return(True) + decoy.when(mock_instrument_core.get_current_volume()).then_return(0.0) + + subject.liquid_presence_detection = True + subject.mix(repetitions=10, volume=10.0, location=input_location, rate=1.23) + decoy.verify( + mock_instrument_core.aspirate(), # type: ignore[call-arg] + ignore_extra_args=True, + times=10, + ) + decoy.verify( + mock_instrument_core.dispense(), # type: ignore[call-arg] + ignore_extra_args=True, + times=10, + ) + + decoy.verify( + mock_instrument_core.liquid_probe_with_recovery(), # type: ignore[call-arg] + ignore_extra_args=True, + times=1, + )