From 1556c1c442d4de3619a38a56b07d20b42b60c0fc Mon Sep 17 00:00:00 2001 From: Laura Cox <31892318+Laura-Danielle@users.noreply.github.com> Date: Mon, 26 Feb 2024 18:21:33 +0200 Subject: [PATCH] refactor(api): Save and load tip length calibrations from tiprack uri (#14512) # Overview This is an effort to fix the problem described in an escalations case [here](https://opentrons.atlassian.net/browse/RSS-468?atlOrigin=eyJpIjoiMTBmOTdlYTQ0YjU4NGRkYmFjNDkwZjY3ZmU2YmU2ZmEiLCJwIjoiamlyYS1zbGFjay1pbnQifQ). Where the tip length data does not persist during a protocol run, but does in LPC. @SyntaxColoring discovered that it seemed to be a result of the labware hash not always being the same once a dict has been converted to/from a pydantic model. We both agreed that the easiest path forward would be to lookup tip length calibrations by labware URI instead. # Note The decision was made to keep around tiprack hash for the delete endpoint until we decide to bump the version header of the API. A TODO was included in the endpoint. # Test Plan - Without modifying tip length data on a robot, ensure that behavior does not change (i.e. robot moves to all the correct places). - Modify the tip length calibration to be purposefully off (as described in the ticket) and verify that the tip length persists both in LPC _and_ the protocol run. - Delete tip length calibrations and start fresh. Ensure that behavior is normal. # Changelog - Load and save tip length calibrations by tiprack uri rather than hash. If the old format is detected, we should automatically migrate the data shape. - Updated relevant locations for the new data shape # Review requests Check out the code and make sure everything makes sense to you. # Risk assessment High. This is modifying a critical component of loading tip length calibration. --- api-client/src/calibration/types.ts | 4 +- .../calibration_storage/ot2/models/v1.py | 8 +- .../calibration_storage/ot2/tip_length.py | 78 ++++++++++++------- .../instruments/ot2/instrument_calibration.py | 9 +-- .../test_tip_length_ot2.py | 31 ++++++-- .../test_instrument_calibration.py | 5 +- ...nalysisError_ModuleInStagingAreaCol3].json | 2 +- ...00_96_GRIPPER_HS_TM_TC_MB_2_16_Smoke].json | 14 ++++ ...MB_2_16_DeckConfiguration1_NoModules].json | 2 + ...P300M_P20S_MM_TM_TC1_5_2_6_PD40Error].json | 2 +- ...nalysisError_DropLabwareIntoTrashBin].json | 1 + ...OT2_None_None_2_13_PythonSyntaxError].json | 2 +- ...P300M_P20S_TC_HS_TM_2_16_SmokeTestV3].json | 11 +++ ..._HS_TM_TC_MB_2_16_DeckConfiguration1].json | 2 + .../hooks/__fixtures__/taskListFixtures.ts | 5 ++ .../CalibrationDetails/OverflowMenu.tsx | 2 +- .../__tests__/OverflowMenu.test.tsx | 2 +- app/src/redux/calibration/api-types.ts | 2 +- .../__fixtures__/tip-length-calibration.ts | 3 + .../firmware_bindings/constants.py | 7 ++ .../messages/message_definitions.py | 70 +++++++++++++++++ .../firmware_bindings/messages/messages.py | 6 ++ .../firmware_bindings/messages/payloads.py | 35 ++++++++- .../robot_server/service/tip_length/router.py | 18 +++-- .../test_tip_length_access.tavern.yaml | 4 +- .../tip_length/test_tip_length_management.py | 21 +++-- 26 files changed, 281 insertions(+), 65 deletions(-) diff --git a/api-client/src/calibration/types.ts b/api-client/src/calibration/types.ts index 645f904d45b..c14ce57e64a 100644 --- a/api-client/src/calibration/types.ts +++ b/api-client/src/calibration/types.ts @@ -8,7 +8,7 @@ export interface PipOffsetDeletionParams { export interface TipLengthDeletionParams { calType: 'tipLength' - tiprack_hash: string + tiprack_uri: string pipette_id: string } export type DeleteCalRequestParams = @@ -93,7 +93,7 @@ export interface TipLengthCalibration { source: CalibrationSourceType status: IndividualCalibrationHealthStatus id: string - uri?: string | null + uri: string } export interface AllTipLengthCalibrations { diff --git a/api/src/opentrons/calibration_storage/ot2/models/v1.py b/api/src/opentrons/calibration_storage/ot2/models/v1.py index 98f7dadca1c..585700c84c5 100644 --- a/api/src/opentrons/calibration_storage/ot2/models/v1.py +++ b/api/src/opentrons/calibration_storage/ot2/models/v1.py @@ -32,8 +32,12 @@ class TipLengthModel(BaseModel): default_factory=CalibrationStatus, description="The status of the calibration data.", ) - uri: typing.Union[LabwareUri, Literal[""]] = Field( - ..., description="The tiprack URI associated with the tip length data." + # Old data may have a `uri` field, replaced later by `definitionHash`. + # uri: typing.Union[LabwareUri, Literal[""]] = Field( + # ..., description="The tiprack URI associated with the tip length data." + # ) + definitionHash: str = Field( + ..., description="The tiprack hash associated with the tip length data." ) @validator("tipLength") diff --git a/api/src/opentrons/calibration_storage/ot2/tip_length.py b/api/src/opentrons/calibration_storage/ot2/tip_length.py index eca8f723f09..7aff6ec9515 100644 --- a/api/src/opentrons/calibration_storage/ot2/tip_length.py +++ b/api/src/opentrons/calibration_storage/ot2/tip_length.py @@ -7,6 +7,7 @@ from opentrons import config from .. import file_operators as io, helpers, types as local_types +from opentrons_shared_data.pipette.dev_types import LabwareUri from opentrons.protocols.api_support.constants import OPENTRONS_NAMESPACE from opentrons.util.helpers import utc_now @@ -22,9 +23,9 @@ # Get Tip Length Calibration -def _conver_tip_length_model_to_dict( - to_dict: typing.Dict[str, v1.TipLengthModel] -) -> typing.Dict[str, typing.Any]: +def _convert_tip_length_model_to_dict( + to_dict: typing.Dict[LabwareUri, v1.TipLengthModel] +) -> typing.Dict[LabwareUri, typing.Any]: # This is a workaround since pydantic doesn't have a nice way to # add encoders when converting to a dict. dict_of_tip_lengths = {} @@ -35,17 +36,23 @@ def _conver_tip_length_model_to_dict( def tip_lengths_for_pipette( pipette_id: str, -) -> typing.Dict[str, v1.TipLengthModel]: +) -> typing.Dict[LabwareUri, v1.TipLengthModel]: tip_lengths = {} try: tip_length_filepath = config.get_tip_length_cal_path() / f"{pipette_id}.json" all_tip_lengths_for_pipette = io.read_cal_file(tip_length_filepath) - for tiprack, data in all_tip_lengths_for_pipette.items(): + for tiprack_identifier, data in all_tip_lengths_for_pipette.items(): + # We normally key these calibrations by their tip rack URI, + # but older software had them keyed by their tip rack hash. + # Migrate from the old format, if necessary. + if "/" not in tiprack_identifier: + data["definitionHash"] = tiprack_identifier + tiprack_identifier = data.pop("uri") try: - tip_lengths[tiprack] = v1.TipLengthModel(**data) + tip_lengths[LabwareUri(tiprack_identifier)] = v1.TipLengthModel(**data) except (json.JSONDecodeError, ValidationError): log.warning( - f"Tip length calibration is malformed for {tiprack} on {pipette_id}" + f"Tip length calibration is malformed for {tiprack_identifier} on {pipette_id}" ) pass return tip_lengths @@ -64,10 +71,10 @@ def load_tip_length_calibration( :param pip_id: pipette you are using :param definition: full definition of the tiprack """ - labware_hash = helpers.hash_labware_def(definition) + labware_uri = helpers.uri_from_definition(definition) load_name = definition["parameters"]["loadName"] try: - return tip_lengths_for_pipette(pip_id)[labware_hash] + return tip_lengths_for_pipette(pip_id)[labware_uri] except KeyError as e: raise local_types.TipLengthCalNotFound( f"Tip length of {load_name} has not been " @@ -89,16 +96,16 @@ def get_all_tip_length_calibrations() -> typing.List[v1.TipLengthCalibration]: if filepath.stem == "index": continue tip_lengths = tip_lengths_for_pipette(filepath.stem) - for tiprack_hash, tip_length in tip_lengths.items(): + for tiprack_uri, tip_length in tip_lengths.items(): all_tip_lengths_available.append( v1.TipLengthCalibration( pipette=filepath.stem, - tiprack=tiprack_hash, + tiprack=tip_length.definitionHash, tipLength=tip_length.tipLength, lastModified=tip_length.lastModified, source=tip_length.source, status=tip_length.status, - uri=tip_length.uri, + uri=tiprack_uri, ) ) return all_tip_lengths_available @@ -129,28 +136,45 @@ def get_custom_tiprack_definition_for_tlc(labware_uri: str) -> "LabwareDefinitio # Delete Tip Length Calibration -def delete_tip_length_calibration(tiprack: str, pipette_id: str) -> None: +def delete_tip_length_calibration( + pipette_id: str, + tiprack_uri: typing.Optional[LabwareUri] = None, + tiprack_hash: typing.Optional[str] = None, +) -> None: """ - Delete tip length calibration based on tiprack hash and - pipette serial number + Delete tip length calibration based on an optional tiprack uri or + tiprack hash and pipette serial number. - :param tiprack: tiprack hash + :param tiprack_uri: tiprack uri + :param tiprack_hash: tiprack uri :param pipette: pipette serial number """ tip_lengths = tip_lengths_for_pipette(pipette_id) - - if tiprack in tip_lengths: + tip_length_dir = config.get_tip_length_cal_path() + if tiprack_uri in tip_lengths: # maybe make modify and delete same file? - del tip_lengths[tiprack] - tip_length_dir = config.get_tip_length_cal_path() + del tip_lengths[tiprack_uri] + + if tip_lengths: + dict_of_tip_lengths = _convert_tip_length_model_to_dict(tip_lengths) + io.save_to_file(tip_length_dir, pipette_id, dict_of_tip_lengths) + else: + io.delete_file(tip_length_dir / f"{pipette_id}.json") + elif tiprack_hash and any(tiprack_hash in v.dict() for v in tip_lengths.values()): + # NOTE this is for backwards compatibilty only + # TODO delete this check once the tip_length DELETE router + # no longer depends on a tiprack hash + for k, v in tip_lengths.items(): + if tiprack_hash in v.dict(): + tip_lengths.pop(k) if tip_lengths: - dict_of_tip_lengths = _conver_tip_length_model_to_dict(tip_lengths) + dict_of_tip_lengths = _convert_tip_length_model_to_dict(tip_lengths) io.save_to_file(tip_length_dir, pipette_id, dict_of_tip_lengths) else: io.delete_file(tip_length_dir / f"{pipette_id}.json") else: raise local_types.TipLengthCalNotFound( - f"Tip length for hash {tiprack} has not been " + f"Tip length for uri {tiprack_uri} and hash {tiprack_hash} has not been " f"calibrated for this pipette: {pipette_id} and cannot" "be loaded" ) @@ -176,7 +200,7 @@ def create_tip_length_data( cal_status: typing.Optional[ typing.Union[local_types.CalibrationStatus, v1.CalibrationStatus] ] = None, -) -> typing.Dict[str, v1.TipLengthModel]: +) -> typing.Dict[LabwareUri, v1.TipLengthModel]: """ Function to correctly format tip length data. @@ -197,13 +221,13 @@ def create_tip_length_data( lastModified=utc_now(), source=local_types.SourceType.user, status=cal_status_model, - uri=labware_uri, + definitionHash=labware_hash, ) if not definition.get("namespace") == OPENTRONS_NAMESPACE: _save_custom_tiprack_definition(labware_uri, definition) - data = {labware_hash: tip_length_data} + data = {labware_uri: tip_length_data} return data @@ -220,7 +244,7 @@ def _save_custom_tiprack_definition( def save_tip_length_calibration( pip_id: str, - tip_length_cal: typing.Dict[str, v1.TipLengthModel], + tip_length_cal: typing.Dict[LabwareUri, v1.TipLengthModel], ) -> None: """ Function used to save tip length calibration to file. @@ -235,5 +259,5 @@ def save_tip_length_calibration( all_tip_lengths.update(tip_length_cal) - dict_of_tip_lengths = _conver_tip_length_model_to_dict(all_tip_lengths) + dict_of_tip_lengths = _convert_tip_length_model_to_dict(all_tip_lengths) io.save_to_file(tip_length_dir_path, pip_id, dict_of_tip_lengths) diff --git a/api/src/opentrons/hardware_control/instruments/ot2/instrument_calibration.py b/api/src/opentrons/hardware_control/instruments/ot2/instrument_calibration.py index 29900d68a6d..f2f8a7fc426 100644 --- a/api/src/opentrons/hardware_control/instruments/ot2/instrument_calibration.py +++ b/api/src/opentrons/hardware_control/instruments/ot2/instrument_calibration.py @@ -130,18 +130,15 @@ def load_tip_length_for_pipette( pipette_id, tiprack ) - # TODO (lc 09-26-2022) We shouldn't have to do a hash twice. We should figure out what - # information we actually need from the labware definition and pass it into - # the `load_tip_length_calibration` function. - tiprack_hash = helpers.hash_labware_def(tiprack) + tiprack_uri = helpers.uri_from_definition(tiprack) return TipLengthCalibration( tip_length=tip_length_data.tipLength, source=tip_length_data.source, pipette=pipette_id, - tiprack=tiprack_hash, + tiprack=tip_length_data.definitionHash, last_modified=tip_length_data.lastModified, - uri=tip_length_data.uri, + uri=tiprack_uri, status=types.CalibrationStatus( markedAt=tip_length_data.status.markedAt, markedBad=tip_length_data.status.markedBad, diff --git a/api/tests/opentrons/calibration_storage/test_tip_length_ot2.py b/api/tests/opentrons/calibration_storage/test_tip_length_ot2.py index 93a208e0071..4b63b52d3fc 100644 --- a/api/tests/opentrons/calibration_storage/test_tip_length_ot2.py +++ b/api/tests/opentrons/calibration_storage/test_tip_length_ot2.py @@ -1,9 +1,11 @@ import pytest -from typing import cast, Any, TYPE_CHECKING +from typing import Any, TYPE_CHECKING +from opentrons import config from opentrons.calibration_storage import ( types as cs_types, helpers, + file_operators as io, ) from opentrons.calibration_storage.ot2 import ( @@ -15,10 +17,10 @@ clear_tip_length_calibration, models, ) +from opentrons_shared_data.pipette.dev_types import LabwareUri if TYPE_CHECKING: from opentrons_shared_data.labware.dev_types import LabwareDefinition - from opentrons_shared_data.pipette.dev_types import LabwareUri @pytest.fixture @@ -38,6 +40,18 @@ def starting_calibration_data( save_tip_length_calibration("pip1", tip_length1) save_tip_length_calibration("pip2", tip_length2) save_tip_length_calibration("pip1", tip_length3) + inside_data = tip_length3[LabwareUri("dummy_namespace/minimal_labware_def/1")] + data = { + inside_data.definitionHash: { + "tipLength": 27, + "lastModified": inside_data.lastModified.isoformat(), + "source": inside_data.source, + "status": inside_data.status.dict(), + "uri": "dummy_namespace/minimal_labware_def/1", + } + } + tip_length_dir_path = config.get_tip_length_cal_path() + io.save_to_file(tip_length_dir_path, "pip2", data) def test_save_tip_length_calibration( @@ -48,13 +62,13 @@ def test_save_tip_length_calibration( """ assert tip_lengths_for_pipette("pip1") == {} assert tip_lengths_for_pipette("pip2") == {} - tip_rack_hash = helpers.hash_labware_def(minimal_labware_def) + tip_rack_uri = helpers.uri_from_definition(minimal_labware_def) tip_length1 = create_tip_length_data(minimal_labware_def, 22.0) tip_length2 = create_tip_length_data(minimal_labware_def, 31.0) save_tip_length_calibration("pip1", tip_length1) save_tip_length_calibration("pip2", tip_length2) - assert tip_lengths_for_pipette("pip1")[tip_rack_hash].tipLength == 22.0 - assert tip_lengths_for_pipette("pip2")[tip_rack_hash].tipLength == 31.0 + assert tip_lengths_for_pipette("pip1")[tip_rack_uri].tipLength == 22.0 + assert tip_lengths_for_pipette("pip2")[tip_rack_uri].tipLength == 31.0 def test_get_tip_length_calibration( @@ -64,11 +78,12 @@ def test_get_tip_length_calibration( Test ability to get a tip length calibration model. """ tip_length_data = load_tip_length_calibration("pip1", minimal_labware_def) + tip_rack_hash = helpers.hash_labware_def(minimal_labware_def) assert tip_length_data == models.v1.TipLengthModel( tipLength=22.0, source=cs_types.SourceType.user, lastModified=tip_length_data.lastModified, - uri=cast("LabwareUri", "opentronstest/minimal_labware_def/1"), + definitionHash=tip_rack_hash, ) with pytest.raises(cs_types.TipLengthCalNotFound): @@ -83,8 +98,8 @@ def test_delete_specific_tip_calibration( """ assert len(tip_lengths_for_pipette("pip1").keys()) == 2 assert tip_lengths_for_pipette("pip2") != {} - tip_rack_hash = helpers.hash_labware_def(minimal_labware_def) - delete_tip_length_calibration(tip_rack_hash, "pip1") + tip_rack_uri = helpers.uri_from_definition(minimal_labware_def) + delete_tip_length_calibration("pip1", tiprack_uri=tip_rack_uri) assert len(tip_lengths_for_pipette("pip1").keys()) == 1 assert tip_lengths_for_pipette("pip2") != {} diff --git a/api/tests/opentrons/hardware_control/instruments/test_instrument_calibration.py b/api/tests/opentrons/hardware_control/instruments/test_instrument_calibration.py index b850803ba61..6aa3ca2a009 100644 --- a/api/tests/opentrons/hardware_control/instruments/test_instrument_calibration.py +++ b/api/tests/opentrons/hardware_control/instruments/test_instrument_calibration.py @@ -81,7 +81,7 @@ def test_load_tip_length( tip_length_data = v1_models.TipLengthModel( tipLength=1.23, lastModified=datetime(year=2023, month=1, day=1), - uri=LabwareUri("def456"), + definitionHash="asdfghjk", source=subject.SourceType.factory, status=v1_models.CalibrationStatus( markedBad=True, @@ -99,6 +99,9 @@ def test_load_tip_length( decoy.when(calibration_storage.helpers.hash_labware_def(tip_rack_dict)).then_return( "asdfghjk" ) + decoy.when( + calibration_storage.helpers.uri_from_definition(tip_rack_dict) + ).then_return(LabwareUri("def456")) result = subject.load_tip_length_for_pipette( pipette_id="abc123", tiprack=tip_rack_definition diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[25f79fd65e][Flex_None_None_TM_2_16_AnalysisError_ModuleInStagingAreaCol3].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[25f79fd65e][Flex_None_None_TM_2_16_AnalysisError_ModuleInStagingAreaCol3].json index 8d959836e18..5dd0f2c0346 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[25f79fd65e][Flex_None_None_TM_2_16_AnalysisError_ModuleInStagingAreaCol3].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[25f79fd65e][Flex_None_None_TM_2_16_AnalysisError_ModuleInStagingAreaCol3].json @@ -564,7 +564,7 @@ "errorInfo": { "args": "('nest_1_reservoir_290ml in slot C4 prevents temperatureModuleV2 from using slot C3.',)", "class": "DeckConflictError", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 69, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"Flex_None_None_TM_2_16_AnalysisError_ModuleInStagingAreaCol3.py\", line 17, in run\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/api_support/util.py\", line 383, in _check_version_wrapper\n return decorated_obj(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/protocol_context.py\", line 818, in load_module\n module_core = self._core.load_module(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/protocol.py\", line 435, in load_module\n deck_conflict.check(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/deck_conflict.py\", line 185, in check\n wrapped_deck_conflict.check(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/motion_planning/deck_conflict.py\", line 224, in check\n raise DeckConflictError(\n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 69, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"Flex_None_None_TM_2_16_AnalysisError_ModuleInStagingAreaCol3.py\", line 17, in run\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/api_support/util.py\", line 383, in _check_version_wrapper\n return decorated_obj(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/protocol_context.py\", line 818, in load_module\n module_core = self._core.load_module(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/protocol.py\", line 435, in load_module\n deck_conflict.check(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/deck_conflict.py\", line 190, in check\n wrapped_deck_conflict.check(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/motion_planning/deck_conflict.py\", line 224, in check\n raise DeckConflictError(\n" }, "errorType": "PythonException", "wrappedErrors": [] diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[2a185c4e1c][Flex_P1000_96_GRIPPER_HS_TM_TC_MB_2_16_Smoke].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[2a185c4e1c][Flex_P1000_96_GRIPPER_HS_TM_TC_MB_2_16_Smoke].json index c638710d9ea..f363e79201f 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[2a185c4e1c][Flex_P1000_96_GRIPPER_HS_TM_TC_MB_2_16_Smoke].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[2a185c4e1c][Flex_P1000_96_GRIPPER_HS_TM_TC_MB_2_16_Smoke].json @@ -8271,6 +8271,7 @@ "addressableAreaName": "movableTrashB3", "alternateDropLocation": true, "forceDirect": false, + "ignoreTipConfiguration": true, "offset": { "x": 0.0, "y": 0.0, @@ -8373,6 +8374,7 @@ "addressableAreaName": "movableTrashB3", "alternateDropLocation": true, "forceDirect": false, + "ignoreTipConfiguration": true, "offset": { "x": 0.0, "y": 0.0, @@ -8475,6 +8477,7 @@ "addressableAreaName": "movableTrashB3", "alternateDropLocation": true, "forceDirect": false, + "ignoreTipConfiguration": true, "offset": { "x": 0.0, "y": 0.0, @@ -8577,6 +8580,7 @@ "addressableAreaName": "movableTrashB3", "alternateDropLocation": true, "forceDirect": false, + "ignoreTipConfiguration": true, "offset": { "x": 0.0, "y": 0.0, @@ -8679,6 +8683,7 @@ "addressableAreaName": "movableTrashB3", "alternateDropLocation": true, "forceDirect": false, + "ignoreTipConfiguration": true, "offset": { "x": 0.0, "y": 0.0, @@ -8781,6 +8786,7 @@ "addressableAreaName": "movableTrashB3", "alternateDropLocation": true, "forceDirect": false, + "ignoreTipConfiguration": true, "offset": { "x": 0.0, "y": 0.0, @@ -8883,6 +8889,7 @@ "addressableAreaName": "movableTrashB3", "alternateDropLocation": true, "forceDirect": false, + "ignoreTipConfiguration": true, "offset": { "x": 0.0, "y": 0.0, @@ -8985,6 +8992,7 @@ "addressableAreaName": "movableTrashB3", "alternateDropLocation": true, "forceDirect": false, + "ignoreTipConfiguration": true, "offset": { "x": 0.0, "y": 0.0, @@ -9087,6 +9095,7 @@ "addressableAreaName": "movableTrashB3", "alternateDropLocation": true, "forceDirect": false, + "ignoreTipConfiguration": true, "offset": { "x": 0.0, "y": 0.0, @@ -9189,6 +9198,7 @@ "addressableAreaName": "movableTrashB3", "alternateDropLocation": true, "forceDirect": false, + "ignoreTipConfiguration": true, "offset": { "x": 0.0, "y": 0.0, @@ -9291,6 +9301,7 @@ "addressableAreaName": "movableTrashB3", "alternateDropLocation": true, "forceDirect": false, + "ignoreTipConfiguration": true, "offset": { "x": 0.0, "y": 0.0, @@ -9393,6 +9404,7 @@ "addressableAreaName": "movableTrashB3", "alternateDropLocation": true, "forceDirect": false, + "ignoreTipConfiguration": true, "offset": { "x": 0.0, "y": 0.0, @@ -9641,6 +9653,7 @@ "addressableAreaName": "movableTrashB3", "alternateDropLocation": false, "forceDirect": false, + "ignoreTipConfiguration": true, "offset": { "x": 0.0, "y": 0.0, @@ -10316,6 +10329,7 @@ "addressableAreaName": "movableTrashB3", "alternateDropLocation": false, "forceDirect": false, + "ignoreTipConfiguration": true, "offset": { "x": 0.0, "y": 0.0, diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[2a32a763f5][Flex_P1000_96_GRIPPER_HS_TM_TC_MB_2_16_DeckConfiguration1_NoModules].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[2a32a763f5][Flex_P1000_96_GRIPPER_HS_TM_TC_MB_2_16_DeckConfiguration1_NoModules].json index 29699064add..e1749edf244 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[2a32a763f5][Flex_P1000_96_GRIPPER_HS_TM_TC_MB_2_16_DeckConfiguration1_NoModules].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[2a32a763f5][Flex_P1000_96_GRIPPER_HS_TM_TC_MB_2_16_DeckConfiguration1_NoModules].json @@ -10514,6 +10514,7 @@ "addressableAreaName": "movableTrashC1", "alternateDropLocation": true, "forceDirect": false, + "ignoreTipConfiguration": true, "offset": { "x": 0.0, "y": 0.0, @@ -10758,6 +10759,7 @@ "addressableAreaName": "movableTrashD1", "alternateDropLocation": true, "forceDirect": false, + "ignoreTipConfiguration": true, "offset": { "x": 0.0, "y": 0.0, diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4835239037][OT2_P300M_P20S_MM_TM_TC1_5_2_6_PD40Error].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4835239037][OT2_P300M_P20S_MM_TM_TC1_5_2_6_PD40Error].json index 2a4ecdd58d3..ac2117946a3 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4835239037][OT2_P300M_P20S_MM_TM_TC1_5_2_6_PD40Error].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4835239037][OT2_P300M_P20S_MM_TM_TC1_5_2_6_PD40Error].json @@ -6949,7 +6949,7 @@ "errorInfo": { "args": "('Cannot aspirate more than pipette max volume',)", "class": "AssertionError", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/task_queue.py\", line 90, in _run\n await self._run_func()\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_wrappers.py\", line 173, in execute\n await to_thread.run_sync(run_protocol, protocol, context)\n\n File \"/usr/local/lib/python3.10/site-packages/anyio/to_thread.py\", line 31, in run_sync\n return await get_asynclib().run_sync_in_worker_thread(\n\n File \"/usr/local/lib/python3.10/site-packages/anyio/_backends/_asyncio.py\", line 937, in run_sync_in_worker_thread\n return await future\n\n File \"/usr/local/lib/python3.10/site-packages/anyio/_backends/_asyncio.py\", line 867, in run\n result = context.run(func, *args)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute.py\", line 45, in run_protocol\n execute_json_v4.dispatch_json(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_json_v4.py\", line 272, in dispatch_json\n pipette_command_map[command_type]( # type: ignore\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_json_v3.py\", line 159, in _aspirate\n pipette.aspirate(volume, location)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/api_support/util.py\", line 383, in _check_version_wrapper\n return decorated_obj(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/instrument_context.py\", line 267, in aspirate\n self._core.aspirate(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py\", line 119, in aspirate\n new_volume <= self._pipette_dict[\"working_volume\"]\n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/task_queue.py\", line 90, in _run\n await self._run_func()\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_wrappers.py\", line 173, in execute\n await to_thread.run_sync(run_protocol, protocol, context)\n\n File \"/usr/local/lib/python3.10/site-packages/anyio/to_thread.py\", line 33, in run_sync\n return await get_asynclib().run_sync_in_worker_thread(\n\n File \"/usr/local/lib/python3.10/site-packages/anyio/_backends/_asyncio.py\", line 877, in run_sync_in_worker_thread\n return await future\n\n File \"/usr/local/lib/python3.10/site-packages/anyio/_backends/_asyncio.py\", line 807, in run\n result = context.run(func, *args)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute.py\", line 45, in run_protocol\n execute_json_v4.dispatch_json(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_json_v4.py\", line 272, in dispatch_json\n pipette_command_map[command_type]( # type: ignore\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_json_v3.py\", line 159, in _aspirate\n pipette.aspirate(volume, location)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/api_support/util.py\", line 383, in _check_version_wrapper\n return decorated_obj(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/instrument_context.py\", line 267, in aspirate\n self._core.aspirate(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py\", line 119, in aspirate\n new_volume <= self._pipette_dict[\"working_volume\"]\n" }, "errorType": "PythonException", "wrappedErrors": [] diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[5eb46a4f85][Flex_P1000_96_GRIPPER_2_16_AnalysisError_DropLabwareIntoTrashBin].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[5eb46a4f85][Flex_P1000_96_GRIPPER_2_16_AnalysisError_DropLabwareIntoTrashBin].json index 93b099eae94..af05109c1e0 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[5eb46a4f85][Flex_P1000_96_GRIPPER_2_16_AnalysisError_DropLabwareIntoTrashBin].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[5eb46a4f85][Flex_P1000_96_GRIPPER_2_16_AnalysisError_DropLabwareIntoTrashBin].json @@ -1257,6 +1257,7 @@ "addressableAreaName": "movableTrashC3", "alternateDropLocation": true, "forceDirect": false, + "ignoreTipConfiguration": true, "offset": { "x": 0.0, "y": 0.0, diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[753ac8811f][OT2_None_None_2_13_PythonSyntaxError].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[753ac8811f][OT2_None_None_2_13_PythonSyntaxError].json index 872147c26bc..c2eba70dccc 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[753ac8811f][OT2_None_None_2_13_PythonSyntaxError].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[753ac8811f][OT2_None_None_2_13_PythonSyntaxError].json @@ -30,7 +30,7 @@ "msg": "No module named 'superspecialmagic'", "name": "superspecialmagic", "path": "None", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/task_queue.py\", line 90, in _run\n await self._run_func()\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_wrappers.py\", line 173, in execute\n await to_thread.run_sync(run_protocol, protocol, context)\n\n File \"/usr/local/lib/python3.10/site-packages/anyio/to_thread.py\", line 31, in run_sync\n return await get_asynclib().run_sync_in_worker_thread(\n\n File \"/usr/local/lib/python3.10/site-packages/anyio/_backends/_asyncio.py\", line 937, in run_sync_in_worker_thread\n return await future\n\n File \"/usr/local/lib/python3.10/site-packages/anyio/_backends/_asyncio.py\", line 867, in run\n result = context.run(func, *args)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute.py\", line 27, in run_protocol\n run_python(protocol, context)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 46, in run_python\n exec(proto.contents, new_globs)\n\n File \"OT2_None_None_2_13_PythonSyntaxError.py\", line 4, in \n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/task_queue.py\", line 90, in _run\n await self._run_func()\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_wrappers.py\", line 173, in execute\n await to_thread.run_sync(run_protocol, protocol, context)\n\n File \"/usr/local/lib/python3.10/site-packages/anyio/to_thread.py\", line 33, in run_sync\n return await get_asynclib().run_sync_in_worker_thread(\n\n File \"/usr/local/lib/python3.10/site-packages/anyio/_backends/_asyncio.py\", line 877, in run_sync_in_worker_thread\n return await future\n\n File \"/usr/local/lib/python3.10/site-packages/anyio/_backends/_asyncio.py\", line 807, in run\n result = context.run(func, *args)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute.py\", line 27, in run_protocol\n run_python(protocol, context)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 46, in run_python\n exec(proto.contents, new_globs)\n\n File \"OT2_None_None_2_13_PythonSyntaxError.py\", line 4, in \n" }, "errorType": "PythonException", "wrappedErrors": [] diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a23a1de3ce][OT2_P300M_P20S_TC_HS_TM_2_16_SmokeTestV3].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a23a1de3ce][OT2_P300M_P20S_TC_HS_TM_2_16_SmokeTestV3].json index 49b64623b97..0c7c361123c 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a23a1de3ce][OT2_P300M_P20S_TC_HS_TM_2_16_SmokeTestV3].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a23a1de3ce][OT2_P300M_P20S_TC_HS_TM_2_16_SmokeTestV3].json @@ -11102,6 +11102,7 @@ "addressableAreaName": "fixedTrash", "alternateDropLocation": false, "forceDirect": false, + "ignoreTipConfiguration": true, "offset": { "x": 0.0, "y": 0.0, @@ -11137,6 +11138,7 @@ "addressableAreaName": "fixedTrash", "alternateDropLocation": false, "forceDirect": false, + "ignoreTipConfiguration": true, "offset": { "x": 0.0, "y": 0.0, @@ -11216,6 +11218,7 @@ "addressableAreaName": "fixedTrash", "alternateDropLocation": false, "forceDirect": false, + "ignoreTipConfiguration": true, "offset": { "x": 0.0, "y": 0.0, @@ -11295,6 +11298,7 @@ "addressableAreaName": "fixedTrash", "alternateDropLocation": false, "forceDirect": false, + "ignoreTipConfiguration": true, "offset": { "x": 0.0, "y": 0.0, @@ -11374,6 +11378,7 @@ "addressableAreaName": "fixedTrash", "alternateDropLocation": false, "forceDirect": false, + "ignoreTipConfiguration": true, "offset": { "x": 0.0, "y": 0.0, @@ -11453,6 +11458,7 @@ "addressableAreaName": "fixedTrash", "alternateDropLocation": false, "forceDirect": false, + "ignoreTipConfiguration": true, "offset": { "x": 0.0, "y": 0.0, @@ -11532,6 +11538,7 @@ "addressableAreaName": "fixedTrash", "alternateDropLocation": false, "forceDirect": false, + "ignoreTipConfiguration": true, "offset": { "x": 0.0, "y": 0.0, @@ -11561,6 +11568,7 @@ "addressableAreaName": "fixedTrash", "alternateDropLocation": true, "forceDirect": false, + "ignoreTipConfiguration": true, "offset": { "x": 0.0, "y": 0.0, @@ -14985,6 +14993,7 @@ "addressableAreaName": "fixedTrash", "alternateDropLocation": true, "forceDirect": false, + "ignoreTipConfiguration": true, "offset": { "x": 0.0, "y": 0.0, @@ -15186,6 +15195,7 @@ "addressableAreaName": "fixedTrash", "alternateDropLocation": true, "forceDirect": false, + "ignoreTipConfiguration": true, "offset": { "x": 0.0, "y": 0.0, @@ -15263,6 +15273,7 @@ "addressableAreaName": "fixedTrash", "alternateDropLocation": true, "forceDirect": false, + "ignoreTipConfiguration": true, "offset": { "x": 0.0, "y": 0.0, diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[afe15b729c][Flex_P1000_96_GRIPPER_HS_TM_TC_MB_2_16_DeckConfiguration1].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[afe15b729c][Flex_P1000_96_GRIPPER_HS_TM_TC_MB_2_16_DeckConfiguration1].json index caffbd78786..1bb7131a414 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[afe15b729c][Flex_P1000_96_GRIPPER_HS_TM_TC_MB_2_16_DeckConfiguration1].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[afe15b729c][Flex_P1000_96_GRIPPER_HS_TM_TC_MB_2_16_DeckConfiguration1].json @@ -12283,6 +12283,7 @@ "addressableAreaName": "movableTrashC1", "alternateDropLocation": true, "forceDirect": false, + "ignoreTipConfiguration": true, "offset": { "x": 0.0, "y": 0.0, @@ -12527,6 +12528,7 @@ "addressableAreaName": "movableTrashD1", "alternateDropLocation": true, "forceDirect": false, + "ignoreTipConfiguration": true, "offset": { "x": 0.0, "y": 0.0, diff --git a/app/src/organisms/Devices/hooks/__fixtures__/taskListFixtures.ts b/app/src/organisms/Devices/hooks/__fixtures__/taskListFixtures.ts index e23593fe599..e7613939722 100644 --- a/app/src/organisms/Devices/hooks/__fixtures__/taskListFixtures.ts +++ b/app/src/organisms/Devices/hooks/__fixtures__/taskListFixtures.ts @@ -91,6 +91,7 @@ export const mockBadTipLengthCalibrations: TipLengthCalibration[] = [ source: 'user', status: { markedBad: true, source: null, markedAt: null }, id: 'test-tip-length-id-1', + uri: 'test-uri', }, { tipLength: 0, @@ -100,6 +101,7 @@ export const mockBadTipLengthCalibrations: TipLengthCalibration[] = [ source: 'user', status: { markedBad: true, source: null, markedAt: null }, id: 'test-tip-length-id-2', + uri: 'test-uri-2', }, ] @@ -112,6 +114,7 @@ export const mockCompleteTipLengthCalibrations: TipLengthCalibration[] = [ source: 'user', status: { markedBad: false, source: null, markedAt: null }, id: 'test-tip-length-id-1', + uri: 'test-uri', }, { tipLength: 0, @@ -121,6 +124,7 @@ export const mockCompleteTipLengthCalibrations: TipLengthCalibration[] = [ source: 'user', status: { markedBad: false, source: null, markedAt: null }, id: 'test-tip-length-id-2', + uri: 'test-uri-2', }, ] @@ -133,6 +137,7 @@ export const mockIncompleteTipLengthCalibrations: TipLengthCalibration[] = [ source: 'user', status: { markedBad: false, source: null, markedAt: null }, id: 'test-tip-length-id-2', + uri: 'test-uri-2', }, ] diff --git a/app/src/organisms/RobotSettingsCalibration/CalibrationDetails/OverflowMenu.tsx b/app/src/organisms/RobotSettingsCalibration/CalibrationDetails/OverflowMenu.tsx index 228d17155d1..aa6a1a68536 100644 --- a/app/src/organisms/RobotSettingsCalibration/CalibrationDetails/OverflowMenu.tsx +++ b/app/src/organisms/RobotSettingsCalibration/CalibrationDetails/OverflowMenu.tsx @@ -160,7 +160,7 @@ export function OverflowMenu({ if (applicableTipLengthCal == null) return params = { calType, - tiprack_hash: applicableTipLengthCal.tiprack, + tiprack_uri: applicableTipLengthCal.uri, pipette_id: applicableTipLengthCal.pipette, } } diff --git a/app/src/organisms/RobotSettingsCalibration/CalibrationDetails/__tests__/OverflowMenu.test.tsx b/app/src/organisms/RobotSettingsCalibration/CalibrationDetails/__tests__/OverflowMenu.test.tsx index a1341620419..b518dcb6ce1 100644 --- a/app/src/organisms/RobotSettingsCalibration/CalibrationDetails/__tests__/OverflowMenu.test.tsx +++ b/app/src/organisms/RobotSettingsCalibration/CalibrationDetails/__tests__/OverflowMenu.test.tsx @@ -261,7 +261,7 @@ describe('OverflowMenu', () => { } const expectedCallParams = { calType: 'tipLength', - tiprack_hash: mockTipLengthCalibrationResponse.tiprack, + tiprack_uri: mockTipLengthCalibrationResponse.uri, pipette_id: mockTipLengthCalibrationResponse.pipette, } const [{ getByText, getByLabelText }] = render(props) diff --git a/app/src/redux/calibration/api-types.ts b/app/src/redux/calibration/api-types.ts index d12f752e021..624af038c00 100644 --- a/app/src/redux/calibration/api-types.ts +++ b/app/src/redux/calibration/api-types.ts @@ -128,7 +128,7 @@ export interface TipLengthCalibration { source: CalibrationSource status: IndividualCalibrationStatus id: string - uri?: string | null + uri: string } export interface AllTipLengthCalibrations { diff --git a/app/src/redux/calibration/tip-length/__fixtures__/tip-length-calibration.ts b/app/src/redux/calibration/tip-length/__fixtures__/tip-length-calibration.ts index b6820445451..86257389b3b 100644 --- a/app/src/redux/calibration/tip-length/__fixtures__/tip-length-calibration.ts +++ b/app/src/redux/calibration/tip-length/__fixtures__/tip-length-calibration.ts @@ -23,6 +23,7 @@ export const mockTipLengthCalibration1: TipLengthCalibration = { markedAt: '', }, id: 'someID', + uri: 'test-uri', } export const mockTipLengthCalibration2: TipLengthCalibration = { @@ -37,6 +38,7 @@ export const mockTipLengthCalibration2: TipLengthCalibration = { markedAt: '', }, id: 'someID', + uri: 'test-uri', } export const mockTipLengthCalibration3: TipLengthCalibration = { @@ -51,6 +53,7 @@ export const mockTipLengthCalibration3: TipLengthCalibration = { markedAt: '', }, id: 'someID', + uri: 'test-uri', } export const mockPipetteMatchTipLengthCalibration: AllTipLengthCalibrations = { diff --git a/hardware/opentrons_hardware/firmware_bindings/constants.py b/hardware/opentrons_hardware/firmware_bindings/constants.py index 61387ea798d..6d173e6effc 100644 --- a/hardware/opentrons_hardware/firmware_bindings/constants.py +++ b/hardware/opentrons_hardware/firmware_bindings/constants.py @@ -250,6 +250,13 @@ class MessageId(int, Enum): peripheral_status_response = 0x8D baseline_sensor_response = 0x8E + set_hepa_fan_state_request = 0x90 + get_hepa_fan_state_request = 0x91 + get_hepa_fan_state_response = 0x92 + set_hepa_uv_state_request = 0x93 + get_hepa_uv_state_request = 0x94 + get_hepa_uv_state_response = 0x95 + @unique class ErrorSeverity(int, Enum): diff --git a/hardware/opentrons_hardware/firmware_bindings/messages/message_definitions.py b/hardware/opentrons_hardware/firmware_bindings/messages/message_definitions.py index 9af02770745..49698329264 100644 --- a/hardware/opentrons_hardware/firmware_bindings/messages/message_definitions.py +++ b/hardware/opentrons_hardware/firmware_bindings/messages/message_definitions.py @@ -907,3 +907,73 @@ class HepaUVInfoResponse(BaseMessage): # noqa: D101 payloads.HepaUVInfoResponsePayload ] = payloads.HepaUVInfoResponsePayload message_id: Literal[MessageId.hepauv_info_response] = MessageId.hepauv_info_response + + +@dataclass +class SetHepaFanStateRequest(BaseMessage): + """Request to set the state and duty cycle of the hepa fan.""" + + payload: payloads.SetHepaFanStateRequestPayload + payload_type: Type[ + payloads.SetHepaFanStateRequestPayload + ] = payloads.SetHepaFanStateRequestPayload + message_id: Literal[ + MessageId.set_hepa_fan_state_request + ] = MessageId.set_hepa_fan_state_request + + +@dataclass +class GetHepaFanStateRequest(EmptyPayloadMessage): + """Request the Hepa/UV to send the state and duty cycle of the fan.""" + + message_id: Literal[ + MessageId.get_hepa_fan_state_request + ] = MessageId.get_hepa_fan_state_request + + +@dataclass +class GetHepaFanStateResponse(BaseMessage): + """Hepa/UV response with the state and duty cycle of the fan.""" + + payload: payloads.GetHepaFanStatePayloadResponse + payload_type: Type[ + payloads.GetHepaFanStatePayloadResponse + ] = payloads.GetHepaFanStatePayloadResponse + message_id: Literal[ + MessageId.get_hepa_fan_state_response + ] = MessageId.get_hepa_fan_state_response + + +@dataclass +class SetHepaUVStateRequest(BaseMessage): + """Sets the state and timeout in seconds the UV light should stay on.""" + + payload: payloads.SetHepaUVStateRequestPayload + payload_type: Type[ + payloads.SetHepaUVStateRequestPayload + ] = payloads.SetHepaUVStateRequestPayload + message_id: Literal[ + MessageId.set_hepa_uv_state_request + ] = MessageId.set_hepa_uv_state_request + + +@dataclass +class GetHepaUVStateRequest(EmptyPayloadMessage): + """Request the Hepa/UV send the state and timeout in seconds for the UV light.""" + + message_id: Literal[ + MessageId.get_hepa_uv_state_request + ] = MessageId.get_hepa_uv_state_request + + +@dataclass +class GetHepaUVStateResponse(BaseMessage): + """Response from the Hepa/UV state and timeout in seconds for the UV light.""" + + payload: payloads.GetHepaUVStatePayloadResponse + payload_type: Type[ + payloads.GetHepaUVStatePayloadResponse + ] = payloads.GetHepaUVStatePayloadResponse + message_id: Literal[ + MessageId.get_hepa_uv_state_response + ] = MessageId.get_hepa_uv_state_response diff --git a/hardware/opentrons_hardware/firmware_bindings/messages/messages.py b/hardware/opentrons_hardware/firmware_bindings/messages/messages.py index b1563f5ecf4..930c82bab79 100644 --- a/hardware/opentrons_hardware/firmware_bindings/messages/messages.py +++ b/hardware/opentrons_hardware/firmware_bindings/messages/messages.py @@ -100,6 +100,12 @@ defs.SetGripperJawHoldoffRequest, defs.GripperJawHoldoffRequest, defs.GripperJawHoldoffResponse, + defs.SetHepaFanStateRequest, + defs.GetHepaFanStateRequest, + defs.GetHepaFanStateResponse, + defs.SetHepaUVStateRequest, + defs.GetHepaUVStateRequest, + defs.GetHepaUVStateResponse, ] diff --git a/hardware/opentrons_hardware/firmware_bindings/messages/payloads.py b/hardware/opentrons_hardware/firmware_bindings/messages/payloads.py index 650c5d1e30c..c2efd8ac416 100644 --- a/hardware/opentrons_hardware/firmware_bindings/messages/payloads.py +++ b/hardware/opentrons_hardware/firmware_bindings/messages/payloads.py @@ -631,7 +631,40 @@ def build(cls, data: bytes) -> "GetMotorUsageResponsePayload": @dataclass(eq=False) class HepaUVInfoResponsePayload(EmptyPayload): - """A response carrying data about an attached gripper.""" + """A response carrying data about an attached hepa uv.""" model: utils.UInt16Field serial: SerialDataCodeField + + +@dataclass(eq=False) +class SetHepaFanStateRequestPayload(EmptyPayload): + """A request to set the state and pwm of a the hepa fan.""" + + duty_cycle: utils.UInt32Field + fan_on: utils.Int8Field + + +@dataclass(eq=False) +class GetHepaFanStatePayloadResponse(EmptyPayload): + """A response with the state and pwm of the fan.""" + + duty_cycle: utils.UInt32Field + fan_on: utils.UInt8Field + + +@dataclass(eq=False) +class SetHepaUVStateRequestPayload(EmptyPayload): + """A request to set the state and timeout in seconds of the hepa uv light.""" + + timeout_s: utils.UInt32Field + uv_light_on: utils.UInt8Field + + +@dataclass(eq=False) +class GetHepaUVStatePayloadResponse(EmptyPayload): + """A response with the state and timeout in seconds of the hepa uv light.""" + + timeout_s: utils.UInt32Field + uv_light_on: utils.UInt8Field + remaining_time_s: utils.UInt32Field diff --git a/robot-server/robot_server/service/tip_length/router.py b/robot-server/robot_server/service/tip_length/router.py index 2d6461e0b7f..f1b5fa3166a 100644 --- a/robot-server/robot_server/service/tip_length/router.py +++ b/robot-server/robot_server/service/tip_length/router.py @@ -1,6 +1,6 @@ from starlette import status from fastapi import APIRouter, Depends -from typing import Optional +from typing import Optional, cast from opentrons.calibration_storage import types as cal_types from opentrons.calibration_storage.ot2 import tip_length, models @@ -12,6 +12,7 @@ from robot_server.service.shared_models import calibration as cal_model from opentrons.hardware_control import API +from opentrons_shared_data.pipette.dev_types import LabwareUri router = APIRouter() @@ -80,17 +81,24 @@ async def get_all_tip_length_calibrations( @router.delete( "/calibration/tip_length", description="Delete one specific tip length calibration by pipette " - "serial and tiprack hash", + "serial and tiprack uri", responses={status.HTTP_404_NOT_FOUND: {"model": ErrorBody}}, ) async def delete_specific_tip_length_calibration( - tiprack_hash: str, pipette_id: str, _: API = Depends(get_ot2_hardware) + pipette_id: str, + tiprack_hash: Optional[str] = None, + tiprack_uri: Optional[str] = None, + _: API = Depends(get_ot2_hardware), ): try: - tip_length.delete_tip_length_calibration(tiprack_hash, pipette_id) + tip_length.delete_tip_length_calibration( + pipette_id, + tiprack_uri=cast(LabwareUri, tiprack_uri), + tiprack_hash=tiprack_hash, + ) except cal_types.TipLengthCalNotFound: raise RobotServerError( definition=CommonErrorDef.RESOURCE_NOT_FOUND, resource="TipLengthCalibration", - id=f"{tiprack_hash}&{pipette_id}", + id=f"{tiprack_uri}&{pipette_id}", ) diff --git a/robot-server/tests/integration/test_tip_length_access.tavern.yaml b/robot-server/tests/integration/test_tip_length_access.tavern.yaml index 9b181e0877a..35e8f6d2d07 100644 --- a/robot-server/tests/integration/test_tip_length_access.tavern.yaml +++ b/robot-server/tests/integration/test_tip_length_access.tavern.yaml @@ -148,14 +148,14 @@ marks: *cal_marks stages: - name: DELETE request with correct pipette AND tiprack request: - url: "{ot2_server_base_url}/calibration/tip_length?pipette_id=321&tiprack_hash=130e17bb7b2f0c0472dcc01c1ff6f600ca1a6f9f86a90982df56c4bf43776824" + url: "{ot2_server_base_url}/calibration/tip_length?pipette_id=321&tiprack_uri=opentrons/opentrons_96_filtertiprack_200ul/1" method: DELETE response: status_code: 200 - name: DELETE request with incorrect pipette AND tiprack request: - url: "{ot2_server_base_url}/calibration/tip_length?pipette_id=321&tiprack_hash=wronghash" + url: "{ot2_server_base_url}/calibration/tip_length?pipette_id=321&tiprack_uri=wronguri" method: DELETE response: status_code: 404 diff --git a/robot-server/tests/service/tip_length/test_tip_length_management.py b/robot-server/tests/service/tip_length/test_tip_length_management.py index 628e6b0df29..1103e0c2703 100644 --- a/robot-server/tests/service/tip_length/test_tip_length_management.py +++ b/robot-server/tests/service/tip_length/test_tip_length_management.py @@ -1,5 +1,6 @@ PIPETTE_ID = "123" LW_HASH = "130e17bb7b2f0c0472dcc01c1ff6f600ca1a6f9f86a90982df56c4bf43776824" +LW_URI = "opentrons/opentrons_96_filtertiprack_200ul/1" FAKE_PIPETTE_ID = "fake_pip" WRONG_LW_HASH = "wronghash" @@ -32,12 +33,10 @@ def test_access_tip_length_calibration(api_client, set_up_tip_length_temp_direct assert resp.json()["data"] == [] -def test_delete_tip_length_calibration( - api_client, set_up_pipette_offset_temp_directory -): +def test_delete_tip_length_calibration(api_client, set_up_tip_length_temp_directory): resp = api_client.delete( f"/calibration/tip_length?pipette_id={FAKE_PIPETTE_ID}&" - f"tiprack_hash={WRONG_LW_HASH}" + f"tiprack_uri={WRONG_LW_HASH}" ) assert resp.status_code == 404 body = resp.json() @@ -53,7 +52,19 @@ def test_delete_tip_length_calibration( ] } + resp = api_client.get( + f"/calibration/tip_length?pipette_id={PIPETTE_ID}&" f"tiprack_uri={LW_URI}" + ) + assert resp.status_code == 200 + assert resp.json()["data"][0]["uri"] == LW_URI + resp = api_client.delete( - f"/calibration/tip_length?pipette_id={PIPETTE_ID}&" f"tiprack_hash={LW_HASH}" + f"/calibration/tip_length?pipette_id={PIPETTE_ID}&" f"tiprack_uri={LW_URI}" ) assert resp.status_code == 200 + + resp = api_client.get( + f"/calibration/tip_length?pipette_id={PIPETTE_ID}&" f"tiprack_uri={LW_URI}" + ) + assert resp.status_code == 200 + assert resp.json()["data"] == []