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(protocol-engine): absorbance plate reader engine load behavior and lid movement command #15599

Merged
merged 28 commits into from
Aug 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 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
acabc45
Removal of hard coded pickup and drop offsets from open and close lid…
CaseyBatten Aug 8, 2024
7f70a80
Merge branch 'edge' into abs96_move-lid-command
CaseyBatten Aug 8, 2024
9c26a9a
linting fixes
CaseyBatten Aug 8, 2024
9639e28
command schema v9 fix
CaseyBatten Aug 9, 2024
c77522e
schema lint
CaseyBatten Aug 9, 2024
83fd7d0
compatible labware pipette fix
CaseyBatten Aug 9, 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
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
Loading