Skip to content

Commit

Permalink
merge in edge
Browse files Browse the repository at this point in the history
  • Loading branch information
andySigler committed Sep 20, 2023
2 parents 0859d29 + 1f3cb50 commit 4e2aba1
Show file tree
Hide file tree
Showing 78 changed files with 1,333 additions and 567 deletions.
10 changes: 10 additions & 0 deletions api-client/src/instruments/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@ export type InstrumentData = PipetteData | GripperData | BadPipette | BadGripper
// pipettes module already exports type `Mount`
type Mount = 'left' | 'right' | 'extension'

export interface InconsistentCalibrationFailure {
kind: 'inconsistent-pipette-offset'
offsets: Map<'left' | 'right', { x: number; y: number; z: number }>
limit: number
}

export type CalibrationReasonabilityCheckFailure = InconsistentCalibrationFailure

export interface SharedInstrumentData {
mount: Mount
}
Expand All @@ -13,6 +21,7 @@ export interface GripperData {
offset: { x: number; y: number; z: number }
source: string
last_modified?: string
reasonability_check_failures?: null[]
}
}
firmwareVersion?: string
Expand All @@ -32,6 +41,7 @@ export interface PipetteData {
offset: { x: number; y: number; z: number }
source: string
last_modified?: string
reasonability_check_failures?: CalibrationReasonabilityCheckFailure[]
}
}
firmwareVersion?: string
Expand Down
9 changes: 9 additions & 0 deletions api-client/src/robot/getDoorStatus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { GET, request } from '../request'

import type { ResponsePromise } from '../request'
import type { HostConfig } from '../types'
import type { DoorStatus } from './types'

export function getDoorStatus(config: HostConfig): ResponsePromise<DoorStatus> {
return request<DoorStatus>(GET, '/robot/door/status', null, config)
}
2 changes: 2 additions & 0 deletions api-client/src/robot/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
export { getDoorStatus } from './getDoorStatus'
export { getEstopStatus } from './getEstopStatus'
export { acknowledgeEstopDisengage } from './acknowledgeEstopDisengage'
export { getLights } from './getLights'
export { setLights } from './setLights'
export type {
DoorStatus,
EstopPhysicalStatus,
EstopState,
EstopStatus,
Expand Down
6 changes: 6 additions & 0 deletions api-client/src/robot/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
export interface DoorStatus {
data: {
status: 'open' | 'closed'
doorRequiredClosedForProtocol: boolean
}
}
export type EstopState =
| 'physicallyEngaged'
| 'logicallyEngaged'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import typing
from dataclasses import dataclass
from typing_extensions import Literal, Final
from dataclasses import dataclass, field
from datetime import datetime

from opentrons.config import feature_flags as ff
Expand All @@ -20,6 +21,18 @@
)
from opentrons.hardware_control.types import OT3Mount

PIPETTE_OFFSET_CONSISTENCY_LIMIT: Final = 1.5


@dataclass
class InconsistentPipetteOffsets:
kind: Literal["inconsistent-pipette-offset"]
offsets: typing.Dict[Mount, Point]
limit: float


ReasonabilityCheckFailure = typing.Union[InconsistentPipetteOffsets]


@dataclass
class PipetteOffsetByPipetteMount:
Expand All @@ -33,6 +46,13 @@ class PipetteOffsetByPipetteMount:
last_modified: typing.Optional[datetime] = None


@dataclass
class PipetteOffsetSummary(PipetteOffsetByPipetteMount):
reasonability_check_failures: typing.List[ReasonabilityCheckFailure] = field(
default_factory=list
)


