From 09341e491025e3559980e9cb1b0410726957dcba Mon Sep 17 00:00:00 2001 From: vegano1 Date: Wed, 18 Sep 2024 08:27:42 -0400 Subject: [PATCH] merge in PLAT-474-add-multi-readings branch --- .../drivers/absorbance_reader/abstract.py | 14 +++++-- .../drivers/absorbance_reader/async_byonoy.py | 40 ++++++++++++------- .../drivers/absorbance_reader/driver.py | 5 ++- .../drivers/absorbance_reader/hid_protocol.py | 17 +++++++- .../drivers/absorbance_reader/simulator.py | 12 ++++-- api/src/opentrons/drivers/types.py | 2 +- .../modules/absorbance_reader.py | 30 +++++++++----- .../hardware_control/modules/magdeck.py | 6 +-- .../hardware_control/modules/mod_abc.py | 2 +- .../hardware_control/modules/tempdeck.py | 8 ++-- .../commands/absorbance_reader/close_lid.py | 2 +- .../commands/absorbance_reader/initialize.py | 2 +- .../commands/absorbance_reader/open_lid.py | 2 +- .../commands/absorbance_reader/read.py | 2 +- .../drivers/absorbance_reader/test_driver.py | 4 +- .../modules/module_data_mapper.py | 2 +- 16 files changed, 101 insertions(+), 49 deletions(-) diff --git a/api/src/opentrons/drivers/absorbance_reader/abstract.py b/api/src/opentrons/drivers/absorbance_reader/abstract.py index 6dc403fdff9..0391786f3e8 100644 --- a/api/src/opentrons/drivers/absorbance_reader/abstract.py +++ b/api/src/opentrons/drivers/absorbance_reader/abstract.py @@ -1,6 +1,7 @@ from abc import ABC, abstractmethod -from typing import Dict, List, Tuple +from typing import Dict, List, Optional, Tuple from opentrons.drivers.types import ( + ABSMeasurementMode, AbsorbanceReaderLidStatus, AbsorbanceReaderDeviceState, AbsorbanceReaderPlatePresence, @@ -32,11 +33,18 @@ async def get_available_wavelengths(self) -> List[int]: ... @abstractmethod - async def get_single_measurement(self, wavelength: int) -> List[float]: + async def initialize_measurement( + self, + wavelengths: List[int], + mode: ABSMeasurementMode = ABSMeasurementMode.SINGLE, + reference_wavelength: Optional[int] = None, + ) -> None: + """Initialize measurement for the device in single or multi mode for the given wavelengths""" ... @abstractmethod - async def initialize_measurement(self, wavelength: int) -> None: + async def get_measurement(self) -> List[List[float]]: + """Gets an absorbance reading with the current config.""" ... @abstractmethod diff --git a/api/src/opentrons/drivers/absorbance_reader/async_byonoy.py b/api/src/opentrons/drivers/absorbance_reader/async_byonoy.py index c2d9a248dd7..50f688206d0 100644 --- a/api/src/opentrons/drivers/absorbance_reader/async_byonoy.py +++ b/api/src/opentrons/drivers/absorbance_reader/async_byonoy.py @@ -10,11 +10,13 @@ ErrorCodeNames, DeviceStateNames, SlotStateNames, + MeasurementConfig, ) from opentrons.drivers.types import ( AbsorbanceReaderLidStatus, AbsorbanceReaderPlatePresence, AbsorbanceReaderDeviceState, + ABSMeasurementMode, ) from opentrons.drivers.rpi_drivers.types import USBPort from opentrons.hardware_control.modules.errors import AbsorbanceReaderDisconnectedError @@ -110,7 +112,7 @@ def __init__( self._loop = loop self._supported_wavelengths: Optional[list[int]] = None self._device_handle: Optional[int] = None - self._current_config: Optional[AbsProtocol.MeasurementConfig] = None + self._current_config: Optional[MeasurementConfig] = None async def open(self) -> bool: """ @@ -225,8 +227,8 @@ async def get_supported_wavelengths(self) -> list[int]: self._supported_wavelengths = wavelengths return wavelengths - async def get_single_measurement(self, wavelength: int) -> List[float]: - """Get a single measurement based on the current configuration.""" + async def get_measurement(self) -> List[List[float]]: + """Get a measurement based on the current configuration.""" handle = self._verify_device_handle() assert ( self._current_config is not None @@ -237,13 +239,13 @@ async def get_single_measurement(self, wavelength: int) -> List[float]: err, measurements = await self._loop.run_in_executor( executor=self._executor, func=partial( - self._interface.byonoy_abs96_single_measure, + measure_func, handle, self._current_config, ), ) - self._raise_if_error(err.name, f"Error getting single measurement: {err}") - return measurements + self._raise_if_error(err.name, f"Error getting measurement: {err}") + return measurements if isinstance(measurements[0], List) else [measurements] # type: ignore async def get_plate_presence(self) -> AbsorbanceReaderPlatePresence: """Get the state of the plate for the reader.""" @@ -265,7 +267,7 @@ def _get_supported_wavelengths(self) -> List[int]: self._supported_wavelengths = wavelengths return wavelengths - def _initialize_measurement(self, conf: AbsProtocol.MeasurementConfig) -> None: + def _initialize_measurement(self, conf: MeasurementConfig) -> None: handle = self._verify_device_handle() if isinstance(conf, AbsProtocol.SingleMeasurementConfig): err = self._interface.byonoy_abs96_initialize_single_measurement( @@ -278,7 +280,12 @@ def _initialize_measurement(self, conf: AbsProtocol.MeasurementConfig) -> None: self._raise_if_error(err.name, f"Error initializing measurement: {err}") self._current_config = conf - def _set_sample_wavelength(self, wavelength: int) -> AbsProtocol.MeasurementConfig: + def _initialize( + self, + mode: ABSMeasurementMode, + wavelengths: List[int], + reference_wavelength: Optional[int] = None, + ) -> None: if not self._supported_wavelengths: self._get_supported_wavelengths() assert self._supported_wavelengths @@ -293,17 +300,20 @@ def _set_sample_wavelength(self, wavelength: int) -> AbsProtocol.MeasurementConf conf.sample_wavelengths = wavelengths else: raise ValueError( - f"Unsupported wavelength: {wavelength}, expected: {self._supported_wavelengths}" + f"Unsupported wavelength: {wavelengths}, expected: {self._supported_wavelengths}" ) - - def _initialize(self, wavelength: int) -> None: - conf = self._set_sample_wavelength(wavelength) self._initialize_measurement(conf) - async def initialize(self, wavelength: int) -> None: - """Initialize the device so we can start reading samples from it.""" + async def initialize( + self, + mode: ABSMeasurementMode, + wavelengths: List[int], + reference_wavelength: Optional[int] = None, + ) -> None: + """initialize the device so we can start reading samples from it.""" await self._loop.run_in_executor( - executor=self._executor, func=partial(self._initialize, wavelength) + executor=self._executor, + func=partial(self._initialize, mode, wavelengths, reference_wavelength), ) def _verify_device_handle(self) -> int: diff --git a/api/src/opentrons/drivers/absorbance_reader/driver.py b/api/src/opentrons/drivers/absorbance_reader/driver.py index e028f3b6455..5899fef89d0 100644 --- a/api/src/opentrons/drivers/absorbance_reader/driver.py +++ b/api/src/opentrons/drivers/absorbance_reader/driver.py @@ -4,6 +4,7 @@ from typing import Dict, Optional, List, Tuple, TYPE_CHECKING from opentrons.drivers.types import ( + ABSMeasurementMode, AbsorbanceReaderLidStatus, AbsorbanceReaderDeviceState, AbsorbanceReaderPlatePresence, @@ -70,8 +71,8 @@ async def initialize_measurement( ) -> None: await self._connection.initialize(mode, wavelengths, reference_wavelength) - async def initialize_measurement(self, wavelength: int) -> None: - await self._connection.initialize(wavelength) + async def get_measurement(self) -> List[List[float]]: + return await self._connection.get_measurement() async def get_status(self) -> AbsorbanceReaderDeviceState: return await self._connection.get_device_status() diff --git a/api/src/opentrons/drivers/absorbance_reader/hid_protocol.py b/api/src/opentrons/drivers/absorbance_reader/hid_protocol.py index 1839e66c697..d05059889ae 100644 --- a/api/src/opentrons/drivers/absorbance_reader/hid_protocol.py +++ b/api/src/opentrons/drivers/absorbance_reader/hid_protocol.py @@ -1,9 +1,11 @@ from typing import ( Dict, + Optional, Protocol, List, Literal, Tuple, + Union, runtime_checkable, TypeVar, ) @@ -69,8 +71,13 @@ class SlotState(Protocol): value: int @runtime_checkable - class MeasurementConfig(Protocol): + class SingleMeasurementConfig(Protocol): sample_wavelength: int + reference_wavelength: Optional[int] + + @runtime_checkable + class MultiMeasurementConfig(Protocol): + sample_wavelengths: List[int] @runtime_checkable class DeviceInfo(Protocol): @@ -143,7 +150,7 @@ def byonoy_abs96_initialize_multiple_measurement( ... def byonoy_abs96_single_measure( - self, device_handle: int, conf: MeasurementConfig + self, device_handle: int, conf: SingleMeasurementConfig ) -> Tuple[ErrorCode, List[float]]: ... @@ -154,3 +161,9 @@ def byonoy_abs96_multiple_measure( def byonoy_available_devices(self) -> List[Device]: ... + + +MeasurementConfig = Union[ + AbsorbanceHidInterface.SingleMeasurementConfig, + AbsorbanceHidInterface.MultiMeasurementConfig, +] diff --git a/api/src/opentrons/drivers/absorbance_reader/simulator.py b/api/src/opentrons/drivers/absorbance_reader/simulator.py index 6a2a0aa6228..c4d7af2c1eb 100644 --- a/api/src/opentrons/drivers/absorbance_reader/simulator.py +++ b/api/src/opentrons/drivers/absorbance_reader/simulator.py @@ -2,6 +2,7 @@ from opentrons.util.async_helpers import ensure_yield from opentrons.drivers.types import ( + ABSMeasurementMode, AbsorbanceReaderLidStatus, AbsorbanceReaderDeviceState, AbsorbanceReaderPlatePresence, @@ -54,11 +55,16 @@ async def get_available_wavelengths(self) -> List[int]: return [450, 570, 600, 650] @ensure_yield - async def get_single_measurement(self, wavelength: int) -> List[float]: - return [0.0] + async def get_measurement(self) -> List[List[float]]: + return [[0.0]] @ensure_yield - async def initialize_measurement(self, wavelength: int) -> None: + async def initialize_measurement( + self, + wavelengths: List[int], + mode: ABSMeasurementMode = ABSMeasurementMode.SINGLE, + reference_wavelength: Optional[int] = None, + ) -> None: pass @ensure_yield diff --git a/api/src/opentrons/drivers/types.py b/api/src/opentrons/drivers/types.py index d8b5c84c902..d000ec96370 100644 --- a/api/src/opentrons/drivers/types.py +++ b/api/src/opentrons/drivers/types.py @@ -1,6 +1,6 @@ """ Type definitions for modules in this tree """ from dataclasses import dataclass -from typing import Dict, NamedTuple, Optional +from typing import Any, Dict, List, NamedTuple, Optional from enum import Enum diff --git a/api/src/opentrons/hardware_control/modules/absorbance_reader.py b/api/src/opentrons/hardware_control/modules/absorbance_reader.py index 6bae6385854..da7c4746086 100644 --- a/api/src/opentrons/hardware_control/modules/absorbance_reader.py +++ b/api/src/opentrons/hardware_control/modules/absorbance_reader.py @@ -12,6 +12,8 @@ AbsorbanceReaderLidStatus, AbsorbanceReaderPlatePresence, AbsorbanceReaderDeviceState, + ABSMeasurementMode, + ABSMeasurementConfig, ) from opentrons.hardware_control.execution_manager import ExecutionManager @@ -194,13 +196,21 @@ def __init__( self._device_info = device_info self._reader = reader self._poller = poller + self._measurement_config: Optional[ABSMeasurementConfig] = None + self._device_status = AbsorbanceReaderStatus.IDLE self._error: Optional[str] = None self._reader.register_error_handler(self._enter_error_state) @property def status(self) -> AbsorbanceReaderStatus: - """Return some string describing status.""" - return AbsorbanceReaderStatus.IDLE + """Return some string describing the device status.""" + state = self._reader.device_state + if state not in [ + AbsorbanceReaderDeviceState.UNKNOWN, + AbsorbanceReaderDeviceState.OK, + ]: + return AbsorbanceReaderStatus.ERROR + return self._device_status @property def lid_status(self) -> AbsorbanceReaderLidStatus: @@ -236,6 +246,8 @@ def live_data(self) -> LiveData: return { "status": self.status.value, "data": { + "uptime": self.uptime, + "deviceStatus": self.status.value, "lidStatus": self.lid_status.value, "platePresence": self.plate_presence.value, "measureMode": conf.get("measureMode", ""), @@ -345,13 +357,13 @@ async def set_sample_wavelength( reference_wavelength=reference_wavelength, ) - async def start_measure(self, wavelength: int) -> List[float]: - """Initiate a single measurement.""" - return await self._driver.get_single_measurement(wavelength) - - async def get_current_wavelength(self) -> None: - """Get the Absorbance Reader's current active wavelength.""" - pass # TODO: implement + async def start_measure(self) -> List[List[float]]: + """Initiate a measurement depending on the measurement mode.""" + try: + self._device_status = AbsorbanceReaderStatus.MEASURING + return await self._driver.get_measurement() + finally: + self._device_status = AbsorbanceReaderStatus.IDLE async def get_current_lid_status(self) -> AbsorbanceReaderLidStatus: """Get the Absorbance Reader's current lid status.""" diff --git a/api/src/opentrons/hardware_control/modules/magdeck.py b/api/src/opentrons/hardware_control/modules/magdeck.py index 467ce07803a..a97afde77b6 100644 --- a/api/src/opentrons/hardware_control/modules/magdeck.py +++ b/api/src/opentrons/hardware_control/modules/magdeck.py @@ -1,6 +1,6 @@ import asyncio import logging -from typing import Mapping, Optional +from typing import Dict, Optional from opentrons.drivers.mag_deck import ( SimulatingDriver, MagDeckDriver, @@ -83,7 +83,7 @@ def __init__( execution_manager: ExecutionManager, hw_control_loop: asyncio.AbstractEventLoop, driver: AbstractMagDeckDriver, - device_info: Mapping[str, str], + device_info: Dict[str, str], disconnected_callback: types.ModuleDisconnectedCallback = None, ) -> None: """Constructor""" @@ -166,7 +166,7 @@ def current_height(self) -> float: return self._current_height @property - def device_info(self) -> Mapping[str, str]: + def device_info(self) -> Dict[str, str]: """ Returns: a dict diff --git a/api/src/opentrons/hardware_control/modules/mod_abc.py b/api/src/opentrons/hardware_control/modules/mod_abc.py index 79574b7fb4a..b07c6156a88 100644 --- a/api/src/opentrons/hardware_control/modules/mod_abc.py +++ b/api/src/opentrons/hardware_control/modules/mod_abc.py @@ -31,7 +31,7 @@ def parse_fw_version(version: str) -> Version: raise InvalidVersion() except InvalidVersion: device_version = parse("v0.0.0") - return cast(Version, device_version) # type: ignore + return cast(Version, device_version) class AbstractModule(abc.ABC): diff --git a/api/src/opentrons/hardware_control/modules/tempdeck.py b/api/src/opentrons/hardware_control/modules/tempdeck.py index 246aabe6c47..1e3b4bba2d5 100644 --- a/api/src/opentrons/hardware_control/modules/tempdeck.py +++ b/api/src/opentrons/hardware_control/modules/tempdeck.py @@ -2,7 +2,7 @@ import asyncio import logging -from typing import Mapping, Optional +from typing import Dict, Optional from opentrons.hardware_control.modules.types import ( ModuleDisconnectedCallback, @@ -29,7 +29,7 @@ class TempDeck(mod_abc.AbstractModule): """Hardware control interface for an attached Temperature Module.""" - MODULE_TYPE = types.ModuleType.TEMPERATURE + MODULE_TYPE = types.ModuleType["TEMPERATURE"] FIRST_GEN2_REVISION = 20 @classmethod @@ -100,7 +100,7 @@ def __init__( driver: AbstractTempDeckDriver, reader: TempDeckReader, poller: Poller, - device_info: Mapping[str, str], + device_info: Dict[str, str], hw_control_loop: asyncio.AbstractEventLoop, disconnected_callback: ModuleDisconnectedCallback = None, ) -> None: @@ -190,7 +190,7 @@ async def deactivate(self, must_be_running: bool = True) -> None: await self._reader.read() @property - def device_info(self) -> Mapping[str, str]: + def device_info(self) -> Dict[str, str]: return self._device_info @property diff --git a/api/src/opentrons/protocol_engine/commands/absorbance_reader/close_lid.py b/api/src/opentrons/protocol_engine/commands/absorbance_reader/close_lid.py index 31c51676e7d..4b1135668d0 100644 --- a/api/src/opentrons/protocol_engine/commands/absorbance_reader/close_lid.py +++ b/api/src/opentrons/protocol_engine/commands/absorbance_reader/close_lid.py @@ -16,7 +16,7 @@ from opentrons.drivers.types import AbsorbanceReaderLidStatus if TYPE_CHECKING: - from opentrons.protocol_engine.state import StateView + from opentrons.protocol_engine.state.state import StateView from opentrons.protocol_engine.execution import ( EquipmentHandler, LabwareMovementHandler, diff --git a/api/src/opentrons/protocol_engine/commands/absorbance_reader/initialize.py b/api/src/opentrons/protocol_engine/commands/absorbance_reader/initialize.py index c8a9018a0df..e194ccce64e 100644 --- a/api/src/opentrons/protocol_engine/commands/absorbance_reader/initialize.py +++ b/api/src/opentrons/protocol_engine/commands/absorbance_reader/initialize.py @@ -12,7 +12,7 @@ from ...errors.error_occurrence import ErrorOccurrence if TYPE_CHECKING: - from opentrons.protocol_engine.state import StateView + from opentrons.protocol_engine.state.state import StateView from opentrons.protocol_engine.execution import EquipmentHandler diff --git a/api/src/opentrons/protocol_engine/commands/absorbance_reader/open_lid.py b/api/src/opentrons/protocol_engine/commands/absorbance_reader/open_lid.py index f12a612f649..e6da9edade5 100644 --- a/api/src/opentrons/protocol_engine/commands/absorbance_reader/open_lid.py +++ b/api/src/opentrons/protocol_engine/commands/absorbance_reader/open_lid.py @@ -16,7 +16,7 @@ from opentrons.drivers.types import AbsorbanceReaderLidStatus if TYPE_CHECKING: - from opentrons.protocol_engine.state import StateView + from opentrons.protocol_engine.state.state import StateView from opentrons.protocol_engine.execution import ( EquipmentHandler, LabwareMovementHandler, diff --git a/api/src/opentrons/protocol_engine/commands/absorbance_reader/read.py b/api/src/opentrons/protocol_engine/commands/absorbance_reader/read.py index 8fc65734c0f..6466316e3a0 100644 --- a/api/src/opentrons/protocol_engine/commands/absorbance_reader/read.py +++ b/api/src/opentrons/protocol_engine/commands/absorbance_reader/read.py @@ -10,7 +10,7 @@ from ...errors.error_occurrence import ErrorOccurrence if TYPE_CHECKING: - from opentrons.protocol_engine.state import StateView + from opentrons.protocol_engine.state.state import StateView from opentrons.protocol_engine.execution import EquipmentHandler diff --git a/api/tests/opentrons/drivers/absorbance_reader/test_driver.py b/api/tests/opentrons/drivers/absorbance_reader/test_driver.py index a9a754b1176..5fe71d2763c 100644 --- a/api/tests/opentrons/drivers/absorbance_reader/test_driver.py +++ b/api/tests/opentrons/drivers/absorbance_reader/test_driver.py @@ -29,7 +29,9 @@ class MockErrorCode(Enum): @pytest.fixture -async def mock_async_byonoy(mock_interface, mock_device) -> AsyncByonoy: +async def mock_async_byonoy( + mock_interface: MagicMock, mock_device: MagicMock +) -> AsyncByonoy: loop = asyncio.get_running_loop() return AsyncByonoy( mock_interface, mock_device, ThreadPoolExecutor(max_workers=1), loop diff --git a/robot-server/robot_server/modules/module_data_mapper.py b/robot-server/robot_server/modules/module_data_mapper.py index 3b3193e9c69..ef09b423f58 100644 --- a/robot-server/robot_server/modules/module_data_mapper.py +++ b/robot-server/robot_server/modules/module_data_mapper.py @@ -1,5 +1,5 @@ """Module identification and response data mapping.""" -from typing import Annotated, List, Type, cast, Optional +from typing import List, Type, cast, Optional from fastapi import Depends from opentrons_shared_data.module import load_definition