Skip to content

Commit cfefcbc

Browse files
feat(api): add option to ignore different tip presence states (#14980)
## Overview This code adds an argument called `ht_operational_sensor` to `get_tip_presence_status`, that when used tells the api to only return the tip presence state of the instrument probe type specified. This allows calibration and partial tip flows to execute and check against their expected tip status without failing. ## TODO A follow-up pr will go up using this parameter for the `get_tip_presence` call in the calibration flow. ## Review Requests I'll most likely address any non-blocking change requests in a follow-up pr so we can cut the internal release as fast as possible, but let me know if: - `ht_operational_sensor` makes sense or if we can think of a better name - we should otherwise go about anything differently here.
1 parent 4794f55 commit cfefcbc

File tree

6 files changed

+101
-12
lines changed

6 files changed

+101
-12
lines changed

api/src/opentrons/hardware_control/backends/flex_protocol.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -383,7 +383,9 @@ async def capacitive_pass(
383383
def subsystems(self) -> Dict[SubSystem, SubSystemState]:
384384
...
385385

386-
async def get_tip_status(self, mount: OT3Mount) -> TipStateType:
386+
async def get_tip_status(
387+
self, mount: OT3Mount, ht_operation_sensor: Optional[InstrumentProbeType] = None
388+
) -> TipStateType:
387389
...
388390

389391
def current_tip_state(self, mount: OT3Mount) -> Optional[bool]:

api/src/opentrons/hardware_control/backends/ot3controller.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1521,8 +1521,14 @@ async def update_tip_detector(self, mount: OT3Mount, sensor_count: int) -> None:
15211521
async def teardown_tip_detector(self, mount: OT3Mount) -> None:
15221522
await self._tip_presence_manager.clear_detector(mount)
15231523

1524-
async def get_tip_status(self, mount: OT3Mount) -> TipStateType:
1525-
return await self.tip_presence_manager.get_tip_status(mount)
1524+
async def get_tip_status(
1525+
self,
1526+
mount: OT3Mount,
1527+
ht_operational_sensor: Optional[InstrumentProbeType] = None,
1528+
) -> TipStateType:
1529+
return await self.tip_presence_manager.get_tip_status(
1530+
mount, ht_operational_sensor
1531+
)
15261532

15271533
def current_tip_state(self, mount: OT3Mount) -> Optional[bool]:
15281534
return self.tip_presence_manager.current_tip_state(mount)

api/src/opentrons/hardware_control/backends/ot3simulator.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -780,7 +780,11 @@ def subsystems(self) -> Dict[SubSystem, SubSystemState]:
780780
for axis in self._present_axes
781781
}
782782

783-
async def get_tip_status(self, mount: OT3Mount) -> TipStateType:
783+
async def get_tip_status(
784+
self,
785+
mount: OT3Mount,
786+
ht_operational_sensor: Optional[InstrumentProbeType] = None,
787+
) -> TipStateType:
784788
return TipStateType(self._sim_tip_state[mount])
785789

786790
def current_tip_state(self, mount: OT3Mount) -> Optional[bool]:

api/src/opentrons/hardware_control/backends/tip_presence_manager.py

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from typing import cast, Callable, Optional, List, Set
44
from typing_extensions import TypedDict, Literal
55

6-
from opentrons.hardware_control.types import TipStateType, OT3Mount
6+
from opentrons.hardware_control.types import TipStateType, OT3Mount, InstrumentProbeType
77

88
from opentrons_hardware.drivers.can_bus import CanMessenger
99
from opentrons_hardware.firmware_bindings.constants import NodeId
@@ -14,8 +14,11 @@
1414
from opentrons_shared_data.errors.exceptions import (
1515
TipDetectorNotFound,
1616
UnmatchedTipPresenceStates,
17+
GeneralError,
1718
)
1819

20+
from .ot3utils import sensor_id_for_instrument
21+
1922
log = logging.getLogger(__name__)
2023

2124
TipListener = Callable[[OT3Mount, bool], None]
@@ -111,17 +114,40 @@ def current_tip_state(self, mount: OT3Mount) -> Optional[bool]:
111114
return state
112115

113116
@staticmethod
114-
def _get_tip_presence(results: List[tip_types.TipNotification]) -> TipStateType:
117+
def _get_tip_presence(
118+
results: List[tip_types.TipNotification],
119+
ht_operational_sensor: Optional[InstrumentProbeType] = None,
120+
) -> TipStateType:
121+
"""
122+
We can use ht_operational_sensor used to specify that we only care
123+
about the status of one tip presence sensor on a high throughput
124+
pipette, and the other is allowed to be different.
125+
"""
126+
if ht_operational_sensor:
127+
target_sensor_id = sensor_id_for_instrument(ht_operational_sensor)
128+
for r in results:
129+
if r.sensor == target_sensor_id:
130+
return TipStateType(r.presence)
131+
# raise an error if requested sensor response isn't found
132+
raise GeneralError(
133+
message=f"Requested status for sensor {ht_operational_sensor} not found."
134+
)
115135
# more than one sensor reported, we have to check if their states match
116136
if len(set(r.presence for r in results)) > 1:
117137
raise UnmatchedTipPresenceStates(
118138
{int(r.sensor): int(r.presence) for r in results}
119139
)
120140
return TipStateType(results[0].presence)
121141

122-
async def get_tip_status(self, mount: OT3Mount) -> TipStateType:
142+
async def get_tip_status(
143+
self,
144+
mount: OT3Mount,
145+
ht_operational_sensor: Optional[InstrumentProbeType] = None,
146+
) -> TipStateType:
123147
detector = self.get_detector(mount)
124-
return self._get_tip_presence(await detector.request_tip_status())
148+
return self._get_tip_presence(
149+
await detector.request_tip_status(), ht_operational_sensor
150+
)
125151

126152
def get_detector(self, mount: OT3Mount) -> TipDetector:
127153
detector = self._detectors[self._get_key(mount)]

api/src/opentrons/hardware_control/ot3api.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2072,6 +2072,7 @@ async def _high_throughput_check_tip(self) -> AsyncIterator[None]:
20722072
async def get_tip_presence_status(
20732073
self,
20742074
mount: Union[top_types.Mount, OT3Mount],
2075+
ht_operational_sensor: Optional[InstrumentProbeType] = None,
20752076
) -> TipStateType:
20762077
"""
20772078
Check tip presence status. If a high throughput pipette is present,
@@ -2085,14 +2086,19 @@ async def get_tip_presence_status(
20852086
and self._gantry_load == GantryLoad.HIGH_THROUGHPUT
20862087
):
20872088
await stack.enter_async_context(self._high_throughput_check_tip())
2088-
result = await self._backend.get_tip_status(real_mount)
2089+
result = await self._backend.get_tip_status(
2090+
real_mount, ht_operational_sensor
2091+
)
20892092
return result
20902093

20912094
async def verify_tip_presence(
2092-
self, mount: Union[top_types.Mount, OT3Mount], expected: TipStateType
2095+
self,
2096+
mount: Union[top_types.Mount, OT3Mount],
2097+
expected: TipStateType,
2098+
ht_operational_sensor: Optional[InstrumentProbeType] = None,
20932099
) -> None:
20942100
real_mount = OT3Mount.from_mount(mount)
2095-
status = await self.get_tip_presence_status(real_mount)
2101+
status = await self.get_tip_presence_status(real_mount, ht_operational_sensor)
20962102
if status != expected:
20972103
raise FailedTipStateCheck(expected, status.value)
20982104

api/tests/opentrons/hardware_control/backends/test_ot3_tip_presence_manager.py

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from typing import AsyncIterator, Dict
33
from decoy import Decoy
44

5-
from opentrons.hardware_control.types import OT3Mount, TipStateType
5+
from opentrons.hardware_control.types import OT3Mount, TipStateType, InstrumentProbeType
66
from opentrons.hardware_control.backends.tip_presence_manager import TipPresenceManager
77
from opentrons_hardware.hardware_control.tip_presence import (
88
TipDetector,
@@ -110,6 +110,51 @@ async def test_get_tip_status_for_high_throughput(
110110
result == expected_type
111111

112112

113+
@pytest.mark.parametrize(
114+
"tip_presence,expected_type,sensor_to_look_at",
115+
[
116+
(
117+
{SensorId.S0: False, SensorId.S1: False},
118+
TipStateType.ABSENT,
119+
InstrumentProbeType.PRIMARY,
120+
),
121+
(
122+
{SensorId.S0: True, SensorId.S1: True},
123+
TipStateType.PRESENT,
124+
InstrumentProbeType.SECONDARY,
125+
),
126+
(
127+
{SensorId.S0: False, SensorId.S1: True},
128+
TipStateType.ABSENT,
129+
InstrumentProbeType.PRIMARY,
130+
),
131+
(
132+
{SensorId.S0: False, SensorId.S1: True},
133+
TipStateType.PRESENT,
134+
InstrumentProbeType.SECONDARY,
135+
),
136+
],
137+
)
138+
async def test_allow_different_tip_states_ht(
139+
subject: TipPresenceManager,
140+
tip_detector_controller: TipDetectorController,
141+
tip_presence: Dict[SensorId, bool],
142+
expected_type: TipStateType,
143+
sensor_to_look_at: InstrumentProbeType,
144+
) -> None:
145+
mount = OT3Mount.LEFT
146+
await tip_detector_controller.retrieve_tip_status_highthroughput(tip_presence)
147+
148+
result = await subject.get_tip_status(mount, sensor_to_look_at)
149+
result == expected_type
150+
151+
# if sensor_to_look_at is not used, different tip states
152+
# should result in an UnmatchedTipStates error
153+
if len(set(tip_presence[t] for t in tip_presence)) > 1:
154+
with pytest.raises(UnmatchedTipPresenceStates):
155+
result = await subject.get_tip_status(mount)
156+
157+
113158
@pytest.mark.parametrize(
114159
"tip_presence",
115160
[

0 commit comments

Comments
 (0)