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): add protocol api and engine support for absorbance plate reader #15266

Merged
merged 28 commits into from
Jun 12, 2024
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
3e56cfd
PLAT-194 deck configuration for plate reader
ahiuchingau May 24, 2024
af7558b
plate reader definition revised
ahiuchingau May 24, 2024
af452a9
frontend support for absorbance plate reader fixture
ahiuchingau May 24, 2024
e857448
PLAT-322 live data model for abs plate reader
ahiuchingau May 24, 2024
638ffa2
PLAT-323 deck configurator abs reader fixture
ahiuchingau May 24, 2024
efc776b
add absorbance reader driver enum names - get device status
ahiuchingau May 24, 2024
bac887a
PLAT-324 abs plate reader module core
ahiuchingau May 24, 2024
5103831
PLAT-325 add protocol module context
ahiuchingau May 24, 2024
0b27647
PLAT-206 & PLAT-204: add engine commands for initialize and measure'
ahiuchingau May 24, 2024
09aa5f2
fix module hardware controlg
ahiuchingau May 24, 2024
32161d1
protocol engine support
ahiuchingau May 24, 2024
e6ba88c
some frontend changes to support the module
ahiuchingau May 24, 2024
0c7d7d8
fix js-lint part 1
ahiuchingau May 28, 2024
6e38646
make sure abs plate reader is added to attachedModule in apiClient
ahiuchingau May 28, 2024
dd9f5e8
oops
ahiuchingau May 29, 2024
cf45742
format
ahiuchingau May 29, 2024
23225ab
update deck config test
ahiuchingau May 29, 2024
4e2036d
update command snapshot
ahiuchingau May 29, 2024
5b26d71
remove unused import
ahiuchingau May 30, 2024
009d81d
bump api version requirement for module context to 2.18
ahiuchingau May 30, 2024
d66a358
add unit tests
ahiuchingau May 31, 2024
acdf35b
review changes
ahiuchingau Jun 11, 2024
2ec2cef
remove unused func
ahiuchingau Jun 11, 2024
017c521
made prettier
ahiuchingau Jun 11, 2024
9da154d
add OT_PD feature flag to enable abs plate reader
ahiuchingau Jun 11, 2024
2447ddb
hide abs reader behind a feature flag in pd
ahiuchingau Jun 12, 2024
fcc5b2f
fix lint & typo
ahiuchingau Jun 12, 2024
b269f43
remove unused imports
ahiuchingau Jun 12, 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
8 changes: 8 additions & 0 deletions api-client/src/modules/api-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,12 @@ export interface HeaterShakerData {
errorDetails: string | null
status: HeaterShakerStatus
}
export interface AbsorbanceReaderData {
lidStatus: 'open' | 'closed' | 'unknown'
platePresence: 'present' | 'absent' | 'unknown'
sampleWavelength: number | null
status: AbsorbanceReaderStatus
}

export type TemperatureStatus =
| 'idle'
Expand Down Expand Up @@ -112,3 +118,5 @@ export type LatchStatus =
| 'idle_closed'
| 'idle_unknown'
| 'unknown'

export type AbsorbanceReaderStatus = 'idle' | 'measuring' | 'error'
9 changes: 9 additions & 0 deletions api-client/src/modules/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ import type {
ThermocyclerModuleModel,
MagneticModuleModel,
HeaterShakerModuleModel,
AbsorbanceReaderModel,
TEMPERATURE_MODULE_TYPE,
MAGNETIC_MODULE_TYPE,
THERMOCYCLER_MODULE_TYPE,
HEATERSHAKER_MODULE_TYPE,
ABSORBANCE_READER_TYPE,
} from '@opentrons/shared-data'

import type * as ApiTypes from './api-types'
Expand Down Expand Up @@ -44,11 +46,18 @@ export interface HeaterShakerModule extends CommonModuleInfo {
data: ApiTypes.HeaterShakerData
}

export interface AbsorbanceReaderModule extends CommonModuleInfo {
moduleType: typeof ABSORBANCE_READER_TYPE
moduleModel: AbsorbanceReaderModel
data: ApiTypes.AbsorbanceReaderData
}

export type AttachedModule =
| TemperatureModule
| MagneticModule
| ThermocyclerModule
| HeaterShakerModule
| AbsorbanceReaderModule

