Skip to content

Commit

Permalink
feat(protocol-engine): absorbance plate reader engine load behavior a…
Browse files Browse the repository at this point in the history
…nd lid movement command (#15599)

Covers PLAT-447 PLAT-381 PLAT-210 PLAT-209 PLAT-215 
Introduces the fundamental load module and labware presence behavior for the plate reader and it's associated lid, includes the `open_lid` and `close_lid` commands for the plate reader.
  • Loading branch information
ahiuchingau authored Aug 9, 2024
1 parent 7e28775 commit 58a7d19
Show file tree
Hide file tree
Showing 54 changed files with 1,281 additions and 235 deletions.
1 change: 1 addition & 0 deletions api/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ def get_version():
"pyserial>=3.5",
"typing-extensions>=4.0.0,<5",
"click>=8.0.0,<9",
"pyusb==1.2.1",
'importlib-metadata >= 1.0 ; python_version < "3.8"',
"packaging>=21.0",
]
Expand Down
18 changes: 17 additions & 1 deletion api/src/opentrons/drivers/absorbance_reader/async_byonoy.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
AbsorbanceHidInterface as AbsProtocol,
ErrorCodeNames,
DeviceStateNames,
SlotStateNames,
)
from opentrons.drivers.types import (
AbsorbanceReaderLidStatus,
Expand Down Expand Up @@ -276,7 +277,10 @@ async def get_single_measurement(self, wavelength: int) -> List[float]:
)

async def get_plate_presence(self) -> AbsorbanceReaderPlatePresence:
return AbsorbanceReaderPlatePresence.UNKNOWN
presence = await self._loop.run_in_executor(
executor=self._executor, func=self._get_slot_status
)
return self.convert_plate_presence(presence.name)

async def get_device_status(self) -> AbsorbanceReaderDeviceState:
status = await self._loop.run_in_executor(
Expand All @@ -296,3 +300,15 @@ def convert_device_state(
"BYONOY_DEVICE_STATE_ERROR": AbsorbanceReaderDeviceState.ERROR,
}
return state_map[device_state]

@staticmethod
def convert_plate_presence(
slot_state: SlotStateNames,
) -> AbsorbanceReaderPlatePresence:
state_map: Dict[SlotStateNames, AbsorbanceReaderPlatePresence] = {
"UNKNOWN": AbsorbanceReaderPlatePresence.UNKNOWN,
"EMPTY": AbsorbanceReaderPlatePresence.ABSENT,
"OCCUPIED": AbsorbanceReaderPlatePresence.PRESENT,
"UNDETERMINED": AbsorbanceReaderPlatePresence.UNKNOWN,
}
return state_map[slot_state]
8 changes: 4 additions & 4 deletions api/src/opentrons/drivers/absorbance_reader/hid_protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,10 @@
]

SlotStateNames = Literal[
"BYONOY_SLOT_UNKNOWN",
"BYONOY_SLOT_EMPTY",
"BYONOY_SLOT_OCCUPIED",
"BYONOY_SLOT_UNDETERMINED",
"UNKNOWN",
"EMPTY",
"OCCUPIED",
"UNDETERMINED",
]

