Skip to content
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): Validate plate reader status using live data hookups for engine and introduce lid status to the PAPI #15872

Merged
merged 31 commits into from
Aug 14, 2024
Merged
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
f694335
PLAT-397 add module lid definition
ahiuchingau Jul 10, 2024
ed13f7c
PLAT-398 add module lid dock addressable area
ahiuchingau Jul 10, 2024
957f246
squash! PLAT-397 add module lid definition
ahiuchingau Jul 10, 2024
6051392
fixup async byonoy library get_plate_presence()
ahiuchingau Jul 10, 2024
29d0418
PLAT-215 load lid in lid dock when module is loaded
ahiuchingau Jul 10, 2024
7935c4f
PLAT-209 & PLAT-210 add open_lid and close_lid protocol engine comman…
ahiuchingau Jul 10, 2024
d6200b6
some changes required to get the app working
ahiuchingau Jul 10, 2024
0e2b885
squash! PLAT-209 & PLAT-210 add open_lid and close_lid protocol engin…
ahiuchingau Jul 10, 2024
febc1d2
update absorbance module offset in definition
ahiuchingau Jul 10, 2024
d656112
add a protocol engine error
ahiuchingau Jul 10, 2024
262cdf2
error handling
ahiuchingau Jul 10, 2024
eb053b1
update setup-py
ahiuchingau Jul 10, 2024
fece1e7
format
ahiuchingau Jul 10, 2024
58ace2d
AddAbsorbanceReaderLidAction to add lid id as module substate
ahiuchingau Jul 10, 2024
cda987a
fix linter errors
ahiuchingau Jul 11, 2024
772d913
simplified protocol commmands
ahiuchingau Jul 12, 2024
6371d4d
streamline engine logic include plate reader lid as fixed labware and…
CaseyBatten Jul 24, 2024
f1ff265
labware fixture correction for app tests
CaseyBatten Jul 25, 2024
dbb5792
remove lid from app view
CaseyBatten Jul 25, 2024
051e1bd
Merge branch 'edge' into abs96_move-lid-command
CaseyBatten Jul 25, 2024
4ceef15
correct max api version
CaseyBatten Jul 25, 2024
93176e3
analysis validation fix accounting for reader lid as fixture
CaseyBatten Jul 25, 2024
a9b16dd
Lid position automatic validation and engine adjustment and labware m…
CaseyBatten Aug 1, 2024
3b47af6
full lid status live data hookups and initial polling architecture
CaseyBatten Aug 2, 2024
e3d8fea
Merge branch 'edge' into abs96_lid_status_engine
CaseyBatten Aug 9, 2024
e1d99dd
Merge branch 'edge' into abs96_lid_status_engine
CaseyBatten Aug 9, 2024
564cd9f
fix for is lid on
CaseyBatten Aug 12, 2024
91a3242
linting and app fix
CaseyBatten Aug 12, 2024
9b01734
no op open and close lid if lid already in desired end position
CaseyBatten Aug 12, 2024
f776469
doc and error text updates
CaseyBatten Aug 13, 2024
ab5caef
lid live data positioning corrections
CaseyBatten Aug 14, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion api/docs/v2/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -445,7 +445,7 @@
("py:class", r".*protocol_api\.config.*"),
("py:class", r".*opentrons_shared_data.*"),
("py:class", r".*protocol_api._parameters.Parameters.*"),
("py:class", r".*AbsorbanceReaderContext"), # shh it's a secret (for now)
("py:class", r".*AbsorbanceReaderContext"),
("py:class", r".*RobotContext"), # shh it's a secret (for now)
("py:class", r'.*AbstractLabware|APIVersion|LabwareLike|LoadedCoreMap|ModuleTypes|NoneType|OffDeckType|ProtocolCore|WellCore'), # laundry list of not fully qualified things
]
1 change: 1 addition & 0 deletions api/src/opentrons/drivers/absorbance_reader/driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ async def get_available_wavelengths(self) -> List[int]:
return await self._connection.get_supported_wavelengths()

