Skip to content

Commit dcc8129

Browse files
authored
feat(api, app): add disable Flex Stacker labware detection as a feature flag (#19116)
# Overview This PR adds the ability to disable labware detection on the stackers during a protocol run as a robot settings feature flag. This can be useful when working with labware whose reflectivity falls outside our validated range, or in cases where the sensors are malfunctioning.
1 parent bbfccf2 commit dcc8129

File tree

16 files changed

+303
-14
lines changed

16 files changed

+303
-14
lines changed

api/src/opentrons/config/advanced_settings.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,14 @@ class Setting(NamedTuple):
222222
robot_type=[RobotTypeEnum.OT2, RobotTypeEnum.FLEX],
223223
internal_only=True,
224224
),
225+
SettingDefinition(
226+
_id="disableFlexStackerLabwareDetection",
227+
title="Disable Flex Stacker's labware detection features",
228+
description=(
229+
"Flex Stackers will ignore labware's presence in the hopper and on the shuttle. Protocol runs will no longer raise the following recoverable errors: Hopper Empty, Shuttle Empty and Shuttle Occupied."
230+
),
231+
robot_type=[RobotTypeEnum.FLEX],
232+
),
225233
]
226234

227235

@@ -733,6 +741,16 @@ def _migrate36to37(previous: SettingsMap) -> SettingsMap:
733741
return {k: v for k, v in previous.items() if "allowLiquidClasses" != k}
734742

735743