export interface ModulesMeta {
cursor: number
Expand Down
7 changes: 5 additions & 2 deletions api/src/opentrons/drivers/absorbance_reader/abstract.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
from abc import ABC, abstractmethod
from typing import Dict, List
from opentrons.drivers.types import AbsorbanceReaderLidStatus
from opentrons.drivers.types import (
AbsorbanceReaderLidStatus,
AbsorbanceReaderDeviceState,
)


class AbstractAbsorbanceReaderDriver(ABC):
Expand Down Expand Up @@ -36,7 +39,7 @@ async def initialize_measurement(self, wavelength: int) -> None:
...

@abstractmethod
async def get_status(self) -> None:
async def get_status(self) -> AbsorbanceReaderDeviceState:
...

@abstractmethod
Expand Down
30 changes: 28 additions & 2 deletions api/src/opentrons/drivers/absorbance_reader/async_byonoy.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,17 @@
from concurrent.futures.thread import ThreadPoolExecutor
from functools import partial
from typing import Optional, List, Dict
import usb.core as usb_core # type: ignore[import-untyped]


from .hid_protocol import AbsorbanceHidInterface as AbsProtocol, ErrorCodeNames
from .hid_protocol import (
AbsorbanceHidInterface as AbsProtocol,
ErrorCodeNames,
DeviceStateNames,
)
from opentrons.drivers.types import (
AbsorbanceReaderLidStatus,
AbsorbanceReaderPlatePresence,
AbsorbanceReaderDeviceState,
)
from opentrons.drivers.rpi_drivers.types import USBPort

Expand All @@ -36,6 +40,8 @@ def serial_number_from_port(name: str) -> str:
"""
Get the serial number from a port using pyusb.
"""
import usb.core as usb_core # type: ignore[import-untyped]

port_numbers = tuple(int(s) for s in name.split("-")[1].split("."))
device = usb_core.find(port_numbers=port_numbers)
if device:
Expand Down Expand Up @@ -232,6 +238,7 @@ async def get_device_static_info(self) -> Dict[str, str]:
return {
"serial": self._device.sn,
"model": "ABS96",
"version": "1.0",
}

async def get_device_information(self) -> Dict[str, str]:
Expand Down Expand Up @@ -270,3 +277,22 @@ async def get_single_measurement(self, wavelength: int) -> List[float]:

async def get_plate_presence(self) -> AbsorbanceReaderPlatePresence:
return AbsorbanceReaderPlatePresence.UNKNOWN

async def get_device_status(self) -> AbsorbanceReaderDeviceState:
status = await self._loop.run_in_executor(
executor=self._executor,
func=self._get_device_status,
)
return self.convert_device_state(status.name)

@staticmethod
def convert_device_state(
device_state: DeviceStateNames,
) -> AbsorbanceReaderDeviceState:
state_map: Dict[DeviceStateNames, AbsorbanceReaderDeviceState] = {
"BYONOY_DEVICE_STATE_UNKNOWN": AbsorbanceReaderDeviceState.UNKNOWN,
"BYONOY_DEVICE_STATE_OK": AbsorbanceReaderDeviceState.OK,
"BYONOY_DEVICE_STATE_BROKEN_FW": AbsorbanceReaderDeviceState.BROKEN_FW,
"BYONOY_DEVICE_STATE_ERROR": AbsorbanceReaderDeviceState.ERROR,
}
return state_map[device_state]
13 changes: 10 additions & 3 deletions api/src/opentrons/drivers/absorbance_reader/driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@
import asyncio
from typing import Dict, Optional, List, TYPE_CHECKING

from opentrons.drivers.types import AbsorbanceReaderLidStatus
from opentrons.drivers.types import (
AbsorbanceReaderLidStatus,
AbsorbanceReaderDeviceState,
AbsorbanceReaderPlatePresence,
)
from opentrons.drivers.absorbance_reader.abstract import AbstractAbsorbanceReaderDriver
from opentrons.drivers.rpi_drivers.types import USBPort

Expand Down Expand Up @@ -61,5 +65,8 @@ async def get_single_measurement(self, wavelength: int) -> List[float]:
async def initialize_measurement(self, wavelength: int) -> None:
await self._connection.initialize(wavelength)

async def get_status(self) -> None:
pass
async def get_status(self) -> AbsorbanceReaderDeviceState:
return await self._connection.get_device_status()

async def get_plate_presence(self) -> AbsorbanceReaderPlatePresence:
return await self._connection.get_plate_presence()
23 changes: 20 additions & 3 deletions api/src/opentrons/drivers/absorbance_reader/hid_protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
List,
Literal,
Tuple,
ClassVar,
runtime_checkable,
TypeVar,
)
Expand Down Expand Up @@ -36,6 +35,20 @@
"BYONOY_ERROR_INTERNAL",
]