async def get_single_measurement(self, wavelength: int) -> List[float]:
# TODO (cb, 08-02-2024): The list of measurements for 96 wells is rotated 180 degrees (well A1 is where well H12 should be) this must be corrected
return await self._connection.get_single_measurement(wavelength)

async def initialize_measurement(self, wavelength: int) -> None:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -204,11 +204,11 @@ def status(self) -> AbsorbanceReaderStatus:

@property
def lid_status(self) -> AbsorbanceReaderLidStatus:
return AbsorbanceReaderLidStatus.UNKNOWN
return self._reader.lid_status

@property
def plate_presence(self) -> AbsorbanceReaderPlatePresence:
return AbsorbanceReaderPlatePresence.UNKNOWN
return self._reader.plate_presence

@property
def device_info(self) -> Mapping[str, str]:
Expand Down Expand Up @@ -321,6 +321,11 @@ async def get_current_wavelength(self) -> None:
"""Get the Absorbance Reader's current active wavelength."""
pass # TODO: implement

async def get_current_lid_status(self) -> AbsorbanceReaderLidStatus:
"""Get the Absorbance Reader's current lid status."""
await self._reader.get_lid_status()
return self._reader.lid_status

def _enter_error_state(self, error: Exception) -> None:
self._error = str(error)
if isinstance(error, AbsorbanceReaderDisconnectedError):
Expand Down
7 changes: 7 additions & 0 deletions api/src/opentrons/protocol_api/core/engine/module_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -562,3 +562,10 @@ def open_lid(self) -> None:
moduleId=self.module_id,
)
)

def is_lid_on(self) -> bool:
"""Returns True if the Absorbance Reader's lid is currently on the Reader slot."""
abs_state = self._engine_client.state.modules.get_absorbance_reader_substate(
self.module_id
)
return abs_state.is_lid_on
4 changes: 4 additions & 0 deletions api/src/opentrons/protocol_api/core/module.py
Original file line number Diff line number Diff line change
Expand Up @@ -369,3 +369,7 @@ def close_lid(self) -> None:
@abstractmethod
def open_lid(self) -> None:
"""Open the Absorbance Reader's lid."""

@abstractmethod
def is_lid_on(self) -> bool:
"""Return True if the Absorbance Reader's lid is currently closed."""
7 changes: 6 additions & 1 deletion api/src/opentrons/protocol_api/module_contexts.py
Original file line number Diff line number Diff line change
Expand Up @@ -994,9 +994,14 @@ def close_lid(self) -> None:

@requires_version(2, 21)
def open_lid(self) -> None:
"""Close the lid of the Absorbance Reader."""
"""Open the lid of the Absorbance Reader."""
self._core.open_lid()

@requires_version(2, 21)
def is_lid_on(self) -> bool:
"""Return True if the Absorbance Reader's lid is currently closed."""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's Sphinx it up

Suggested change
"""Return True if the Absorbance Reader's lid is currently closed."""
"""Return ``True`` if the Absorbance Reader's lid is currently closed."""

I'm OK with leaving the implication that False means it's open, but if false means open or unknown, we should state that.

return self._core.is_lid_on()

@requires_version(2, 21)
def initialize(self, wavelength: int) -> None:
"""Initialize the Absorbance Reader by taking zero reading."""
Expand Down
8 changes: 8 additions & 0 deletions api/src/opentrons/protocol_api/protocol_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
RobotTypeError,
UnsupportedAPIError,
)
from opentrons_shared_data.errors.exceptions import CommandPreconditionViolated

from ._types import OffDeckType
from .core.common import ModuleCore, LabwareCore, ProtocolCore
Expand Down Expand Up @@ -707,6 +708,13 @@ def move_labware(
f"Expected labware of type 'Labware' but got {type(labware)}."
)

# Ensure that when moving to an absorbance reader than the lid is open
if isinstance(new_location, AbsorbanceReaderContext):
if new_location.is_lid_on():
raise CommandPreconditionViolated(
f"Cannot move {labware.name} onto the Absorbance Reader Module when Lid is Closed."
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
f"Cannot move {labware.name} onto the Absorbance Reader Module when Lid is Closed."
f"Cannot move {labware.name} onto the Absorbance Reader Module when its lid is closed."

nit

)