744+
def _migrate37to38(previous: SettingsMap) -> SettingsMap:
745+
"""Migrate to version 36 of the feature flags file.
746+
747+
- Adds the disableFlexStackerLabwareDetection config element.
748+
"""
749+
newmap = {k: v for k, v in previous.items()}
750+
newmap["disableFlexStackerLabwareDetection"] = None
751+
return newmap
752+
753+
736754
_MIGRATIONS = [
737755
_migrate0to1,
738756
_migrate1to2,
@@ -771,6 +789,7 @@ def _migrate36to37(previous: SettingsMap) -> SettingsMap:
771789
_migrate34to35,
772790
_migrate35to36,
773791
_migrate36to37,
792+
_migrate37to38,
774793
]
775794
"""
776795
List of all migrations to apply, indexed by (version - 1). See _migrate below

api/src/opentrons/config/feature_flags.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,3 +78,9 @@ def enable_performance_metrics(robot_type: RobotTypeEnum) -> bool:
7878

7979
def oem_mode_enabled() -> bool:
8080
return advs.get_setting_with_env_overload("enableOEMMode", RobotTypeEnum.FLEX)
81+
82+
83+
def flex_stacker_tof_sensors_disabled() -> bool:
84+
return advs.get_setting_with_env_overload(
85+
"disableFlexStackerLabwareDetection", RobotTypeEnum.FLEX
86+
)

api/src/opentrons/hardware_control/modules/flex_stacker.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
FlexStackerData,
5454
)
5555
from opentrons.hardware_control.types import StatusBarState, StatusBarUpdateEvent
56+
from opentrons.config import feature_flags as ff
5657

5758
from opentrons_shared_data.errors.exceptions import (
5859
FlexStackerStallError,
@@ -730,6 +731,8 @@ async def verify_shuttle_labware_presence(
730731
self, direction: Direction, labware_expected: bool
731732
) -> None:
732733
"""Check whether or not a labware is detected on the shuttle."""
734+
if ff.flex_stacker_tof_sensors_disabled():
735+
return
733736
result = await self.labware_detected(StackerAxis.X, direction)
734737
if labware_expected != result:
735738
if labware_expected:
@@ -748,6 +751,8 @@ async def verify_hopper_labware_presence(
748751
self, direction: Direction, labware_expected: bool
749752
) -> None:
750753
"""Check whether or not a labware is detected inside the hopper."""
754+
if ff.flex_stacker_tof_sensors_disabled():
755+
return
751756
result = await self.labware_detected(StackerAxis.Z, direction)
752757
if labware_expected != result:
753758
raise FlexStackerHopperLabwareError(

api/tests/opentrons/config/test_advanced_settings_migration.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
@pytest.fixture
1010
def migrated_file_version() -> int:
11-
return 37
11+
return 38
1212

1313

1414
# make sure to set a boolean value in default_file_settings only if
@@ -30,6 +30,7 @@ def default_file_settings() -> Dict[str, Any]:
3030
"enableErrorRecoveryExperiments": None,
3131
"enableOEMMode": None,
3232
"enablePerformanceMetrics": None,
33+
"disableFlexStackerLabwareDetection": None,
3334
}
3435

3536

@@ -438,6 +439,18 @@ def v37_config(v36_config: Dict[str, Any]) -> Dict[str, Any]:
438439
return r
439440

440441

442+
@pytest.fixture
443+
def v38_config(v37_config: Dict[str, Any]) -> Dict[str, Any]:
444+
r = v37_config.copy()
445+
r.update(
446+
{
447+
"_version": 38,
448+
"disableFlexStackerLabwareDetection": None,
449+
}
450+
)
451+
return r
452+
453+
441454
@pytest.fixture(
442455
params=[
443456
lazy_fixture("empty_settings"),
@@ -479,6 +492,7 @@ def v37_config(v36_config: Dict[str, Any]) -> Dict[str, Any]:
479492
lazy_fixture("v35_config"),
480493
lazy_fixture("v36_config"),
481494
lazy_fixture("v37_config"),
495+
lazy_fixture("v38_config"),
482496
],
483497
)
484498
def old_settings(request: SubRequest) -> Dict[str, Any]:
@@ -569,4 +583,5 @@ def test_ensures_config() -> None:
569583
"enableErrorRecoveryExperiments": None,
570584
"enableOEMMode": None,
571585
"enablePerformanceMetrics": None,
586+
"disableFlexStackerLabwareDetection": None,
572587
}

app/index.html

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1-
<!doctype html>
1+
<!DOCTYPE html>
22
<html lang="en">
33
<head>
44
<meta charset="UTF-8" />
5-
<meta name='viewport' content='width=device-width,initial-scale=1'>
5+
<meta name="viewport" content="width=device-width,initial-scale=1" />
66

7-
<link href="https://fonts.googleapis.com/css?family=Open+Sans:400,400i,600,600i,700,700i" rel="stylesheet">
7+
<link
8+
href="https://fonts.googleapis.com/css?family=Open+Sans:400,400i,600,600i,700,700i"
9+
rel="stylesheet"
10+
/>
811

912
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
1013
<title>Opentrons</title>

app/src/assets/localization/en/device_settings.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@
9191
"device_reset_slideout_description": "Select individual settings to only clear specific data types.",
9292
"device_resets_cannot_be_undone": "Resets cannot be undone",
9393
"directly_connected_to_this_computer": "Directly connected to this computer.",
94+
"disable_stacker_sensors": "Disable Stacker sensors for labware detection in z-axis and x-axis",
95+
"disable_stacker_sensors_description": "Disable sensors for all stackers that are connected to the robot.",
9496
"disconnect": "Disconnect",
9597
"disconnect_from_ssid": "Disconnect from {{ssid}}",
9698
"disconnect_from_wifi": "Disconnect from Wi-Fi",

app/src/atoms/SoftwareKeyboard/index.css

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,8 @@
77
padding: 5px;
88
border-radius: 5px;
99
background-color: #ececec;
10-
font-family:
11-
HelveticaNeue-Light,
12-
Helvetica Neue Light,
13-
Helvetica Neue,
14-
Helvetica,
15-
Arial,
16-
Lucida Grande,
17-
sans-serif;
10+
font-family: HelveticaNeue-Light, Helvetica Neue Light, Helvetica Neue,
11+
Helvetica, Arial, Lucida Grande, sans-serif;
1812
touch-action: manipulation;
1913
-webkit-user-select: none;
2014
-moz-user-select: none;
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { useTranslation } from 'react-i18next'
2+
3+
import {
4+
ALIGN_CENTER,
5+
Box,
6+
Flex,
7+
JUSTIFY_SPACE_BETWEEN,
8+
LegacyStyledText,
9+
SPACING,
10+
TYPOGRAPHY,
11+
} from '@opentrons/components'
12+
13+
import { ToggleButton } from '/app/atoms/buttons'
14+
import { useDisableStackerSensors } from '/app/resources/robot-settings'
15+
16+
interface DisableStackerSensorsProps {
17+
robotName: string
18+
isRobotBusy: boolean
19+
}
20+
21+
export function DisableStackerSensors({
22+
robotName,
23+
isRobotBusy,
24+
}: DisableStackerSensorsProps): JSX.Element {
25+
const { t } = useTranslation('device_settings')
26+
const { sensorsDisabled, toggleSensors } = useDisableStackerSensors(robotName)
27+
28+
return (
29+
<Flex alignItems={ALIGN_CENTER} justifyContent={JUSTIFY_SPACE_BETWEEN}>
30+
<Box width="70%">
31+
<LegacyStyledText
32+
css={TYPOGRAPHY.pSemiBold}
33+
paddingBottom={SPACING.spacing4}
34+
id="AdvancedSettings_disableStackerSensors"
35+
>
36+
{t('disable_stacker_sensors')}
37+
</LegacyStyledText>
38+
<LegacyStyledText as="p">
39+
{t('disable_stacker_sensors_description')}
40+
</LegacyStyledText>
41+
</Box>
42+
<ToggleButton
43+
label="disable_stacker_sensors"
44+
toggledOn={sensorsDisabled}
45+
onClick={toggleSensors}
46+
id="RobotSettings_DisableStackerSensorsToggleButton"
47+
disabled={isRobotBusy}
48+
/>
49+
</Flex>
50+
)
51+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { fireEvent, screen } from '@testing-library/react'
2+
import { beforeEach, describe, expect, it, vi } from 'vitest'
3+
4+
import '@testing-library/jest-dom/vitest'
5+
6+
import { renderWithProviders } from '/app/__testing-utils__'
7+
import { i18n } from '/app/i18n'
8+
import { useDisableStackerSensors } from '/app/resources/robot-settings'
9+
10+
import { DisableStackerSensors } from '../DisableStackerSensors'
11+
12+
import type { ComponentProps } from 'react'
13+
14+
vi.mock('/app/resources/robot-settings')
15+
16+
const ROBOT_NAME = 'otie'
17+
const mockToggleSensors = vi.fn()
18+
const render = (props: ComponentProps<typeof DisableStackerSensors>) => {
19+
return renderWithProviders(<DisableStackerSensors {...props} />, {
20+
i18nInstance: i18n,
21+
})
22+
}
23+
24+
describe('DisableStackerSensors', () => {
25+
let props: ComponentProps<typeof DisableStackerSensors>
26+
27+
beforeEach(() => {
28+
props = {
29+
robotName: ROBOT_NAME,
30+
isRobotBusy: false,
31+
}
32+
vi.mocked(useDisableStackerSensors).mockReturnValue({
33+
sensorsDisabled: false,
34+
toggleSensors: mockToggleSensors,
35+
})
36+
})
37+
38+
it('should render text and toggle button', () => {
39+
render(props)
40+
screen.getByText(
41+
'Disable Stacker sensors for labware detection in z-axis and x-axis'
42+
)
43+
screen.getByText(
44+
'Disable sensors for all stackers that are connected to the robot.'
45+
)
46+
expect(screen.getByLabelText('disable_stacker_sensors')).toBeInTheDocument()
47+
})
48+
49+
it('should call a mock function when clicking toggle button', () => {
50+
render(props)
51+
fireEvent.click(screen.getByLabelText('disable_stacker_sensors'))
52+
expect(mockToggleSensors).toHaveBeenCalled()
53+
})
54+
55+
it('shoud make toggle button disabled when robot is busy', () => {
56+
props = {
57+
...props,
58+
isRobotBusy: true,
59+
}
60+
render(props)
61+
expect(screen.getByLabelText('disable_stacker_sensors')).toBeDisabled()
62+
})
63+
})

app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export * from './DeviceReset'
22
export * from './DisplayRobotName'
3+
export * from './DisableStackerSensors'
34
export * from './EnableStatusLight'
45
export * from './FactoryMode'
56
export * from './GantryHoming'

0 commit comments

Comments
 (0)