Skip to content

Commit 7bf339f

Browse files
authored
fix(api, hardware-testing): Flex stacker PVT diagnostics test fixes + Check the X for labware when dispensing. (#19014)
1 parent 785c0ee commit 7bf339f

File tree

11 files changed

+359
-34
lines changed

11 files changed

+359
-34
lines changed

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

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,17 @@
22

33
import asyncio
44
import logging
5-
from typing import Any, Awaitable, Callable, Dict, Literal, Optional, Mapping, cast
5+
from typing import (
6+
Any,
7+
Awaitable,
8+
Callable,
9+
Dict,
10+
List,
11+
Literal,
12+
Optional,
13+
Mapping,
14+
cast,
15+
)
616

717
from opentrons.drivers.flex_stacker.types import (
818
AxisParams,
@@ -14,6 +24,7 @@
1424
StackerAxis,
1525
StallGuardParams,
1626
TOFDetection,
27+
TOFMeasurementResult,
1728
TOFSensor,
1829
HardwareRevision,
1930
TOFSensorMode,
@@ -520,11 +531,16 @@ async def dispense_labware(
520531
"""Dispenses the next labware in the stacker."""
521532
self.verify_labware_height(labware_height)
522533
await self._prepare_for_action()
534+
523535
if enforce_hopper_lw_sensing:
524536
await self.verify_hopper_labware_presence(Direction.EXTEND, True)
525537

526-
# Move platform along the X then Z axis
538+
# Move platform along the X and make sure we DONT detect labware
527539
await self._move_and_home_axis(StackerAxis.X, Direction.RETRACT, HOME_OFFSET_MD)
540+
if enforce_shuttle_lw_sensing:
541+
await self.verify_shuttle_labware_presence(Direction.RETRACT, False)
542+
543+
# Move platform along the Z axis
528544
await self._move_and_home_axis(StackerAxis.Z, Direction.EXTEND, HOME_OFFSET_SM)
529545

530546
# Transfer
@@ -556,7 +572,7 @@ async def store_labware(
556572
if enforce_shuttle_lw_sensing:
557573
await self.verify_shuttle_labware_presence(Direction.RETRACT, True)
558574

559-
# Move the Z so the labware sits right under any labware already stored
575+
# Move the Z so the labware sits right under the labware already stored
560576
latch_clear_distance = labware_height + PLATFORM_OFFSET - LATCH_CLEARANCE
561577
distance = MAX_TRAVEL[StackerAxis.Z] - latch_clear_distance
562578
await self.move_axis(StackerAxis.Z, Direction.EXTEND, distance)
@@ -638,7 +654,13 @@ async def home_all(self, ignore_latch: bool = False) -> None:
638654
await self.home_axis(StackerAxis.Z, Direction.RETRACT)
639655
await self.home_axis(StackerAxis.X, Direction.EXTEND)
640656

641-
async def labware_detected(self, axis: StackerAxis, direction: Direction) -> bool:
657+
async def labware_detected(
658+
self,
659+
axis: StackerAxis,
660+
direction: Direction,
661+
histogram: Optional[TOFMeasurementResult] = None,
662+
baseline: Optional[Dict[int, List[float]]] = None,
663+
) -> bool:
642664
"""Detect labware on the TOF sensor using the `baseline` method
643665
644666
NOTE: This method is still under development and is inconsistent when detecting
@@ -648,11 +670,13 @@ async def labware_detected(self, axis: StackerAxis, direction: Direction) -> boo
648670
"""
649671
dir_str = cast(Literal["extend", "retract"], str(direction))
650672
sensor = TOFSensor.X if axis == StackerAxis.X else TOFSensor.Z
651-
baseline = load_tof_baseline_data(self.model())[sensor.value][dir_str]
673+
baseline = (
674+
baseline or load_tof_baseline_data(self.model())[sensor.value][dir_str]
675+
)
652676
config = TOF_DETECTION_CONFIG[sensor][direction]
653677

654678
# Take a histogram reading and determine if labware was detected
655-
histogram = await self._driver.get_tof_histogram(sensor)
679+
histogram = histogram or await self._driver.get_tof_histogram(sensor)
656680
for zone in config.zones:
657681
raw_data = histogram.bins[zone]
658682
baseline_data = baseline[zone]

api/tests/opentrons/hardware_control/modules/test_hc_flexstacker.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -419,9 +419,13 @@ async def test_dispense_labware_motion_sequence(
419419
# We need to verify the move sequence
420420
verify_hopper_labware_presence.assert_called_once_with(Direction.EXTEND, True)
421421
_prepare_for_action.assert_called()
422+
422423
_move_and_home_axis.assert_any_call(
423424
StackerAxis.X, Direction.RETRACT, HOME_OFFSET_MD
424425
)
426+
# Verify labware presence
427+
verify_shuttle_labware_presence.assert_any_call(Direction.RETRACT, False)
428+
425429
_move_and_home_axis.assert_any_call(
426430
StackerAxis.Z, Direction.EXTEND, HOME_OFFSET_SM
427431
)
@@ -440,13 +444,16 @@ async def test_dispense_labware_motion_sequence(
440444
home_axis.assert_any_call(StackerAxis.Z, Direction.RETRACT)
441445

442446
# Verify labware presence
443-
verify_shuttle_labware_presence.assert_called_once_with(Direction.RETRACT, True)
447+
verify_shuttle_labware_presence.assert_any_call(Direction.RETRACT, True)
444448

445449
# Then finally the x is moved to the gripper position
446450
_move_and_home_axis.assert_any_call(
447451
StackerAxis.X, Direction.EXTEND, HOME_OFFSET_MD
448452
)
449453

454+
# Make sure labware presense check on the X retract was called twice
455+
assert verify_shuttle_labware_presence.call_count == 2
456+
450457

451458
@pytest.mark.parametrize(
452459
("labware_height"),

hardware-testing/hardware_testing/modules/flex_stacker/flex_stacker_qc/config.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
test_z_axis_basic,
1111
test_x_axis_basic,
1212
test_l_axis_basic,
13+
test_l_axis_current_speed,
1314
test_z_axis_current_speed,
1415
test_x_axis_current_speed,
1516
test_stallguard,
@@ -38,6 +39,7 @@ class TestSection(enum.Enum):
3839
INSTALL_DETECTION = "INSTALL_DETECTION"
3940
TOF_BASIC = "TOF_BASIC"
4041
TOF_FUNCTIONAL = "TOF_FUNCTIONAL"
42+
L_AXIS_CURRENT_SPEED = "L_AXIS_CURRENT_SPEED"
4143
Z_AXIS_CURRENT_SPEED = "Z_AXIS_CURRENT_SPEED"
4244
X_AXIS_CURRENT_SPEED = "X_AXIS_CURRENT_SPEED"
4345

@@ -99,6 +101,10 @@ class TestConfig:
99101
TestSection.TOF_FUNCTIONAL,
100102
test_tof_functional.run,
101103
),
104+
(
105+
TestSection.L_AXIS_CURRENT_SPEED,
106+
test_l_axis_current_speed.run,
107+
),
102108
(
103109
TestSection.Z_AXIS_CURRENT_SPEED,
104110
test_z_axis_current_speed.run,
@@ -163,6 +169,10 @@ def build_report(test_name: str) -> CSVReport:
163169
title=TestSection.TOF_FUNCTIONAL.value,
164170
lines=test_tof_functional.build_csv_lines(),
165171
),
172+
CSVSection(
173+
title=TestSection.L_AXIS_CURRENT_SPEED.value,
174+
lines=test_l_axis_current_speed.build_csv_lines(),
175+
),
166176
CSVSection(
167177
title=TestSection.Z_AXIS_CURRENT_SPEED.value,
168178
lines=test_z_axis_current_speed.build_csv_lines(),
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
"""Test L Axis."""
2+
from typing import List, Union, Tuple, Optional
3+
from hardware_testing.data import ui
4+
from hardware_testing.data.csv_report import (
5+
CSVReport,
6+
CSVLine,
7+
CSVLineRepeating,
8+
CSVResult,
9+
)
10+
11+
from opentrons.hardware_control.modules.flex_stacker import (
12+
MAX_TRAVEL,
13+
FlexStacker,
14+
STACKER_MOTION_CONFIG,
15+
)
16+
from opentrons.drivers.flex_stacker.types import StackerAxis, Direction
17+
from opentrons_shared_data.errors.exceptions import FlexStackerStallError
18+
19+
TEST_AXIS = StackerAxis.L
20+
HOME_SPEED = STACKER_MOTION_CONFIG[TEST_AXIS]["home"].move_params.max_speed
21+
HOME_CURRENT = STACKER_MOTION_CONFIG[TEST_AXIS]["home"].run_current
22+
23+
TEST_SPEEDS = [50, 100, 150] # mm/s
24+
TEST_CURRENTS = [1.5, 1.2, 0.8, 0.3] # A rms
25+
TEST_ACCELERATION = STACKER_MOTION_CONFIG[TEST_AXIS]["move"].move_params.acceleration
26+
CURRENT_THRESHOD = 0.8 # A rms
27+
TEST_TRIALS = 5
28+
29+
# All units in mm
30+
31+
# The distance from retracted to extended limit switch
32+
AXIS_TRAVEL = MAX_TRAVEL[TEST_AXIS]
33+
OFFSET = 2 # The distance to be off the springs from the limit switch
34+
AXIS_TOLERANCE = 0.5 # Distance tolerance of AXIS_TRAVEL in ONE direction
35+
LIMIT_SWICH_CHECK = 0.1
36+
MOVEMENT_TOLERANCE = 0.5 # Maximum allowed movement error in ONE direction
37+
38+
39+
def build_csv_lines() -> List[Union[CSVLine, CSVLineRepeating]]:
40+
"""Build CSV Lines."""
41+
lines: List[Union[CSVLine, CSVLineRepeating]] = []
42+
for speed in TEST_SPEEDS:
43+
for current in TEST_CURRENTS:
44+
tag = f"speed-{speed}-current-{current}"
45+
lines.append(
46+
CSVLine(f"{tag}-success-failed-pass%", [int, int, float, CSVResult])
47+
)
48+
lines.append(CSVLine(f"{tag}-extend-distance", [float] * TEST_TRIALS))
49+
lines.append(CSVLine(f"{tag}-retract-distance", [float] * TEST_TRIALS))
50+
return lines
51+
52+
53+
async def test_cycle_per_direction(
54+
stacker: FlexStacker,
55+
direction: Direction,
56+
speed: int,
57+
current: float,
58+
) -> Tuple[bool, float]:
59+
"""Test one cycle."""
60+
# latch does not have extend limit switch, so we have to cycle the test
61+
await stacker.open_latch()
62+
63+
# Move at homing speed off the springs
64+
await stacker.move_axis(
65+
TEST_AXIS, direction, OFFSET, HOME_SPEED, None, HOME_CURRENT
66+
)
67+
68+
try:
69+
# moving at the testing speed and current to just before the springs
70+
# minus the tolerances
71+
test_distance = AXIS_TRAVEL - (2 * OFFSET) - AXIS_TOLERANCE - MOVEMENT_TOLERANCE
72+
await stacker.move_axis(
73+
TEST_AXIS, direction, test_distance, speed, TEST_ACCELERATION, current
74+
)
75+
76+
# Move to the farthest position the limit switch could be
77+
check_distance = OFFSET + 2 * AXIS_TOLERANCE + 2 * MOVEMENT_TOLERANCE
78+
try:
79+
await stacker.move_axis(
80+
TEST_AXIS,
81+
direction,
82+
check_distance,
83+
HOME_SPEED,
84+
0,
85+
HOME_CURRENT,
86+
)
87+
except Exception:
88+
pass
89+
90+
# Limit switch only has a retract limit switch
91+
if await stacker._driver.get_limit_switch(TEST_AXIS, Direction.RETRACT):
92+
# The limit switch was triggered within this amount of distance
93+
movement_distance = round(
94+
(AXIS_TRAVEL + OFFSET + AXIS_TOLERANCE + MOVEMENT_TOLERANCE), 1
95+
)
96+
ui.print_info(
97+
f"{TEST_AXIS.name} Axis, {direction}, PASS, {speed}mm/s, {current}A, {movement_distance}mm"
98+
)
99+
return True, movement_distance
100+
except FlexStackerStallError:
101+
ui.print_error("unexpected axis stall!")
102+
# If we reach this point, limit switch did not trigger in expected distance
103+
# Probable stall, Movement distance is unknown, return 0
104+
return False, 0
105+
106+
107+
async def run(stacker: FlexStacker, report: CSVReport, section: str) -> None:
108+
"""Run."""
109+
# Home to closed position
110+
await stacker.home_axis(TEST_AXIS, Direction.RETRACT)
111+
112+
for speed in TEST_SPEEDS:
113+
for current in TEST_CURRENTS:
114+
tag = f"speed-{speed}-current-{current}"
115+
ui.print_header(
116+
f"{TEST_AXIS.name} Speed: {speed} mm/s, Current: {current} A"
117+
)
118+
trial = 0
119+
failures = 0
120+
extend_data: List[Optional[float]] = [None] * TEST_TRIALS
121+
retract_data: List[Optional[float]] = [None] * TEST_TRIALS
122+
# Home to closed position
123+
await stacker.home_axis(TEST_AXIS, Direction.RETRACT)
124+
while trial < TEST_TRIALS:
125+
# Can only test retract direction
126+
retract, dist = await test_cycle_per_direction(
127+
stacker, Direction.RETRACT, speed, current
128+
)
129+
extend_data[trial] = dist
130+
if not retract:
131+
ui.print_error(
132+
f"{TEST_AXIS.name} Axis retract failed at speed {speed} mm/s, "
133+
f"current {current} A, Distance {dist} mm"
134+
)
135+
failures += 1
136+
trial += 1
137+
continue
138+
trial += 1
139+
140+
success_trials = trial - failures
141+
success_rate = (1 - failures / trial) * 100
142+
if current >= CURRENT_THRESHOD:
143+
# If current is above threshold, all trials must pass
144+
result = CSVResult.from_bool(success_rate == 100.0)
145+
else:
146+
result = CSVResult.PASS
147+
report(
148+
section,
149+
f"{tag}-success-failed-pass%",
150+
[success_trials, failures, success_rate, result],
151+
)
152+
report(section, f"{tag}-extend-distance", extend_data)
153+
report(section, f"{tag}-retract-distance", retract_data)
154+
155+
# Stop the test if any trial fails
156+
if result == CSVResult.FAIL:
157+
ui.print_error(
158+
f"{TEST_AXIS.name} Axis failed at speed {speed} mm/s, current {current} A"
159+
)
160+
return
161+
162+
# End test with the latch closed
163+
await stacker.home_axis(TEST_AXIS, Direction.RETRACT)

hardware-testing/hardware_testing/modules/flex_stacker/flex_stacker_qc/test_tof_basic.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@
1616
TOFSensor,
1717
)
1818

19+
from hardware_testing.modules.flex_stacker.flex_stacker_qc.utils import (
20+
convert_histogram_bins_to_csv_string,
21+
)
22+
1923

2024
def build_csv_lines() -> List[Union[CSVLine, CSVLineRepeating]]:
2125
"""Build CSV Lines."""
@@ -71,12 +75,13 @@ async def test_get_tof_sensor_histogram(
7175
"""Test that we can request and store histogram measurements from this TOF sensor."""
7276
print(f"Getting histogram for {sensor}.")
7377
histogram = await stacker._driver.get_tof_histogram(sensor)
78+
histogram_string = convert_histogram_bins_to_csv_string(histogram.bins)
7479
report(
7580
section,
7681
f"tof-{sensor.name}-histogram",
7782
[
7883
CSVResult.PASS,
79-
histogram.bins,
84+
histogram_string,
8085
],
8186
)
8287

0 commit comments

Comments
 (0)