location: Union[
ModuleCore,
LabwareCore,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,14 @@

from ..command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData
from ...errors.error_occurrence import ErrorOccurrence
from ...errors import CannotPerformModuleAction
from opentrons.protocol_engine.types import AddressableAreaLocation

from opentrons.protocol_engine.resources import labware_validation
from .types import MoveLidResult

from opentrons.drivers.types import AbsorbanceReaderLidStatus

if TYPE_CHECKING:
from opentrons.protocol_engine.state import StateView
from opentrons.protocol_engine.execution import (
Expand Down Expand Up @@ -55,17 +59,30 @@ async def execute(
mod_substate = self._state_view.modules.get_absorbance_reader_substate(
module_id=params.moduleId
)
# Make sure the lid is open
mod_substate.raise_if_lid_status_not_expected(lid_on_expected=False)

# Allow propagation of ModuleNotAttachedError.
_ = self._equipment.get_module_hardware_api(mod_substate.module_id)

# lid should currently be docked
# lid should currently be on the module
assert mod_substate.lid_id is not None
loaded_lid = self._state_view.labware.get(mod_substate.lid_id)
assert labware_validation.is_absorbance_reader_lid(loaded_lid.loadName)

# If the lid is already Closed, No-op out
if mod_substate.is_lid_on:
current_offset_id = self._equipment.find_applicable_labware_offset_id(
labware_definition_uri=loaded_lid.definitionUri,
labware_location=loaded_lid.location,
)
return SuccessData(
public=CloseLidResult(
lidId=loaded_lid.id,
newLocation=loaded_lid.location,
offsetId=current_offset_id,
),
private=None,
)

# Allow propagation of ModuleNotAttachedError.
_ = self._equipment.get_module_hardware_api(mod_substate.module_id)

current_location = loaded_lid.location
validated_current_location = (
self._state_view.geometry.ensure_valid_gripper_location(current_location)
Expand Down Expand Up @@ -108,6 +125,21 @@ async def execute(
labware_definition_uri=loaded_lid.definitionUri,
labware_location=new_location,
)

if not self._state_view.config.use_virtual_modules:
abs_reader = self._equipment.get_module_hardware_api(mod_substate.module_id)

if abs_reader is not None:
result = await abs_reader.get_current_lid_status()
if result is not AbsorbanceReaderLidStatus.ON:
raise CannotPerformModuleAction(
"The Opentrons Plate Reader lid mechanicaly position did not match expected Closed state."
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can this error ever bubble up to be shown to users? if so, needs an edit.

Suggested change
"The Opentrons Plate Reader lid mechanicaly position did not match expected Closed state."
"The mechanical position of the Absorbance Plate Reader's lid did not match the expected 'closed' state."

)
else:
raise CannotPerformModuleAction(
"Could not reach the Hardware API for Opentrons Plate Reader Module."
)

return SuccessData(
public=CloseLidResult(
lidId=loaded_lid.id, newLocation=new_location, offsetId=new_offset_id
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,13 @@

from ..command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData
from ...errors.error_occurrence import ErrorOccurrence
from ...errors import CannotPerformModuleAction

from .types import MoveLidResult
from opentrons.protocol_engine.resources import labware_validation

from opentrons.drivers.types import AbsorbanceReaderLidStatus

if TYPE_CHECKING:
from opentrons.protocol_engine.state import StateView
from opentrons.protocol_engine.execution import (
Expand Down Expand Up @@ -50,17 +54,29 @@ async def execute(self, params: OpenLidParams) -> SuccessData[OpenLidResult, Non
mod_substate = self._state_view.modules.get_absorbance_reader_substate(
module_id=params.moduleId
)
# Make sure the lid is closed
mod_substate.raise_if_lid_status_not_expected(lid_on_expected=True)

# Allow propagation of ModuleNotAttachedError.
_ = self._equipment.get_module_hardware_api(mod_substate.module_id)

# lid should currently be on the module
assert mod_substate.lid_id is not None
loaded_lid = self._state_view.labware.get(mod_substate.lid_id)
assert labware_validation.is_absorbance_reader_lid(loaded_lid.loadName)

# If the lid is already Open, No-op out
if not mod_substate.is_lid_on:
current_offset_id = self._equipment.find_applicable_labware_offset_id(
labware_definition_uri=loaded_lid.definitionUri,
labware_location=loaded_lid.location,
)
return SuccessData(
public=OpenLidResult(
lidId=loaded_lid.id,
newLocation=loaded_lid.location,
offsetId=current_offset_id,
),
private=None,
)

# Allow propagation of ModuleNotAttachedError.
_ = self._equipment.get_module_hardware_api(mod_substate.module_id)

current_location = loaded_lid.location
validated_current_location = (
self._state_view.geometry.ensure_valid_gripper_location(current_location)
Expand Down Expand Up @@ -94,6 +110,21 @@ async def execute(self, params: OpenLidParams) -> SuccessData[OpenLidResult, Non
labware_definition_uri=loaded_lid.definitionUri,
labware_location=new_location,
)

if not self._state_view.config.use_virtual_modules:
abs_reader = self._equipment.get_module_hardware_api(mod_substate.module_id)

if abs_reader is not None:
result = await abs_reader.get_current_lid_status()
if result is not AbsorbanceReaderLidStatus.OFF:
raise CannotPerformModuleAction(
"The Opentrons Plate Reader lid mechanicaly position did not match expected Open state."
)
else:
raise CannotPerformModuleAction(
"Could not reach the Hardware API for Opentrons Plate Reader Module."
)

return SuccessData(
public=OpenLidResult(
lidId=loaded_lid.id,
Expand Down
35 changes: 35 additions & 0 deletions api/src/opentrons/protocol_engine/commands/load_module.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

from opentrons.protocol_engine.resources import deck_configuration_provider

from opentrons.drivers.types import AbsorbanceReaderLidStatus

if TYPE_CHECKING:
from ..state import StateView
from ..execution import EquipmentHandler
Expand Down Expand Up @@ -151,6 +153,39 @@ async def execute(
module_id=params.moduleId,
)

# Handle lid position update for loaded Plate Reader module on deck
if (
not self._state_view.config.use_virtual_modules
and params.model == ModuleModel.ABSORBANCE_READER_V1
and params.moduleId is not None
):
abs_reader = self._equipment.get_module_hardware_api(
self._state_view.modules.get_absorbance_reader_substate(
params.moduleId
).module_id
)

if abs_reader is not None:
result = await abs_reader.get_current_lid_status()
if (
isinstance(result, AbsorbanceReaderLidStatus)
and result is not AbsorbanceReaderLidStatus.ON
):
reader_area = self._state_view.modules.ensure_and_convert_module_fixture_location(
params.location.slotName,
self._state_view.config.deck_type,
params.model,
)
lid_labware = self._state_view.labware.get_by_addressable_area(
reader_area
)
if lid_labware is not None:
lid_labware.location = (
self._state_view.modules.absorbance_reader_dock_location(
params.moduleId
)
)

return SuccessData(
public=LoadModuleResult(
moduleId=loaded_module.module_id,
Expand Down
16 changes: 16 additions & 0 deletions api/src/opentrons/protocol_engine/state/labware.py
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,22 @@ def get_by_slot(

return None

def get_by_addressable_area(
self,
addressable_area: str,
) -> Optional[LoadedLabware]:
"""Get the labware located in a given addressable area, if any."""
loaded_labware = list(self._state.labware_by_id.values())

for labware in loaded_labware:
if (
isinstance(labware.location, AddressableAreaLocation)
and labware.location.addressableAreaName == addressable_area
):
return labware

return None

def get_definition(self, labware_id: str) -> LabwareDefinition:
"""Get labware definition by the labware's unique identifier."""
return self.get_definition_by_uri(
Expand Down
Loading