diff --git a/api/src/opentrons/config/reset.py b/api/src/opentrons/config/reset.py
index eb1171bd684..8cc4486aa57 100644
--- a/api/src/opentrons/config/reset.py
+++ b/api/src/opentrons/config/reset.py
@@ -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 = [
@@ -51,6 +52,7 @@ class ResetOptionId(str, Enum):
ResetOptionId.pipette_offset,
ResetOptionId.gripper_offset,
ResetOptionId.runs_history,
+ ResetOptionId.on_device_display,
]
_settings_reset_options = {
@@ -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",
@@ -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)",
+ ),
}
diff --git a/app-shell-odd/package.json b/app-shell-odd/package.json
index 2eedb9a8fc9..2647af12ce8 100644
--- a/app-shell-odd/package.json
+++ b/app-shell-odd/package.json
@@ -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",
diff --git a/app-shell-odd/src/config/index.ts b/app-shell-odd/src/config/index.ts
index 6f821409cb1..73041e50e64 100644
--- a/app-shell-odd/src/config/index.ts
+++ b/app-shell-odd/src/config/index.ts
@@ -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'
@@ -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 =
@@ -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)
+ })
+}
diff --git a/app-shell-odd/src/main.ts b/app-shell-odd/src/main.ts
index 51b0fa7225a..6184722fcd2 100644
--- a/app-shell-odd/src/main.ts
+++ b/app-shell-odd/src/main.ts
@@ -1,5 +1,7 @@
// 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'
@@ -7,7 +9,14 @@ 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'
@@ -41,6 +50,15 @@ app.once('window-all-closed', () => {
function startUp(): void {
log.info('Starting App')
+ console.log('Starting App')
+ const storeNeedsReset = fse.existsSync(
+ 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`))
+ }
systemd.sendStatus('loading app')
process.on('uncaughtException', error => log.error('Uncaught: ', { error }))
process.on('unhandledRejection', reason =>
diff --git a/app-shell/build/release-notes-internal.md b/app-shell/build/release-notes-internal.md
index 486dea2b286..08663572808 100644
--- a/app-shell/build/release-notes-internal.md
+++ b/app-shell/build/release-notes-internal.md
@@ -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
diff --git a/app/src/assets/localization/en/device_settings.json b/app/src/assets/localization/en/device_settings.json
index 9066277f49f..30e7a3a0883 100644
--- a/app/src/assets/localization/en/device_settings.json
+++ b/app/src/assets/localization/en/device_settings.json
@@ -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",
@@ -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.",
diff --git a/app/src/organisms/Devices/RobotSettings/AdvancedTab/AdvancedTabSlideouts/DeviceResetModal.tsx b/app/src/organisms/Devices/RobotSettings/AdvancedTab/AdvancedTabSlideouts/DeviceResetModal.tsx
index 9a60caad1fb..45e3d96abbf 100644
--- a/app/src/organisms/Devices/RobotSettings/AdvancedTab/AdvancedTabSlideouts/DeviceResetModal.tsx
+++ b/app/src/organisms/Devices/RobotSettings/AdvancedTab/AdvancedTabSlideouts/DeviceResetModal.tsx
@@ -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'
@@ -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/`)
}
diff --git a/app/src/organisms/Devices/RobotSettings/AdvancedTab/AdvancedTabSlideouts/DeviceResetSlideout.tsx b/app/src/organisms/Devices/RobotSettings/AdvancedTab/AdvancedTabSlideouts/DeviceResetSlideout.tsx
index c0605a5df01..163e0446475 100644
--- a/app/src/organisms/Devices/RobotSettings/AdvancedTab/AdvancedTabSlideouts/DeviceResetSlideout.tsx
+++ b/app/src/organisms/Devices/RobotSettings/AdvancedTab/AdvancedTabSlideouts/DeviceResetSlideout.tsx
@@ -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 (
{t('resets_cannot_be_undone')}
- {/* Note: (kj:06/07/2023) this part will be updated when be is ready */}
{isOT3 ? (
<>
- {t('factory_reset')}
-
-
- {t('factory_reset_description')}
-
-
-
-
- {t('clear_all_stored_data')}
+ {t('clear_all_data')}
-
+
{t('clear_all_stored_data_description')}
+ {
+ setResetOptions(
+ isEveryOptionSelected
+ ? {}
+ : allOptionsWithoutODD.reduce((acc, val) => {
+ return {
+ ...acc,
+ [val.id]: true,
+ }
+ }, {})
+ )
+ }}
+ value={isEveryOptionSelected}
+ label={t(`select_all_settings`)}
+ isIndeterminate={
+ !isEveryOptionSelected && totalOptionsSelected > 0
+ }
+ />
diff --git a/app/src/organisms/Devices/RobotSettings/AdvancedTab/AdvancedTabSlideouts/__tests__/DeviceResetModal.test.tsx b/app/src/organisms/Devices/RobotSettings/AdvancedTab/AdvancedTabSlideouts/__tests__/DeviceResetModal.test.tsx
index c5ba03f44b3..d40cb759706 100644
--- a/app/src/organisms/Devices/RobotSettings/AdvancedTab/AdvancedTabSlideouts/__tests__/DeviceResetModal.test.tsx
+++ b/app/src/organisms/Devices/RobotSettings/AdvancedTab/AdvancedTabSlideouts/__tests__/DeviceResetModal.test.tsx
@@ -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')
diff --git a/app/src/organisms/Devices/RobotSettings/AdvancedTab/AdvancedTabSlideouts/__tests__/DeviceResetSlideout.test.tsx b/app/src/organisms/Devices/RobotSettings/AdvancedTab/AdvancedTabSlideouts/__tests__/DeviceResetSlideout.test.tsx
index 036cadae2f2..bb4891a0733 100644
--- a/app/src/organisms/Devices/RobotSettings/AdvancedTab/AdvancedTabSlideouts/__tests__/DeviceResetSlideout.test.tsx
+++ b/app/src/organisms/Devices/RobotSettings/AdvancedTab/AdvancedTabSlideouts/__tests__/DeviceResetSlideout.test.tsx
@@ -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()
diff --git a/app/src/organisms/RobotSettingsDashboard/DeviceReset.tsx b/app/src/organisms/RobotSettingsDashboard/DeviceReset.tsx
index 78cfe9dc1c9..7c9f7b194f5 100644
--- a/app/src/organisms/RobotSettingsDashboard/DeviceReset.tsx
+++ b/app/src/organisms/RobotSettingsDashboard/DeviceReset.tsx
@@ -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()
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))
+ }
}
}
diff --git a/robot-server/robot_server/persistence/_persistence_directory.py b/robot-server/robot_server/persistence/_persistence_directory.py
index dd31d5191a8..111e428b92d 100644
--- a/robot-server/robot_server/persistence/_persistence_directory.py
+++ b/robot-server/robot_server/persistence/_persistence_directory.py
@@ -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.
"""
diff --git a/robot-server/robot_server/service/legacy/reset_odd.py b/robot-server/robot_server/service/legacy/reset_odd.py
new file mode 100644
index 00000000000..8176662dcb9
--- /dev/null
+++ b/robot-server/robot_server/service/legacy/reset_odd.py
@@ -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
diff --git a/robot-server/robot_server/service/legacy/routers/settings.py b/robot-server/robot_server/service/legacy/routers/settings.py
index 994992b9c5e..26ad778e81e 100644
--- a/robot-server/robot_server/service/legacy/routers/settings.py
+++ b/robot-server/robot_server/service/legacy/routers/settings.py
@@ -19,6 +19,7 @@
from robot_server.errors import LegacyErrorResponse
from robot_server.hardware import get_hardware, get_robot_type
+from robot_server.service.legacy import reset_odd
from robot_server.service.legacy.models import V1BasicResponse
from robot_server.service.legacy.models.settings import (
AdvancedSettingsResponse,
@@ -232,6 +233,9 @@ async def post_settings_reset_options(
if factory_reset_commands.get(reset_util.ResetOptionId.runs_history, False):
await persistence_resetter.mark_directory_reset()
+ if factory_reset_commands.get(reset_util.ResetOptionId.on_device_display, False):
+ await reset_odd.mark_odd_for_reset_next_boot()
+
# TODO (tz, 5-24-22): The order of a set is undefined because set's aren't ordered.
# The message returned to the client will be printed in the wrong order.
message = (
@@ -307,7 +311,6 @@ async def get_pipette_setting(pipette_id: str) -> PipetteSettings:
async def patch_pipette_setting(
pipette_id: str, settings_update: PipetteSettingsUpdate
) -> PipetteSettings:
-
# Convert fields to dict of field name to value
fields = settings_update.setting_fields or {}
field_values = {k: None if v is None else v.value for k, v in fields.items()}
diff --git a/robot-server/tests/integration/test_settings_reset_options_flex.tavern.yaml b/robot-server/tests/integration/test_settings_reset_options_flex.tavern.yaml
index de24485d339..f39310d6b93 100644
--- a/robot-server/tests/integration/test_settings_reset_options_flex.tavern.yaml
+++ b/robot-server/tests/integration/test_settings_reset_options_flex.tavern.yaml
@@ -23,7 +23,11 @@ stages:
description: !re_search 'Clear gripper offset calibrations'
- id: runsHistory
name: Clear Runs History
- description: !re_search 'Erase this device''s stored history of protocols and runs.'
+ description: !re_search 'Erase this device''s stored history of protocols and runs'
+ - id: onDeviceDisplay
+ name: On-Device Display Configuration
+ description: !re_search 'on-device display'
+
- name: POST Reset gripperOffsetCalibrations true on OT-3
request:
url: '{ot3_server_base_url}/settings/reset'
@@ -34,3 +38,10 @@ stages:
status_code: 200
json:
message: "Options 'gripper_offset' were reset"
+
+ # POSTing bootScripts, pipetteOffsetCalibrations, and runsHistory are untested here because they
+ # should already be covered by the OT-2 test.
+
+ # POSTing onDeviceDisplay is untested here because it writes to a part of the filesystem outside
+ # of robot-server's control, which is not a good thing to do on a dev machine.
+ # We rely on manual end-to-end tests (with the Opentrons App) to cover it.
diff --git a/robot-server/tests/service/legacy/routers/test_settings.py b/robot-server/tests/service/legacy/routers/test_settings.py
index 1d6f15b80cb..65acd3978aa 100644
--- a/robot-server/tests/service/legacy/routers/test_settings.py
+++ b/robot-server/tests/service/legacy/routers/test_settings.py
@@ -485,6 +485,8 @@ async def mock_get_persistence_resetter() -> PersistenceResetter:
"tipLengthCalibrations": True,
"deckCalibration": True,
"runsHistory": True,
+ # TODO(mm, 2023-08-04): Figure out how to test Flex-only options,
+ # then add gripperOffsetCalibrations and onDeviceDisplay.
},
{
ResetOptionId.boot_scripts,
diff --git a/robot-server/tests/service/legacy/test_reset_odd.py b/robot-server/tests/service/legacy/test_reset_odd.py
new file mode 100644
index 00000000000..d6441015ef0
--- /dev/null
+++ b/robot-server/tests/service/legacy/test_reset_odd.py
@@ -0,0 +1,17 @@
+from pathlib import Path
+
+from anyio import Path as AsyncPath
+
+from robot_server.service.legacy import reset_odd as subject
+
+
+async def test_places_marker_file(tmp_path: Path) -> None:
+ reset_marker_path = AsyncPath(tmp_path / "foo")
+ await subject.mark_odd_for_reset_next_boot(reset_marker_path=reset_marker_path)
+ assert await reset_marker_path.exists()
+
+
+async def test_noops_if_parent_dir_does_not_exist(tmp_path: Path) -> None:
+ await subject.mark_odd_for_reset_next_boot(
+ reset_marker_path=AsyncPath(tmp_path / "does_not_exist" / "foo")
+ )