SlotStateNames = Literal[
"BYONOY_SLOT_UNKNOWN",
"BYONOY_SLOT_EMPTY",
"BYONOY_SLOT_OCCUPIED",
"BYONOY_SLOT_UNDETERMINED",
]

DeviceStateNames = Literal[
"BYONOY_DEVICE_STATE_UNKNOWN",
"BYONOY_DEVICE_STATE_OK",
"BYONOY_DEVICE_STATE_BROKEN_FW",
"BYONOY_DEVICE_STATE_ERROR",
]


@runtime_checkable
class AbsorbanceHidInterface(Protocol):
Expand All @@ -51,7 +64,9 @@ class ErrorCode(Protocol):

@runtime_checkable
class SlotState(Protocol):
__members__: ClassVar[Dict[str, int]]
__members__: Dict[SlotStateNames, int]
name: SlotStateNames
value: int

@runtime_checkable
class MeasurementConfig(Protocol):
Expand All @@ -65,7 +80,9 @@ class DeviceInfo(Protocol):

@runtime_checkable
class DeviceState(Protocol):
__members__: ClassVar[Dict[str, int]]
__members__: Dict[DeviceStateNames, int]
name: DeviceStateNames
value: int

def ByonoyAbs96SingleMeasurementConfig(self) -> MeasurementConfig:
...
Expand Down
9 changes: 6 additions & 3 deletions api/src/opentrons/drivers/absorbance_reader/simulator.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
from typing import Dict, List, Optional
from opentrons.util.async_helpers import ensure_yield

from opentrons.drivers.types import AbsorbanceReaderLidStatus
from opentrons.drivers.types import (
AbsorbanceReaderLidStatus,
AbsorbanceReaderDeviceState,
)

from .abstract import AbstractAbsorbanceReaderDriver

Expand Down Expand Up @@ -51,5 +54,5 @@ async def initialize_measurement(self, wavelength: int) -> None:
pass

@ensure_yield
async def get_status(self) -> None:
pass
async def get_status(self) -> AbsorbanceReaderDeviceState:
return AbsorbanceReaderDeviceState.OK
11 changes: 10 additions & 1 deletion api/src/opentrons/drivers/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,4 +73,13 @@ class AbsorbanceReaderPlatePresence(str, Enum):

UNKNOWN = "unknown"
PRESENT = "present"
ABSENCE = "absence"
ABSENT = "absent"


class AbsorbanceReaderDeviceState(str, Enum):
"""Absorbance reader device state."""

UNKNOWN = "unknown"
OK = "ok"
BROKEN_FW = "broken_fw"
ERROR = "error"
2 changes: 2 additions & 0 deletions api/src/opentrons/hardware_control/modules/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
TemperatureStatus,
MagneticStatus,
HeaterShakerStatus,
AbsorbanceReaderStatus,
SpeedStatus,
LiveData,
)
Expand Down Expand Up @@ -47,4 +48,5 @@
"SpeedStatus",
"LiveData",
"AbsorbanceReader",
"AbsorbanceReaderStatus",
]
66 changes: 60 additions & 6 deletions api/src/opentrons/hardware_control/modules/absorbance_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,14 @@
AbsorbanceReaderDriver,
SimulatingDriver,
)
from opentrons.drivers.types import (
AbsorbanceReaderLidStatus,
AbsorbanceReaderPlatePresence,
AbsorbanceReaderDeviceState,
)

