Skip to content

Commit

Permalink
feat(api,robot-server): expose module calibration offsets via /module…
Browse files Browse the repository at this point in the history
…s endpoint. (#12860)
  • Loading branch information
vegano1 authored Jun 9, 2023
1 parent 9f28e7f commit 56897a1
Show file tree
Hide file tree
Showing 10 changed files with 137 additions and 9 deletions.
2 changes: 2 additions & 0 deletions api/src/opentrons/calibration_storage/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
clear_module_offset_calibrations,
get_module_offset,
delete_module_offset_file,
load_all_module_offsets,
)
else:
from .ot2.pipette_offset import (
Expand Down Expand Up @@ -87,6 +88,7 @@
"clear_module_offset_calibrations",
"get_module_offset",
"delete_module_offset_file",
"load_all_module_offsets",
# functions only used in robot server
"_save_custom_tiprack_definition",
"get_custom_tiprack_definition_for_tlc",
Expand Down
12 changes: 12 additions & 0 deletions api/src/opentrons/calibration_storage/ot3/module_offset.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import json
import logging
import os
from functools import lru_cache
from pathlib import Path
from opentrons.hardware_control.modules.types import ModuleType
from opentrons.hardware_control.types import OT3Mount
Expand Down Expand Up @@ -30,6 +31,9 @@ def delete_module_offset_file(module_id: str) -> None:
offset_dir = config.get_opentrons_path("module_calibration_dir")
offset_path = offset_dir / f"{module_id}.json"
io.delete_file(offset_path)
# something changed, clear lru cache
get_module_offset.cache_clear()
load_all_module_offsets.cache_clear()


def clear_module_offset_calibrations() -> None:
Expand All @@ -39,6 +43,9 @@ def clear_module_offset_calibrations() -> None:

offset_dir = config.get_opentrons_path("module_calibration_dir")
io._remove_json_files_in_directories(offset_dir)
# something changed, clear lru cache
get_module_offset.cache_clear()
load_all_module_offsets.cache_clear()


# Save Module Offset Calibrations
Expand Down Expand Up @@ -76,12 +83,16 @@ def save_module_calibration(
status=cal_status_model,
)
io.save_to_file(module_dir, module_id, module_calibration)
# something changed, clear lru cache
get_module_offset.cache_clear()
load_all_module_offsets.cache_clear()


# Get Module Offset Calibrations


@no_type_check
@lru_cache(maxsize=10)
def get_module_offset(
module: ModuleType, module_id: str, slot: Optional[int] = None
) -> Optional[v1.ModuleOffsetModel]:
Expand All @@ -102,6 +113,7 @@ def get_module_offset(
return None


@lru_cache(maxsize=10)
def load_all_module_offsets() -> List[v1.ModuleOffsetModel]:
"""Load all module offsets from the disk."""

Expand Down
2 changes: 1 addition & 1 deletion api/src/opentrons/hardware_control/module_control.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ def get_module_by_module_id(
return found_module

def load_module_offset(
self, module_type: ModuleType, module_id: str, slot: int
self, module_type: ModuleType, module_id: str, slot: Optional[int] = None
) -> ModuleCalibrationOffset:
log.info(f"Loading module offset for {module_type} {module_id}")
return load_module_calibration_offset(module_type, module_id, slot)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,12 @@
class ModuleCalibrationOffset:
"""Class to store module offset calibration data."""

slot: int
offset: Point
module_id: str
module: ModuleType
source: SourceType
status: CalibrationStatus
slot: Optional[int] = None
mount: Optional[OT3Mount] = None
instrument_id: Optional[str] = None
last_modified: Optional[datetime] = None
Expand All @@ -35,7 +35,7 @@ class ModuleCalibrationOffset:
def load_module_calibration_offset(
module_type: ModuleType,
module_id: str,
slot: int,
slot: Optional[int] = None,
) -> ModuleCalibrationOffset:
"""Loads the calibration offset for a module."""
# load default if module offset data do not exist
Expand All @@ -51,9 +51,9 @@ def load_module_calibration_offset(
module_offset_data = get_module_offset(module_type, module_id)
if module_offset_data:
return ModuleCalibrationOffset(
slot=slot,
module=module_type,
module_id=module_id,
slot=module_offset_data.slot,
mount=module_offset_data.mount,
offset=module_offset_data.offset,
last_modified=module_offset_data.lastModified,
Expand Down
15 changes: 15 additions & 0 deletions api/src/opentrons/hardware_control/ot3api.py
Original file line number Diff line number Diff line change
Expand Up @@ -1823,6 +1823,21 @@ async def save_module_offset(
module_type, module_id, mount, slot, offset, instrument_id
)

def get_module_calibration_offset(
self, serial_number: str
) -> Optional[ModuleCalibrationOffset]:
"""Get the module calibration offset of a module."""
module = self._backend.module_controls.get_module_by_module_id(serial_number)
if not module:
self._log.warning(
f"Could not load calibration: unknown module {serial_number}"
)
return None
module_type = module.MODULE_TYPE
return self._backend.module_controls.load_module_offset(
module_type, serial_number
)

def get_attached_pipette(
self, mount: Union[top_types.Mount, OT3Mount]
) -> PipetteDict:
Expand Down
5 changes: 4 additions & 1 deletion robot-server/robot_server/modules/module_data_mapper.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""Module identification and response data mapping."""
from typing import Type, cast
from typing import Type, cast, Optional

from opentrons.hardware_control.modules import (
LiveData,
Expand All @@ -24,6 +24,7 @@
AttachedModuleData,
MagneticModule,
MagneticModuleData,
ModuleCalibrationData,
TemperatureModule,
TemperatureModuleData,
ThermocyclerModule,
Expand All @@ -44,6 +45,7 @@ def map_data(
has_available_update: bool,
live_data: LiveData,
usb_port: HardwareUSBPort,
module_offset: Optional[ModuleCalibrationData],
) -> AttachedModule:
"""Map hardware control data to an attached module response."""
module_model = ModuleModel(model)
Expand Down Expand Up @@ -138,4 +140,5 @@ def map_data(
moduleType=module_type, # type: ignore[arg-type]
moduleModel=module_model, # type: ignore[arg-type]
data=module_data, # type: ignore[arg-type]
moduleOffset=module_offset,
)
15 changes: 15 additions & 0 deletions robot-server/robot_server/modules/module_models.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
"""Request and response models for /modules endpoints."""
from datetime import datetime
from pydantic import BaseModel, Field
from pydantic.generics import GenericModel
from typing import Generic, Optional, TypeVar, Union
from typing_extensions import Literal

from opentrons.calibration_storage.types import SourceType
from opentrons.hardware_control.modules import (
ModuleType,
TemperatureStatus,
Expand All @@ -16,12 +18,22 @@
HeaterShakerLabwareLatchStatus,
)
from opentrons.protocol_engine import ModuleModel
from opentrons.protocol_engine.types import Vec3f

ModuleT = TypeVar("ModuleT", bound=ModuleType)
ModuleModelT = TypeVar("ModuleModelT", bound=ModuleModel)
ModuleDataT = TypeVar("ModuleDataT", bound=BaseModel)


class ModuleCalibrationData(BaseModel):
"""A module's calibration data."""

offset: Vec3f
slot: Optional[int] = None
source: Optional[SourceType] = None
last_modified: Optional[datetime] = None


class UsbPort(BaseModel):
"""The USB port the module is connected to."""

Expand Down Expand Up @@ -65,6 +77,9 @@ class _GenericModule(GenericModel, Generic[ModuleT, ModuleModelT, ModuleDataT]):
)
moduleType: ModuleT = Field(..., description="General type of the module.")
moduleModel: ModuleModelT = Field(..., description="Specific model of the module.")
moduleOffset: Optional[ModuleCalibrationData] = Field(
None, description="The calibrated module offset."
)
data: ModuleDataT
usbPort: UsbPort

Expand Down
26 changes: 24 additions & 2 deletions robot-server/robot_server/modules/router.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
"""Modules routes."""
from fastapi import APIRouter, Depends, status
from typing import List
from typing import List, Dict

from opentrons.hardware_control import HardwareControlAPI
from opentrons.hardware_control.modules import module_calibration
from opentrons.protocol_engine.types import Vec3f

from robot_server.hardware import get_hardware
from robot_server.versioning import get_requested_version
Expand All @@ -15,7 +17,7 @@
PydanticResponse,
)

from .module_models import AttachedModule
from .module_models import AttachedModule, ModuleCalibrationData
from .module_identifier import ModuleIdentifier
from .module_data_mapper import ModuleDataMapper

Expand All @@ -42,16 +44,36 @@ async def get_attached_modules(
hardware=hardware,
)

# Load any the module calibrations
module_calibrations: Dict[str, module_calibration.ModuleCalibrationOffset] = {
mod.module_id: mod for mod in module_calibration.load_all_module_calibrations()
}

response_data: List[AttachedModule] = []
for mod in hardware.attached_modules:
serial_number = mod.device_info["serial"]
calibrated = module_calibrations.get(serial_number)
module_identity = module_identifier.identify(mod.device_info)

response_data.append(
module_data_mapper.map_data(
model=mod.model(),
has_available_update=mod.has_available_update(),
module_identity=module_identity,
live_data=mod.live_data,
usb_port=mod.usb_port,
module_offset=ModuleCalibrationData.construct(
offset=Vec3f(
x=calibrated.offset.x,
y=calibrated.offset.y,
z=calibrated.offset.z,
),
slot=calibrated.slot,
source=calibrated.status.source,
last_modified=calibrated.last_modified,
)
if calibrated
else None,
)
)

Expand Down
18 changes: 18 additions & 0 deletions robot-server/tests/modules/test_module_data_mapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import pytest

from opentrons.protocol_engine import ModuleModel
from opentrons.protocol_engine.types import Vec3f
from opentrons.drivers.rpi_drivers.types import USBPort as HardwareUSBPort
from opentrons.hardware_control.modules import (
LiveData,
Expand All @@ -25,6 +26,7 @@
ThermocyclerModuleData,
HeaterShakerModule,
HeaterShakerModuleData,
ModuleCalibrationData,
)


Expand Down Expand Up @@ -96,6 +98,9 @@ def test_maps_magnetic_module_data(
has_available_update=True,
live_data=input_data,
usb_port=hardware_usb_port,
module_offset=ModuleCalibrationData.construct(
offset=Vec3f(x=0, y=0, z=0),
),
)

assert result == MagneticModule(
Expand All @@ -108,6 +113,7 @@ def test_maps_magnetic_module_data(
moduleModel=ModuleModel(input_model), # type: ignore[arg-type]
usbPort=UsbPort(port=101, hub=202, path="/dev/null"),
data=expected_output_data,
moduleOffset=ModuleCalibrationData(offset=Vec3f(x=0.0, y=0.0, z=0.0)),
)


Expand Down Expand Up @@ -148,6 +154,9 @@ def test_maps_temperature_module_data(input_model: str, input_data: LiveData) ->
has_available_update=True,
live_data=input_data,
usb_port=hardware_usb_port,
module_offset=ModuleCalibrationData.construct(
offset=Vec3f(x=0, y=0, z=0),
),
)

assert result == TemperatureModule(
Expand All @@ -159,6 +168,7 @@ def test_maps_temperature_module_data(input_model: str, input_data: LiveData) ->
moduleType=ModuleType.TEMPERATURE,
moduleModel=ModuleModel(input_model), # type: ignore[arg-type]
usbPort=UsbPort(port=101, hub=202, path="/dev/null"),
moduleOffset=ModuleCalibrationData(offset=Vec3f(x=0.0, y=0.0, z=0.0)),
data=TemperatureModuleData(
status=TemperatureStatus(input_data["status"]),
currentTemperature=input_data["data"]["currentTemp"], # type: ignore[arg-type]
Expand Down Expand Up @@ -233,6 +243,9 @@ def test_maps_thermocycler_module_data(input_model: str, input_data: LiveData) -
has_available_update=True,
live_data=input_data,
usb_port=hardware_usb_port,
module_offset=ModuleCalibrationData.construct(
offset=Vec3f(x=0, y=0, z=0),
),
)

assert result == ThermocyclerModule(
Expand All @@ -244,6 +257,7 @@ def test_maps_thermocycler_module_data(input_model: str, input_data: LiveData) -
moduleType=ModuleType.THERMOCYCLER,
moduleModel=ModuleModel(input_model), # type: ignore[arg-type]
usbPort=UsbPort(port=101, hub=202, path="/dev/null"),
moduleOffset=ModuleCalibrationData(offset=Vec3f(x=0.0, y=0.0, z=0.0)),
data=ThermocyclerModuleData(
status=TemperatureStatus(input_data["status"]),
currentTemperature=input_data["data"]["currentTemp"], # type: ignore[arg-type]
Expand Down Expand Up @@ -320,6 +334,9 @@ def test_maps_heater_shaker_module_data(input_model: str, input_data: LiveData)
has_available_update=True,
live_data=input_data,
usb_port=hardware_usb_port,
module_offset=ModuleCalibrationData.construct(
offset=Vec3f(x=0, y=0, z=0),
),
)

assert result == HeaterShakerModule(
Expand All @@ -331,6 +348,7 @@ def test_maps_heater_shaker_module_data(input_model: str, input_data: LiveData)
moduleType=ModuleType.HEATER_SHAKER,
moduleModel=ModuleModel(input_model), # type: ignore[arg-type]
usbPort=UsbPort(port=101, hub=202, path="/dev/null"),
moduleOffset=ModuleCalibrationData(offset=Vec3f(x=0.0, y=0.0, z=0.0)),
data=HeaterShakerModuleData(
status=HeaterShakerStatus(input_data["status"]),
labwareLatchStatus=input_data["data"]["labwareLatchStatus"], # type: ignore[arg-type]
Expand Down
Loading

0 comments on commit 56897a1

Please sign in to comment.