Skip to content

Commit

Permalink
feat(hardware-testing, api): add module calibration HTTP script and s…
Browse files Browse the repository at this point in the history
…upporting changes (#12415)

* keep using well B1 for the nominal position of the module calibration point
* fix an issue where we are applying double Z_PREP_OFFSET by just subtracting Z_PREP_OFFSET from the nominal position
* fix heater_shaker calibration adapter definitions
* add requests and types-requests to Pipfile and Pipfile.lock
  • Loading branch information
vegano1 authored Apr 10, 2023
1 parent c0f63bd commit eb62844
Show file tree
Hide file tree
Showing 6 changed files with 177 additions and 3 deletions.
9 changes: 7 additions & 2 deletions api/src/opentrons/hardware_control/ot3_calibration.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ def _deck_hit(
to determine whether or not it had hit the deck.
"""
if found_pos > expected_pos + settings.early_sense_tolerance_mm:
raise EarlyCapacitiveSenseTrigger(expected_pos, expected_pos)
raise EarlyCapacitiveSenseTrigger(found_pos, expected_pos)
return (
True if found_pos >= (expected_pos - settings.overrun_tolerance_mm) else False
)
Expand Down Expand Up @@ -762,7 +762,12 @@ async def calibrate_module(
LOG.info(
f"Starting module calibration for {module_id} at {nominal_position} using {mount}"
)
# find the offset
# FIXME (ba, 2023-04-04): Well B1 of the module adapter definition includes the z prep offset
# of 13x13mm in the nominial position, but we are still using PREP_OFFSET_DEPTH in
# find_calibration_structure_height which effectively doubles the offset. We plan
# on removing PREP_OFFSET_DEPTH in the near future, but for now just subtract PREP_OFFSET_DEPTH
# from the nominal position so we dont have to alter any other part of the system.
nominal_position = nominal_position - PREP_OFFSET_DEPTH
offset = await find_calibration_structure_position(
hcapi, mount, nominal_position, method=CalibrationMethod.BINARY_SEARCH
)
Expand Down
5 changes: 4 additions & 1 deletion api/src/opentrons/hardware_control/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,10 @@ class OT3Mount(enum.Enum):

@classmethod
def from_mount(
cls, mount: Union[top_types.Mount, top_types.MountType, "OT3Mount"]
cls,
mount: Union[
top_types.Mount, top_types.MountType, top_types.OT3MountType, "OT3Mount"
],
) -> "OT3Mount":
return cls[mount.name]

Expand Down
6 changes: 6 additions & 0 deletions api/src/opentrons/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,12 @@ def to_hw_mount(self) -> Mount:
return Mount.LEFT if self is MountType.LEFT else Mount.RIGHT


class OT3MountType(str, enum.Enum):
LEFT = "left"
RIGHT = "right"
GRIPPER = "gripper"


# TODO(mc, 2020-11-09): this makes sense in shared-data or other common
# model library
# https://github.com/Opentrons/opentrons/pull/6943#discussion_r519029833
Expand Down
2 changes: 2 additions & 0 deletions hardware-testing/Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ flake8 = "~=3.9.0"
flake8-annotations = "~=2.6.2"
flake8-docstrings = "~=1.6.0"
flake8-noqa = "~=1.2.1"
requests = "==2.26.0"
types-requests = "==2.25.6"

[requires]
python_version = "3.7"
16 changes: 16 additions & 0 deletions hardware-testing/Pipfile.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

142 changes: 142 additions & 0 deletions hardware-testing/hardware_testing/scripts/module_calibration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
"""OT-3 Module Calibration Script."""
import argparse
from traceback import print_exc
import requests


MOUNTS = ["left", "right", "extension"]


MODELS = [
"temperatureModuleV1",
"temperatureModuleV2",
"magneticModuleV1",
"magneticModuleV2",
"thermocyclerModuleV1",
"thermocyclerModuleV2",
"heaterShakerModuleV1",
]


CALIBRATION_ADAPTER = {
"temperatureModuleV1": "opentrons_calibration_adapter_temperature_module",
"temperatureModuleV2": "opentrons_calibration_adapter_temperature_module",
"magneticModuleV1": "opentrons_calibration_adapter_magnetic_module",
"magneticModuleV2": "opentrons_calibration_adapter_magnetic_module",
"thermocyclerModuleV1": "opentrons_calibration_adapter_thermocycler_module",
"thermocyclerModuleV2": "opentrons_calibration_adapter_thermocycler_module",
"heaterShakerModuleV1": "opentrons_calibration_adapter_heatershaker_module",
}


HEADERS = {"opentrons-version": "4"}
BASE_URL = "http://{}:31950"
PARAMS = {"waitUntilComplete": "true"}


def _home_z(ip_addr: str) -> None:
"""Home the z axis for the instrument."""
# Home the instrument axis so we are at a known state
print("Homing z axis")
home_z = {"data": {"commandType": "home", "params": {"axes": ["leftZ", "rightZ"]}}}
url = f"{BASE_URL.format(ip_addr)}/commands"
requests.post(headers=HEADERS, url=url, json=home_z, params=PARAMS)


def _main(args: argparse.Namespace) -> None:
base_url = f"{BASE_URL.format(args.host)}"

# create an empty run
res = requests.post(headers=HEADERS, url=f"{base_url}/runs")
run_id = res.json()["data"]["id"]
url = f"{base_url}/runs/{run_id}/commands"
print(f"Created run {run_id}")

# Home the instrument axis so we are at a known state
_home_z(args.host)

# load the module based on the model
print(f"Loading the module {args.model} at slot {args.slot}")
load_module = {
"data": {
"commandType": "loadModule",
"params": {"model": args.model, "location": {"slotName": args.slot}},
}
}
res = requests.post(headers=HEADERS, url=url, json=load_module, params=PARAMS)
module_id = res.json()["data"]["result"]["moduleId"]

# load the calibration labware for the specific module
print(f"Loading the calibration adapter at slot {args.slot}")
load_labware = {
"data": {
"commandType": "loadLabware",
"params": {
"location": {"moduleId": module_id},
"loadName": CALIBRATION_ADAPTER[args.model],
"namespace": "opentrons",
"version": 1,
},
}
}
res = requests.post(headers=HEADERS, url=url, json=load_labware, params=PARAMS)
labware_id = res.json()["data"]["result"]["labwareId"]

# calibrate the module
print(f"Calibrating {args.model} at slot {args.slot} with mount {args.mount}")
calibrate_module = {
"data": {
"commandType": "calibration/calibrateModule",
"params": {
"moduleId": module_id,
"labwareId": labware_id,
"mount": args.mount,
},
}
}

res = requests.post(headers=HEADERS, url=url, json=calibrate_module, params=PARAMS)
if res.status_code != 201 or not res.json()["data"].get("result"):
error = res.json()["data"]["error"]
error_type = error.get("errorType")
error_details = error.get("detail")
print(f"Failed to calibrate module {args.model} {error_type} - {error_details}")
return

calibration_offset = res.json()["data"]["result"]["moduleOffset"]
print(f"Calibration result {calibration_offset}")


if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Script to test module calibration over HTTP"
)
parser.add_argument(
"--host", help="The ip address of the robot", default="localhost"
)
parser.add_argument(
"--model",
help="The model of the module to calibrate",
choices=MODELS,
required=True,
)
parser.add_argument(
"--slot",
help="The slot on the deck the module is located in",
type=str,
required=True,
)
parser.add_argument(
"--mount",
help="The mount to use for the calibration",
choices=MOUNTS,
required=True,
)
args = parser.parse_args()
try:
_main(args)
except Exception:
print("Unhandled exception")
print_exc()
finally:
_home_z(args.host)

0 comments on commit eb62844

Please sign in to comment.