Skip to content

Commit

Permalink
feat(app-shell-odd,robot-server): Allow resetting on-device display c…
Browse files Browse the repository at this point in the history
…onfiguration (#13227)

Co-authored-by: Shlok Amin <[email protected]>
  • Loading branch information
SyntaxColoring and shlokamin authored Aug 7, 2023
1 parent 3c5753c commit 3fd73a2
Show file tree
Hide file tree
Showing 17 changed files with 211 additions and 40 deletions.
16 changes: 11 additions & 5 deletions api/src/opentrons/config/reset.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ class ResetOptionId(str, Enum):
gripper_offset = "gripperOffsetCalibrations"
tip_length_calibrations = "tipLengthCalibrations"
runs_history = "runsHistory"
on_device_display = "onDeviceDisplay"


_OT_2_RESET_OPTIONS = [
Expand All @@ -51,6 +52,7 @@ class ResetOptionId(str, Enum):
ResetOptionId.pipette_offset,
ResetOptionId.gripper_offset,
ResetOptionId.runs_history,
ResetOptionId.on_device_display,
]

_settings_reset_options = {
Expand All @@ -59,7 +61,7 @@ class ResetOptionId(str, Enum):
),
ResetOptionId.deck_calibration: CommonResetOption(
name="Deck Calibration",
description="Clear deck calibration (will also clear pipette " "offset)",
description="Clear deck calibration (will also clear pipette offset)",
),
ResetOptionId.pipette_offset: CommonResetOption(
name="Pipette Offset Calibrations",
Expand All @@ -71,16 +73,20 @@ class ResetOptionId(str, Enum):
),
ResetOptionId.tip_length_calibrations: CommonResetOption(
name="Tip Length Calibrations",
description="Clear tip length calibrations (will also clear " "pipette offset)",
description="Clear tip length calibrations (will also clear pipette offset)",
),
# TODO(mm, 2022-05-23): Run and protocol history is a robot-server thing,
# and is not a concept known to this package (the `opentrons` library).
# TODO(mm, 2022-05-23): runs_history and on_device_display are robot-server things,
# and are not concepts known to this package (the `opentrons` library).
# This option is defined here only as a convenience for robot-server.
# Find a way to split thing up and define this in robot-server instead.
# Find a way to split things up and define this in robot-server instead.
ResetOptionId.runs_history: CommonResetOption(
name="Clear Runs History",
description="Erase this device's stored history of protocols and runs.",
),
ResetOptionId.on_device_display: CommonResetOption(
name="On-Device Display Configuration",
description="Clear the configuration of the on-device display (touchscreen)",
),
}


Expand Down
1 change: 1 addition & 0 deletions app-shell-odd/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
"form-data": "2.5.0",
"fs-extra": "10.0.0",
"get-stream": "5.1.0",
"lodash": "4.17.21",
"merge-options": "1.0.1",
"node-fetch": "2.6.7",
"node-stream-zip": "1.8.2",
Expand Down
11 changes: 10 additions & 1 deletion app-shell-odd/src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// TODO(mc, 2020-01-31): this module is high-importance and needs unit tests
import Store from 'electron-store'
import get from 'lodash/get'
import forEach from 'lodash/forEach'
import mergeOptions from 'merge-options'
import yargsParser from 'yargs-parser'
import fs from 'fs-extra'
Expand All @@ -23,7 +24,7 @@ import type { Config, Overrides } from './types'

export * from './types'

const ODD_DIR = '/data/ODD'
export const ODD_DIR = '/data/ODD'

