-
Notifications
You must be signed in to change notification settings - Fork 179
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(api): create instr ctx methods for checking liquid presence #15555
Changes from 14 commits
b2566f1
67e9465
8f57203
5b59440
48d6d10
311b42c
fd51c9a
f0ac17a
3902a9c
16722ed
8ea76ee
6ccfc78
f890a42
213bc82
520058f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,13 +1,14 @@ | ||
from __future__ import annotations | ||
|
||
import logging | ||
from contextlib import ExitStack | ||
from typing import Any, List, Optional, Sequence, Union, cast, Dict | ||
from opentrons_shared_data.errors.exceptions import ( | ||
CommandPreconditionViolated, | ||
CommandParameterLimitViolated, | ||
PipetteLiquidNotFoundError, | ||
UnexpectedTipRemovalError, | ||
) | ||
from opentrons.protocol_engine.errors.exceptions import WellDoesNotExistError | ||
from opentrons.legacy_broker import LegacyBroker | ||
from opentrons.hardware_control.dev_types import PipetteDict | ||
from opentrons import types | ||
|
@@ -2046,3 +2047,44 @@ def configure_nozzle_layout( | |
) | ||
# TODO (spp, 2023-12-05): verify that tipracks are on adapters for only full 96 channel config | ||
self._tip_racks = tip_racks or [] | ||
|
||
@requires_version(2, 20) | ||
def detect_liquid_presence(self, well: labware.Well) -> bool: | ||
"""Check if there is liquid in a well. | ||
|
||
:returns: A boolean. | ||
""" | ||
if not isinstance(well, labware.Well): | ||
raise WellDoesNotExistError("You must provide a valid well to check.") | ||
try: | ||
height = self._core.find_liquid_level(well._core) | ||
if height > 0: | ||
return True | ||
return False # it should never get here | ||
except PipetteLiquidNotFoundError: | ||
return False | ||
except Exception as e: | ||
raise e | ||
|
||
@requires_version(2, 20) | ||
def require_liquid_presence(self, well: labware.Well) -> None: | ||
"""If there is no liquid in a well, raise an error. | ||
|
||
:returns: None. | ||
""" | ||
if not isinstance(well, labware.Well): | ||
raise WellDoesNotExistError("You must provide a valid well to check.") | ||
|
||
self._core.find_liquid_level(well._core) | ||
|
||
@requires_version(2, 20) | ||
def measure_liquid_height(self, well: labware.Well) -> float: | ||
"""Check the height of the liquid within a well. | ||
|
||
:returns: The height, in mm, of the liquid from the deck. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For API usability, I think we would ideally have this return the height from the bottom of the well, instead of from the deck. That seems more relevant because it wouldn't be coupled to the labware offsets that the operator happened to choose at the beginning of the run. This doesn't need to happen right now as long as we're marking this method internal and experimental. |
||
""" | ||
if not isinstance(well, labware.Well): | ||
raise WellDoesNotExistError("You must provide a valid well to check.") | ||
|
||
height = self._core.find_liquid_level(well._core) | ||
return float(height) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,6 +2,7 @@ | |
|
||
from typing import cast, Any, Optional, overload | ||
|
||
from opentrons.protocol_engine.errors.error_occurrence import ProtocolCommandFailedError | ||
from opentrons_shared_data.labware.dev_types import LabwareUri | ||
from opentrons_shared_data.labware.labware_definition import LabwareDefinition | ||
|
||
|
@@ -89,6 +90,26 @@ def execute_command_without_recovery( | |
create_request = CreateType(params=cast(Any, params)) | ||
return self._transport.execute_command(create_request) | ||
|
||
def execute_command_with_result( | ||
self, params: commands.CommandParams | ||
) -> Optional[commands.CommandResult]: | ||
"""Execute a ProtocolEngine command, including error recovery, and return a result. | ||
|
||
See `ChildThreadTransport.execute_command_wait_for_recovery()` for exact | ||
behavior. | ||
""" | ||
CreateType = CREATE_TYPES_BY_PARAMS_TYPE[type(params)] | ||
create_request = CreateType(params=cast(Any, params)) | ||
result = self._transport.execute_command_wait_for_recovery(create_request) | ||
if result.error is None: | ||
return result.result | ||
if isinstance(result.error, BaseException): # necessary to pass lint | ||
raise result.error | ||
raise ProtocolCommandFailedError( | ||
original_error=result.error, | ||
message=f"{result.error.errorType}: {result.error.detail}", | ||
) | ||
|
||
Comment on lines
+99
to
+118
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we should remove this new Thankfully, we shouldn't need it for the behavior that we want to implement right now. See my other comment. |
||
@property | ||
def state(self) -> StateView: | ||
"""Get a view of the engine's state.""" | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this gets a lot cleaner if you split this into two methods:
SyncClient.execute_command()
.SyncClient.execute_command_without_recovery()
.SyncClient
is drawing a distinction those two fundamentally modes of operation, representing a tradeoff between having a command result and allowing for error recovery, and representing that tradeoff with two different method signatures. Those map cleanly to the publicInstrumentContext
behaviors that we want to implement, if we maintain the two-method distinction throughInstrumentCore
.