from opentrons.hardware_control.execution_manager import ExecutionManager
from opentrons.hardware_control.poller import Reader
from opentrons.hardware_control.modules import mod_abc
from opentrons.hardware_control.modules.types import (
ModuleType,
Expand Down Expand Up @@ -56,6 +63,7 @@ async def build(
driver=driver,
hw_control_loop=hw_control_loop,
)
await module.setup()
return module

def __init__(
Expand Down Expand Up @@ -83,6 +91,14 @@ def status(self) -> AbsorbanceReaderStatus:
"""Return some string describing status."""
return AbsorbanceReaderStatus.IDLE

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

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

@property
def device_info(self) -> Mapping[str, str]:
"""Return a dict of the module's static information (serial, etc)"""
Expand All @@ -92,8 +108,10 @@ def device_info(self) -> Mapping[str, str]:
def live_data(self) -> LiveData:
"""Return a dict of the module's dynamic information"""
return {
"status": "idle",
"status": self.status.value,
"data": {
"lidStatus": self.lid_status.value,
"platePresence": self.plate_presence.value,
"sampleWavelength": 400,
},
}
Expand Down Expand Up @@ -159,14 +177,50 @@ async def set_sample_wavelength(self, wavelength: int) -> None:
"""Set the Absorbance Reader's active wavelength."""
await self._driver.initialize_measurement(wavelength)

async def start_measure(self, wavelength: int) -> None:
async def start_measure(self, wavelength: int) -> List[float]:
"""Initiate a single measurement."""
await self._driver.get_single_measurement(wavelength)
return await self._driver.get_single_measurement(wavelength)

async def get_supported_wavelengths(self) -> List[int]:
"""Get the Absorbance Reader's supported wavelengths."""
return await self._driver.get_available_wavelengths()
async def setup(self) -> None:
"""Setup the Absorbance Reader."""
is_open = await self._driver.is_connected()
if not is_open:
await self._driver.connect()

async def get_current_wavelength(self) -> None:
"""Get the Absorbance Reader's current active wavelength."""
pass


class AbsorbanceReaderReader(Reader):
device_state: AbsorbanceReaderDeviceState
lid_status: AbsorbanceReaderLidStatus
plate_presence: AbsorbanceReaderPlatePresence
supported_wavelengths: List[int]

def __init__(self, driver: AbsorbanceReaderDriver) -> None:
self.device_state = AbsorbanceReaderDeviceState.UNKNOWN
self.lid_status = AbsorbanceReaderLidStatus.UNKNOWN
self.plate_presence = AbsorbanceReaderPlatePresence.UNKNOWN
self.supported_wavelengths = []
self._driver = driver

async def read(self) -> None:
await self.get_device_status()
await self.get_supported_wavelengths()

async def get_device_status(self) -> None:
"""Get the Absorbance Reader's current status."""
self.device_state = await self._driver.get_status()

async def get_supported_wavelengths(self) -> None:
"""Get the Absorbance Reader's supported wavelengths."""
self.supported_wavelengths = await self._driver.get_available_wavelengths()

async def get_lid_status(self) -> None:
"""Get the Absorbance Reader's lid status."""
self.lid_status = await self._driver.get_lid_status()

async def get_plate_presence(self) -> None:
"""Get the Absorbance Reader's plate presence."""
self.plate_presence = await self._driver.get_plate_presence()
9 changes: 9 additions & 0 deletions api/src/opentrons/hardware_control/modules/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ def to_module_fixture_id(cls, module_type: ModuleType) -> str:
return "heaterShakerModuleV1"
if module_type == ModuleType.MAGNETIC_BLOCK:
return "magneticBlockV1"
if module_type == ModuleType.ABSORBANCE_READER:
return "absorbanceReaderV1"
else:
raise ValueError(
f"Module Type {module_type} does not have a related fixture ID."
Expand Down Expand Up @@ -210,3 +212,10 @@ class AbsorbanceReaderStatus(str, Enum):
IDLE = "idle"
MEASURING = "measuring"
ERROR = "error"


class LidStatus(str, Enum):
ON = "on"
OFF = "off"
UNKNOWN = "unknown"
ERROR = "error"
2 changes: 2 additions & 0 deletions api/src/opentrons/protocol_api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
TemperatureModuleContext,
HeaterShakerContext,
MagneticBlockContext,
AbsorbanceReaderContext,
)
from .disposal_locations import TrashBin, WasteChute
from ._liquid import Liquid
Expand Down Expand Up @@ -50,6 +51,7 @@
"ThermocyclerContext",
"HeaterShakerContext",
"MagneticBlockContext",
"AbsorbanceReaderContext",
"ParameterContext",
"Labware",
"TrashBin",
Expand Down
Loading
Loading