// Note (kj:03/02/2023) this file path will be updated when the embed team cleans up
const BRIGHTNESS_FILE =
Expand Down Expand Up @@ -150,3 +151,11 @@ export function handleConfigChange(
): void {
store().onDidChange(path, changeHandler)
}
export function resetStore(): void {
log().debug('Resetting the store')
store().clear()
const migratedConfig = migrate(DEFAULTS_V12)
forEach(migratedConfig, (configVal, configKey) => {
store().set(configKey, configVal)

Check warning on line 159 in app-shell-odd/src/config/index.ts

View check run for this annotation

Codecov / codecov/patch

app-shell-odd/src/config/index.ts#L155-L159

Added lines #L155 - L159 were not covered by tests
})
}
20 changes: 19 additions & 1 deletion app-shell-odd/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
// electron main entry point
import { app, ipcMain } from 'electron'
import fse from 'fs-extra'
import path from 'path'
import { createUi } from './ui'
import { createLogger } from './log'
import { registerDiscovery } from './discovery'
import { registerRobotLogs } from './robot-logs'
import { registerUpdate, updateLatestVersion } from './update'
import { registerRobotSystemUpdate } from './system-update'
import { registerAppRestart } from './restart'
import { getConfig, getStore, getOverrides, registerConfig } from './config'
import {
getConfig,
getStore,
getOverrides,
registerConfig,
resetStore,
ODD_DIR,
} from './config'
import systemd from './systemd'

