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(hardware-testing, api): add module calibration HTTP script and supporting changes #12415

Merged
merged 5 commits into from
Apr 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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)