@dataclass
class GripperCalibrationOffset:
"""
Expand Down Expand Up @@ -122,3 +142,25 @@ def save_gripper_calibration_offset(
) -> None:
if gripper_id and ff.enable_ot3_hardware_controller():
ot3_gripper_offset.save_gripper_calibration(delta, gripper_id)


def check_instrument_offset_reasonability(
left_offset: Point, right_offset: Point
) -> typing.List[ReasonabilityCheckFailure]:
if (
not left_offset
or left_offset == Point(0, 0, 0)
or not right_offset
or right_offset == Point(0, 0, 0)
):
return []
diff = left_offset - right_offset
if any(abs(d) > PIPETTE_OFFSET_CONSISTENCY_LIMIT for d in diff):
return [
InconsistentPipetteOffsets(
"inconsistent-pipette-offset",
{Mount.LEFT: left_offset, Mount.RIGHT: right_offset},
PIPETTE_OFFSET_CONSISTENCY_LIMIT,
)
]
return []
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,11 @@

from opentrons.hardware_control.dev_types import PipetteDict
from .pipette import Pipette
from .instrument_calibration import PipetteOffsetByPipetteMount
from .instrument_calibration import (
PipetteOffsetSummary,
PipetteOffsetByPipetteMount,
check_instrument_offset_reasonability,
)


MOD_LOG = logging.getLogger(__name__)
Expand Down Expand Up @@ -159,16 +163,16 @@ def _reset(m: OT3Mount) -> None:
else:
_reset(mount)

def get_instrument_offset(
self, mount: OT3Mount
) -> Optional[PipetteOffsetByPipetteMount]:
def get_instrument_offset(self, mount: OT3Mount) -> Optional[PipetteOffsetSummary]:
"""Get the specified pipette's offset."""
assert mount != OT3Mount.GRIPPER, "Wrong mount type to fetch pipette offset"
try:
pipette = self.get_pipette(mount)
except top_types.PipetteNotAttachedError:
return None
return pipette.pipette_offset
return self._return_augmented_offset_data(
pipette, mount, pipette.pipette_offset
)