import type { BrowserWindow } from 'electron'
Expand Down Expand Up @@ -41,6 +50,15 @@ app.once('window-all-closed', () => {

function startUp(): void {
log.info('Starting App')
console.log('Starting App')
const storeNeedsReset = fse.existsSync(

Check warning on line 54 in app-shell-odd/src/main.ts

View check run for this annotation

Codecov / codecov/patch

app-shell-odd/src/main.ts#L53-L54

Added lines #L53 - L54 were not covered by tests
path.join(ODD_DIR, `_CONFIG_TO_BE_DELETED_ON_REBOOT`)
)
if (storeNeedsReset) {
log.debug('store marked to be reset, resetting store')
resetStore()
fse.removeSync(path.join(ODD_DIR, `_CONFIG_TO_BE_DELETED_ON_REBOOT`))

Check warning on line 60 in app-shell-odd/src/main.ts

View check run for this annotation

Codecov / codecov/patch

app-shell-odd/src/main.ts#L58-L60

Added lines #L58 - L60 were not covered by tests
}
systemd.sendStatus('loading app')
process.on('uncaughtException', error => log.error('Uncaught: ', { error }))
process.on('unhandledRejection', reason =>
Expand Down
1 change: 1 addition & 0 deletions app-shell/build/release-notes-internal.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ This is still pretty early in the process, so some things are known not to work,
- All instrument flows should display errors properly now
- Update robot flows don't say OT-2s anymore
- There should be fewer surprise scroll bars on Windows
- The configuration of the on-device display can be factory-reset, which lets you go back to the first-time setup flow



Expand Down
4 changes: 3 additions & 1 deletion app/src/assets/localization/en/device_settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@
"checking_for_updates": "Checking for updates",
"choose_reset_settings": "Choose reset settings",
"choose": "Choose...",
"clear_all_stored_data_description": "Clears instrument calibrations and protocols. Keeps robot name and network settings.",
"clear_all_data": "Clear all data",
"clear_all_stored_data_description": "Resets all settings. You’ll have to redo initial setup before using the robot again.",
"clear_all_stored_data": "Clear all stored data",
"clear_data_and_restart_robot": "Clear data and restart robot",
"clear_individual_data": "Clear individual data",
Expand Down Expand Up @@ -227,6 +228,7 @@
"security_type": "Security Type",
"select_a_network": "Select a network",
"select_a_security_type": "Select a security type",
"select_all_settings": "Select all settings",
"select_authentication_method": "Select authentication method for your selected network.",
"sending_software": "Sending software...",
"share_logs_with_opentrons_description": "Help Opentrons improve its products and services by automatically sending anonymous robot logs. Opentrons uses these logs to troubleshoot robot issues and spot error trends.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@ import {
SUCCESS,
PENDING,
} from '../../../../../redux/robot-api'
import { resetConfig } from '../../../../../redux/robot-admin'
import {
getResetConfigOptions,
resetConfig,
} from '../../../../../redux/robot-admin'
import { useIsOT3 } from '../../../hooks'

import type { State } from '../../../../../redux/types'
import type { ResetConfigRequest } from '../../../../../redux/robot-admin/types'
Expand All @@ -44,13 +48,36 @@ export function DeviceResetModal({
const { t } = useTranslation(['device_settings', 'shared'])
const history = useHistory()
const [dispatchRequest, requestIds] = useDispatchApiRequest()
const isOT3 = useIsOT3(robotName)
const resetRequestStatus = useSelector((state: State) => {
const lastId = last(requestIds)
return lastId != null ? getRequestById(state, lastId) : null
})?.status

const serverResetOptions = useSelector((state: State) =>
getResetConfigOptions(state, robotName)
)

const triggerReset = (): void => {
if (resetOptions != null) {
if (isOT3) {
const totalOptionsSelected = Object.values(resetOptions).filter(
selected => selected === true
).length

const isEveryOptionSelected =
totalOptionsSelected > 0 &&
totalOptionsSelected ===
// filtering out ODD setting because this gets implicitly cleared if all settings are selected
serverResetOptions.filter(o => o.id !== 'onDeviceDisplay').length

if (isEveryOptionSelected) {
resetOptions = {
...resetOptions,
onDeviceDisplay: true,
}
}
}
dispatchRequest(resetConfig(robotName, resetOptions))
history.push(`/devices/`)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,17 @@ export function DeviceResetSlideout({
onCloseClick()
}

const totalOptionsSelected = Object.values(resetOptions).filter(
selected => selected === true
).length

// filtering out ODD setting because this gets implicitly cleared if all settings are selected
const allOptionsWithoutODD = options.filter(o => o.id !== 'onDeviceDisplay')

const isEveryOptionSelected =
totalOptionsSelected > 0 &&
totalOptionsSelected === allOptionsWithoutODD.length

return (
<Slideout
title={t('device_reset')}
Expand Down Expand Up @@ -164,25 +175,35 @@ export function DeviceResetSlideout({
/>
<StyledText as="p">{t('resets_cannot_be_undone')}</StyledText>
</Flex>
{/* Note: (kj:06/07/2023) this part will be updated when be is ready */}
{isOT3 ? (
<>
<Flex flexDirection={DIRECTION_COLUMN} gridGap={SPACING.spacing20}>
<Flex flexDirection={DIRECTION_COLUMN}>
<StyledText as="p" fontWeight={TYPOGRAPHY.fontWeightSemiBold}>
{t('factory_reset')}
</StyledText>
<StyledText as="p" marginTop={SPACING.spacing8}>
{t('factory_reset_description')}
</StyledText>
</Flex>
<Flex flexDirection={DIRECTION_COLUMN}>
<StyledText as="p" fontWeight={TYPOGRAPHY.fontWeightSemiBold}>
{t('clear_all_stored_data')}
{t('clear_all_data')}
</StyledText>
<StyledText as="p" marginTop={SPACING.spacing8}>
<StyledText as="p" marginY={SPACING.spacing8}>
{t('clear_all_stored_data_description')}
</StyledText>
<CheckboxField
onChange={() => {
setResetOptions(
isEveryOptionSelected
? {}
: allOptionsWithoutODD.reduce((acc, val) => {
return {
...acc,
[val.id]: true,
}
}, {})
)
}}
value={isEveryOptionSelected}
label={t(`select_all_settings`)}
isIndeterminate={
!isEveryOptionSelected && totalOptionsSelected > 0
}
/>
</Flex>
</Flex>
<Divider marginY={SPACING.spacing16} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ import { renderWithProviders } from '@opentrons/components'
import { i18n } from '../../../../../../i18n'
import { resetConfig } from '../../../../../../redux/robot-admin'
import { useDispatchApiRequest } from '../../../../../../redux/robot-api'

import { DeviceResetModal } from '../DeviceResetModal'

import type { DispatchApiRequestType } from '../../../../../../redux/robot-api'

jest.mock('../../../../hooks')
jest.mock('../../../../../../redux/robot-admin')
jest.mock('../../../../../../redux/robot-api')

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,20 +106,10 @@ describe('RobotSettings DeviceResetSlideout', () => {
it('should change some options and text for Flex', () => {
mockUseIsOT3.mockReturnValue(true)
const [{ getByText, getByRole, queryByRole, queryByText }] = render()
getByText('Factory Reset')
getByText('Clear all data')
getByText(
'Resets all settings. You’ll have to redo initial setup before using the robot again.'
)
getByText('Clear all stored data')
getByText(
'Clears instrument calibrations and protocols. Keeps robot name and network settings.'
)

expect(
queryByText(
'Resetting Deck and/or Tip Length Calibration data will also clear Pipette Offset Calibration data.'
)
).toBeNull()
expect(queryByText('Clear deck calibration')).toBeNull()
getByText('Clear pipette calibration(s)')
expect(queryByText('Clear tip length calibrations')).toBeNull()
Expand Down
30 changes: 25 additions & 5 deletions app/src/organisms/RobotSettingsDashboard/DeviceReset.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,15 +65,35 @@ export function DeviceReset({
'runsHistory',
'bootScripts',
]
const availableOptions = options.sort(
(a, b) =>
targetOptionsOrder.indexOf(a.id) - targetOptionsOrder.indexOf(b.id)
)
const availableOptions = options
// filtering out ODD setting because this gets implicitly cleared if all settings are selected
.filter(o => o.id !== 'onDeviceDisplay')
.sort(
(a, b) =>
targetOptionsOrder.indexOf(a.id) - targetOptionsOrder.indexOf(b.id)
)
const dispatch = useDispatch<Dispatch>()

const handleClick = (): void => {
if (resetOptions != null) {
dispatchRequest(resetConfig(robotName, resetOptions))
const totalOptionsSelected = Object.values(resetOptions).filter(
selected => selected === true
).length

const isEveryOptionSelected =
totalOptionsSelected > 0 &&
totalOptionsSelected === availableOptions.length

if (isEveryOptionSelected) {
dispatchRequest(
resetConfig(robotName, {
...resetOptions,
onDeviceDisplay: true,
})
)
} else {
dispatchRequest(resetConfig(robotName, resetOptions))
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
_RESET_MARKER_FILE_NAME: Final = "_TO_BE_DELETED_ON_REBOOT"
_RESET_MARKER_FILE_CONTENTS: Final = """\
This file was placed here by robot-server.
It tells robot-server to clear this directory on the next boot.
It tells robot-server to clear this directory on the next boot,
after which it will delete this file.
"""


Expand Down
42 changes: 42 additions & 0 deletions robot-server/robot_server/service/legacy/reset_odd.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
from typing_extensions import Final

from anyio import Path as AsyncPath


# The on-device display process knows to look for this file here.
_RESET_MARKER_PATH: Final = AsyncPath("/data/ODD/_CONFIG_TO_BE_DELETED_ON_REBOOT")

_RESET_MARKER_FILE_CONTENTS: Final = """\
This file was placed here by robot-server.
It tells the on-device display process to clear its configuration on the next boot,
after which it will delete this file.
"""


async def mark_odd_for_reset_next_boot(
reset_marker_path: AsyncPath = _RESET_MARKER_PATH,
) -> None:
"""Mark the configuration of the Flex's on-device display so it gets reset on the next boot.
We can't just delete the config files from this Python process, because the on-device display
process actively reads and writes to them, so it would introduce race conditions.
We could `systemctl stop` the other process to fix that, but that would introduce other
problems: the factory-reset flow involves a separate HTTP request to reboot the machine,
and if we stopped the other process, it wouldn't be around to send it.
This implementation assumes you're running on a real Flex, as opposed to an OT-2 or a
local dev machine.
Params:
reset_marker_path: The path of the marker file to create. This should only be overridden
for unit-testing this function.
"""
try:
await reset_marker_path.write_text(
encoding="utf-8", data=_RESET_MARKER_FILE_CONTENTS
)
except FileNotFoundError:
# No-op if the parent directory doesn't exist.
# This probably means the on-device display hasn't started up for the
# first time, or it's already been reset.
pass
Loading

0 comments on commit 3fd73a2

Please sign in to comment.