DeviceStateNames = Literal[
Expand Down
12 changes: 10 additions & 2 deletions api/src/opentrons/protocol_api/core/engine/labware.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@
from opentrons_shared_data.labware.labware_definition import LabwareRole

from opentrons.protocol_engine import commands as cmd
from opentrons.protocol_engine.errors import LabwareNotOnDeckError, ModuleNotOnDeckError
from opentrons.protocol_engine.errors import (
LabwareNotOnDeckError,
ModuleNotOnDeckError,
LocationIsStagingSlotError,
)
from opentrons.protocol_engine.clients import SyncClient as ProtocolEngineClient
from opentrons.protocol_engine.types import (
LabwareOffsetCreate,
Expand Down Expand Up @@ -185,5 +189,9 @@ def get_deck_slot(self) -> Optional[DeckSlotName]:
return self._engine_client.state.geometry.get_ancestor_slot_name(
self.labware_id
)
except (LabwareNotOnDeckError, ModuleNotOnDeckError):
except (
LabwareNotOnDeckError,
ModuleNotOnDeckError,
LocationIsStagingSlotError,
):
return None
25 changes: 22 additions & 3 deletions api/src/opentrons/protocol_api/core/engine/module_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@
HeaterShakerLabwareLatchStatus,
ThermocyclerLidStatus,
)
from opentrons.types import DeckSlotName

from opentrons.protocol_engine import commands as cmd
from opentrons.types import DeckSlotName
from opentrons.protocol_engine.clients import SyncClient as ProtocolEngineClient
from opentrons.protocol_engine.errors.exceptions import (
LabwareNotLoadedOnModuleError,
Expand Down Expand Up @@ -535,11 +536,29 @@ def initialize(self, wavelength: int) -> None:
)
self._initialized_value = wavelength

def initiate_read(self) -> None:
def read(self) -> None:
"""Initiate read on the Absorbance Reader."""
if self._initialized_value:
self._engine_client.execute_command(
cmd.absorbance_reader.MeasureAbsorbanceParams(
cmd.absorbance_reader.ReadAbsorbanceParams(
moduleId=self.module_id, sampleWavelength=self._initialized_value
)
)

def close_lid(
self,
) -> None:
"""Close the Absorbance Reader's lid."""
self._engine_client.execute_command(
cmd.absorbance_reader.CloseLidParams(
moduleId=self.module_id,
)
)

def open_lid(self) -> None:
"""Close the Absorbance Reader's lid."""
self._engine_client.execute_command(
cmd.absorbance_reader.OpenLidParams(
moduleId=self.module_id,
)
)
25 changes: 25 additions & 0 deletions api/src/opentrons/protocol_api/core/engine/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -441,10 +441,35 @@ def load_module(
existing_module_ids=list(self._module_cores_by_id.keys()),
)

# When the protocol engine is created, we add Module Lids as part of the deck fixed labware
# If a valid module exists in the deck config. For analysis, we add the labware here since
# deck fixed labware is not created under the same conditions.
if self._engine_client.state.config.use_virtual_modules:
self._load_virtual_module_lid(module_core)

self._module_cores_by_id[module_core.module_id] = module_core

return module_core

def _load_virtual_module_lid(
self, module_core: Union[ModuleCore, NonConnectedModuleCore]
) -> None:
if isinstance(module_core, AbsorbanceReaderCore):
lid = self._engine_client.execute_command_without_recovery(
cmd.LoadLabwareParams(
loadName="opentrons_flex_lid_absorbance_plate_reader_module",
location=ModuleLocation(moduleId=module_core.module_id),
namespace="opentrons",
version=1,
displayName="Absorbance Reader Lid",
)
)

self._engine_client.add_absorbance_reader_lid(
module_id=module_core.module_id,
lid_id=lid.labwareId,
)

def _create_non_connected_module_core(
self, load_module_result: LoadModuleResult
) -> NonConnectedModuleCore:
Expand Down
12 changes: 10 additions & 2 deletions api/src/opentrons/protocol_api/core/module.py
Original file line number Diff line number Diff line change
Expand Up @@ -359,5 +359,13 @@ def initialize(self, wavelength: int) -> None:
"""Initialize the Absorbance Reader by taking zero reading."""

@abstractmethod
def initiate_read(self) -> None:
"""Initiate read on the Absorbance Reader."""
def read(self) -> None:
"""Get an absorbance reading from the Absorbance Reader."""

@abstractmethod
def close_lid(self) -> None:
"""Close the Absorbance Reader's lid."""

@abstractmethod
def open_lid(self) -> None:
"""Open the Absorbance Reader's lid."""
22 changes: 16 additions & 6 deletions api/src/opentrons/protocol_api/module_contexts.py
Original file line number Diff line number Diff line change
Expand Up @@ -976,23 +976,33 @@ class AbsorbanceReaderContext(ModuleContext):
It should not be instantiated directly; instead, it should be
created through :py:meth:`.ProtocolContext.load_module`.
.. versionadded:: 2.18
.. versionadded:: 2.21
"""

_core: AbsorbanceReaderCore

@property
@requires_version(2, 18)
@requires_version(2, 21)
def serial_number(self) -> str:
"""Get the module's unique hardware serial number."""
return self._core.get_serial_number()

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

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

@requires_version(2, 21)
def initialize(self, wavelength: int) -> None:
"""Initialize the Absorbance Reader by taking zero reading."""
self._core.initialize(wavelength)

@requires_version(2, 18)
def initiate_read(self) -> None:
@requires_version(2, 21)
def read(self) -> None:
"""Initiate read on the Absorbance Reader."""
self._core.initiate_read()
self._core.read()
11 changes: 10 additions & 1 deletion api/src/opentrons/protocol_api/protocol_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@

from opentrons.types import Mount, Location, DeckLocation, DeckSlotName, StagingSlotName
from opentrons.legacy_broker import LegacyBroker
from opentrons.hardware_control.modules.types import MagneticBlockModel
from opentrons.hardware_control.modules.types import (
MagneticBlockModel,
AbsorbanceReaderModel,
)
from opentrons.legacy_commands import protocol_commands as cmds, types as cmd_types
from opentrons.legacy_commands.helpers import stringify_labware_movement_command
from opentrons.legacy_commands.publisher import (
Expand Down Expand Up @@ -827,6 +830,12 @@ def load_module(
until_version="2.15",
current_version=f"{self._api_version}",
)
if isinstance(
requested_model, AbsorbanceReaderModel
) and self._api_version < APIVersion(2, 21):
raise APIVersionError(
f"Module of type {module_name} is only available in versions 2.21 and above."
)

deck_slot = (
None
Expand Down
2 changes: 2 additions & 0 deletions api/src/opentrons/protocol_engine/actions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
DoorChangeAction,
ResetTipsAction,
SetPipetteMovementSpeedAction,
AddAbsorbanceReaderLidAction,
)

__all__ = [
Expand Down Expand Up @@ -56,6 +57,7 @@
"DoorChangeAction",
"ResetTipsAction",
"SetPipetteMovementSpeedAction",
"AddAbsorbanceReaderLidAction",
# action payload values
"PauseSource",
"FinishErrorDetails",
Expand Down
12 changes: 12 additions & 0 deletions api/src/opentrons/protocol_engine/actions/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,17 @@ class SetPipetteMovementSpeedAction:
speed: Optional[float]


@dataclass(frozen=True)
class AddAbsorbanceReaderLidAction:
"""Add the absorbance reader lid id to the absorbance reader module substate.
This action is dispatched the absorbance reader module is first loaded.
"""

module_id: str
lid_id: str


@dataclass(frozen=True)
class SetErrorRecoveryPolicyAction:
"""See `ProtocolEngine.set_error_recovery_policy()`."""
Expand Down Expand Up @@ -293,5 +304,6 @@ class SetErrorRecoveryPolicyAction:
AddLiquidAction,
ResetTipsAction,
SetPipetteMovementSpeedAction,
AddAbsorbanceReaderLidAction,
SetErrorRecoveryPolicyAction,
]
6 changes: 6 additions & 0 deletions api/src/opentrons/protocol_engine/clients/sync_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,12 @@ def add_addressable_area(self, addressable_area_name: str) -> None:
"add_addressable_area", addressable_area_name=addressable_area_name
)

def add_absorbance_reader_lid(self, module_id: str, lid_id: str) -> None:
"""Add an absorbance reader lid to the module state."""
self._transport.call_method(
"add_absorbance_reader_lid", module_id=module_id, lid_id=lid_id
)

def add_liquid(
self, name: str, color: Optional[str], description: Optional[str]
) -> Liquid:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,20 @@
"""Command models for Absorbance Reader commands."""
from .types import MoveLidResult
from .close_lid import (
CloseLidCommandType,
CloseLidParams,
CloseLidResult,
CloseLid,
CloseLidCreate,
)

from .open_lid import (
OpenLidCommandType,
OpenLidParams,
OpenLidResult,
OpenLid,
OpenLidCreate,
)

from .initialize import (
InitializeCommandType,
Expand All @@ -8,25 +24,40 @@
InitializeCreate,
)

from .measure import (
MeasureAbsorbanceCommandType,
MeasureAbsorbanceParams,
MeasureAbsorbanceResult,
MeasureAbsorbance,
MeasureAbsorbanceCreate,
from .read import (
ReadAbsorbanceCommandType,
ReadAbsorbanceParams,
ReadAbsorbanceResult,
ReadAbsorbance,
ReadAbsorbanceCreate,
)


__all__ = [
"MoveLidResult",
# absorbanace_reader/closeLid
"CloseLidCommandType",
"CloseLidParams",
"CloseLidResult",
"CloseLid",
"CloseLidCreate",
# absorbanace_reader/openLid
"OpenLidCommandType",
"OpenLidParams",
"OpenLidResult",
"OpenLid",
"OpenLidCreate",
# absorbanace_reader/initialize
"InitializeCommandType",
"InitializeParams",
"InitializeResult",
"Initialize",
"InitializeCreate",
# absorbanace_reader/measure
"MeasureAbsorbanceCommandType",
"MeasureAbsorbanceParams",
"MeasureAbsorbanceResult",
"MeasureAbsorbance",
"MeasureAbsorbanceCreate",
"ReadAbsorbanceCommandType",
"ReadAbsorbanceParams",
"ReadAbsorbanceResult",
"ReadAbsorbance",
"ReadAbsorbanceCreate",
# union type
]
Loading

0 comments on commit 58a7d19

Please sign in to comment.