def reset_instrument_offset(self, mount: OT3Mount, to_default: bool) -> None:
"""
Expand All @@ -180,14 +184,47 @@ def reset_instrument_offset(self, mount: OT3Mount, to_default: bool) -> None:

def save_instrument_offset(
self, mount: OT3Mount, delta: top_types.Point
) -> PipetteOffsetByPipetteMount:
) -> PipetteOffsetSummary:
"""
Save a new instrument offset the pipette offset to a particular value.
:param mount: Modify the given mount.
:param delta: The offset to set for the pipette.
"""
pipette = self.get_pipette(mount)
return pipette.save_pipette_offset(mount, delta)
offset_data = pipette.save_pipette_offset(mount, delta)
return self._return_augmented_offset_data(pipette, mount, offset_data)

def _return_augmented_offset_data(
self,
pipette: Pipette,
mount: OT3Mount,
offset_data: PipetteOffsetByPipetteMount,
) -> PipetteOffsetSummary:
if mount == OT3Mount.LEFT:
other_pipette = self._attached_instruments.get(OT3Mount.RIGHT, None)
if other_pipette:
other_offset = other_pipette.pipette_offset.offset
else:
other_offset = top_types.Point(0, 0, 0)
reasonability = check_instrument_offset_reasonability(
offset_data.offset, other_offset
)
else:
other_pipette = self._attached_instruments.get(OT3Mount.LEFT, None)
if other_pipette:
other_offset = other_pipette.pipette_offset.offset
else:
other_offset = top_types.Point(0, 0, 0)
reasonability = check_instrument_offset_reasonability(
other_offset, offset_data.offset
)
return PipetteOffsetSummary(
offset=offset_data.offset,
source=offset_data.source,
status=offset_data.status,
last_modified=offset_data.last_modified,
reasonability_check_failures=reasonability,
)

# TODO(mc, 2022-01-11): change returned map value type to `Optional[PipetteDict]`
# instead of potentially returning an empty dict
Expand Down
6 changes: 3 additions & 3 deletions api/src/opentrons/hardware_control/ot3api.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@
from .instruments.ot3.gripper import compare_gripper_config_and_check_skip, Gripper
from .instruments.ot3.instrument_calibration import (
GripperCalibrationOffset,
PipetteOffsetByPipetteMount,
PipetteOffsetSummary,
)
from .backends.ot3controller import OT3Controller
from .backends.ot3simulator import OT3Simulator
Expand Down Expand Up @@ -2115,7 +2115,7 @@ def reset_instrument(

def get_instrument_offset(
self, mount: OT3Mount
) -> Union[GripperCalibrationOffset, PipetteOffsetByPipetteMount, None]:
) -> Union[GripperCalibrationOffset, PipetteOffsetSummary, None]:
"""Get instrument calibration data."""
# TODO (spp, 2023-04-19): We haven't introduced a 'calibration_offset' key in
# PipetteDict because the dict is shared with OT2 pipettes which have
Expand All @@ -2141,7 +2141,7 @@ async def reset_instrument_offset(

async def save_instrument_offset(
self, mount: Union[top_types.Mount, OT3Mount], delta: top_types.Point
) -> Union[GripperCalibrationOffset, PipetteOffsetByPipetteMount]:
) -> Union[GripperCalibrationOffset, PipetteOffsetSummary]:
"""Save a new offset for a given instrument."""
checked_mount = OT3Mount.from_mount(mount)
if checked_mount == OT3Mount.GRIPPER:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,13 @@
)


from opentrons import calibration_storage
from opentrons import calibration_storage, types as top_types
from opentrons.calibration_storage import helpers as calibration_storage_helpers
from opentrons.calibration_storage.ot2.models import v1 as v1_models
from opentrons.hardware_control.instruments.ot2 import instrument_calibration as subject
from opentrons.hardware_control.instruments.ot3 import (
instrument_calibration as subject_ot3,
)


@pytest.fixture(autouse=True)
Expand Down Expand Up @@ -110,3 +113,35 @@ def test_load_tip_length(
markedAt=datetime(year=2023, month=2, day=2),
),
)


@pytest.mark.parametrize(
"left,right,ok",
[
# If either point is all 0 (uncalibrated) then the check should pass
(top_types.Point(0, 0, 0), top_types.Point(0, 0, 2), True),
(top_types.Point(0, 0, 2), top_types.Point(0, 0, 0), True),
(top_types.Point(0, 0, 0), top_types.Point(0, 0, 0), True),
# If both points are non-zero but all values are within the range the
# check should pass
(top_types.Point(0, 1.0, 1.5), top_types.Point(-1, 0, 0.2), True),
# If both points are non-zero but at least one element is more than
# the range different the test should fail
(top_types.Point(0.1, -1, 1.5), top_types.Point(1.7, 0, 0.2), False),
(top_types.Point(0.1, -1, 1.5), top_types.Point(0.6, 0.6, 1.3), False),
(top_types.Point(0.1, -1, 1.5), top_types.Point(-0.2, -0.1, 5), False),
],
)
def test_instrument_consistency_check_ot3(
left: top_types.Point, right: top_types.Point, ok: bool
) -> None:
result = subject_ot3.check_instrument_offset_reasonability(left, right)
if ok:
assert result == []
else:
assert result[0].kind == "inconsistent-pipette-offset"
assert result[0].offsets == {
top_types.Mount.LEFT: left,
top_types.Mount.RIGHT: right,
}
assert result[0].limit == 1.5
50 changes: 48 additions & 2 deletions api/tests/opentrons/hardware_control/test_pipette_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
import pytest

from decoy import Decoy
from typing import Optional
from typing import Optional, Tuple, Dict

from opentrons import types
from opentrons.hardware_control.types import Axis
from opentrons.hardware_control.types import Axis, OT3Mount
from opentrons.hardware_control.instruments.ot2.pipette import Pipette
from opentrons.hardware_control.instruments.ot2.pipette_handler import (
PipetteHandlerProvider,
Expand All @@ -27,6 +27,11 @@ def mock_pipette_ot3(decoy: Decoy) -> OT3Pipette:
return decoy.mock(cls=OT3Pipette)


@pytest.fixture
def mock_pipettes_ot3(decoy: Decoy) -> Tuple[OT3Pipette, OT3Pipette]:
return (decoy.mock(cls=OT3Pipette), decoy.mock(cls=OT3Pipette))


@pytest.fixture
def subject(decoy: Decoy, mock_pipette: Pipette) -> PipetteHandlerProvider:
inst_by_mount = {types.Mount.LEFT: mock_pipette, types.Mount.RIGHT: None}
Expand Down Expand Up @@ -134,3 +139,44 @@ def test_plan_check_pick_up_tip_with_presses_argument_ot3(
def test_get_pipette_fails(decoy: Decoy, subject: PipetteHandlerProvider):
with pytest.raises(types.PipetteNotAttachedError):
subject.get_pipette(types.Mount.RIGHT)


@pytest.mark.parametrize(
"left_offset,right_offset,ok",
[
(types.Point(100, 200, 300), None, True),
(None, types.Point(-100, 200, -500), True),
(types.Point(100, 200, 300), types.Point(200, 400, 500), False),
],
)
def test_ot3_pipette_handler_gives_checks_with_different_pipettes(
left_offset: Optional[types.Point],
right_offset: Optional[types.Point],
ok: bool,
mock_pipettes_ot3: Tuple[OT3Pipette],
decoy: Decoy,
) -> None:
"""Should give you reasonable results with one or two pipettes attached."""
# with a left and not right pipette, we should be able to pass our checks
inst_by_mount: Dict[OT3Mount, OT3Pipette] = {}
if left_offset is not None:
inst_by_mount[OT3Mount.LEFT] = mock_pipettes_ot3[0]
decoy.when(mock_pipettes_ot3[0].pipette_offset.offset).then_return(left_offset)
if right_offset is not None:
inst_by_mount[OT3Mount.RIGHT] = mock_pipettes_ot3[1]
decoy.when(mock_pipettes_ot3[1].pipette_offset.offset).then_return(right_offset)
subject = OT3PipetteHandler(attached_instruments=inst_by_mount)
if left_offset is not None:
left_result = subject.get_instrument_offset(OT3Mount.LEFT)
assert left_result.offset == left_offset
if ok:
assert left_result.reasonability_check_failures == []
else:
assert len(left_result.reasonability_check_failures) == 1
if right_offset is not None:
right_result = subject.get_instrument_offset(OT3Mount.RIGHT)
assert right_result.offset == right_offset
if ok:
assert right_result.reasonability_check_failures == []
else:
assert len(right_result.reasonability_check_failures) == 1
2 changes: 1 addition & 1 deletion app/src/assets/localization/en/device_details.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
"detach_gripper": "Detach gripper",
"detach_pipette": "Detach pipette",
"disengaged": "Disengaged",
"download_run_log": "Download run log",
"download_run_log": "Download protocol run log",
"empty": "Empty",
"error_details": "error details",
"estop_disconnected": "E-stop disconnected. Robot movement is halted.",
Expand Down
4 changes: 2 additions & 2 deletions app/src/assets/localization/en/device_settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@
"do_not_turn_off": "Do not turn off the robot while updating",
"done": "Done",
"download": "Download",
"download_calibration_data": "Download calibration data",
"download_calibration_data": "Download calibration logs",
"download_error": "Download error",
"download_logs": "Download logs",
"downloading_logs": "Downloading logs...",
Expand Down Expand Up @@ -193,7 +193,7 @@
"password_error_message": "Must be at least 8 characters",
"pause_protocol": "Pause protocol when robot door opens",
"pause_protocol_description": "When enabled, opening the robot door during a run will pause the robot after it has completed its current motion.",
"pipette_calibrations_description": "Pipette calibration uses a metal probe to determine the pipette's exact position relative to precision-cut divots on deck slots.",
"pipette_calibrations_description": "Pipette calibration uses a metal probe to determine the pipette's exact position relative to precision-cut squares on deck slots.",
"pipette_calibrations_title": "Pipette Calibrations",
"pipette_offset_calibration": "pipette offset calibration",
"pipette_offset_calibration_missing": "Pipette Offset calibration missing",
Expand Down
Loading

0 comments on commit 4e2aba1

Please sign in to comment.