From 71ae62eb0bafffdc65d153511e255765030dd173 Mon Sep 17 00:00:00 2001 From: Andy Sigler Date: Thu, 28 Mar 2024 11:56:13 -0400 Subject: [PATCH 001/194] chore(shared-data): Add definition for 96ch v3.6 with 3.0mm backlash (#14721) ncreased backlash compensation improves low-volume performance. NOTE: accuracy functions are copied over from v3.5 definition, will add new v3.6 functions in a follow-up PR estimated to be ready around April 3-8 when the data is ready --- .../definitions/1/pipetteModelSpecs.json | 169 ++++++++++ .../general/ninety_six_channel/p1000/3_6.json | 87 ++++++ .../ninety_six_channel/p1000/3_6.json | 295 ++++++++++++++++++ .../ninety_six_channel/p1000/default/3_6.json | 188 +++++++++++ 4 files changed, 739 insertions(+) create mode 100644 shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_6.json create mode 100644 shared-data/pipette/definitions/2/geometry/ninety_six_channel/p1000/3_6.json create mode 100644 shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/3_6.json diff --git a/shared-data/pipette/definitions/1/pipetteModelSpecs.json b/shared-data/pipette/definitions/1/pipetteModelSpecs.json index 7a039c0e33f..c6367e851b4 100644 --- a/shared-data/pipette/definitions/1/pipetteModelSpecs.json +++ b/shared-data/pipette/definitions/1/pipetteModelSpecs.json @@ -10304,6 +10304,175 @@ "quirks": [], "returnTipHeight": 0.83, "idleCurrent": 0.3 + }, + "p1000_96_v3.6": { + "name": "p1000_96", + "backCompatNames": [], + "top": { + "value": 0.5, + "min": 0, + "max": 45, + "units": "mm", + "type": "float" + }, + "bottom": { + "value": 71.5, + "min": 55, + "max": 80, + "type": "float", + "units": "mm" + }, + "blowout": { + "value": 76.5, + "min": 60, + "max": 85, + "units": "mm", + "type": "float" + }, + "dropTip": { + "value": 92.5, + "min": 78, + "max": 119, + "units": "mm", + "type": "float" + }, + "pickUpCurrent": { + "value": 0.5, + "min": 0.05, + "max": 2.0, + "units": "amps", + "type": "float" + }, + "pickUpDistance": { + "value": 13, + "min": 1, + "max": 30, + "units": "mm", + "type": "float" + }, + "pickUpIncrement": { + "value": 0.0, + "min": 0.0, + "max": 10.0, + "units": "mm", + "type": "float" + }, + "pickUpPresses": { + "value": 1, + "min": 0, + "max": 10, + "units": "presses", + "type": "int" + }, + "pickUpSpeed": { + "value": 10, + "min": 1, + "max": 30, + "units": "mm/s", + "type": "float" + }, + "nozzleOffset": [-8.0, -16.0, -259.15], + "modelOffset": [0.0, 0.0, 25.14], + "ulPerMm": [ + { + "aspirate": [ + [0.6464, 0.4817, 0.0427], + [1.0889, 0.2539, 0.1591], + [1.5136, 0.1624, 0.2587], + [1.9108, 0.1042, 0.3467], + [2.2941, 0.0719, 0.4085], + [2.9978, 0.037, 0.4886], + [3.7731, 0.0378, 0.4863], + [4.7575, 0.0516, 0.4342], + [5.5024, 0.011, 0.6275], + [6.2686, 0.0114, 0.6253], + [7.005, 0.0054, 0.6625], + [8.5207, 0.0063, 0.6563], + [10.0034, 0.003, 0.6844], + [11.5075, 0.0031, 0.6833], + [13.0327, 0.0032, 0.6829], + [14.5356, 0.0018, 0.7003], + [17.5447, 0.0014, 0.7063], + [20.5576, 0.0011, 0.7126], + [23.5624, 0.0007, 0.7197], + [26.5785, 0.0007, 0.721], + [29.593, 0.0005, 0.7248], + [32.6109, 0.0004, 0.7268], + [35.6384, 0.0004, 0.727], + [38.6439, 0.0002, 0.7343], + [41.6815, 0.0004, 0.7284], + [44.6895, 0.0002, 0.7372], + [47.6926, 0.0001, 0.7393], + [51.4567, 0.0001, 0.7382] + ], + + "dispense": [ + [0.6464, 0.4817, 0.0427], + [1.0889, 0.2539, 0.1591], + [1.5136, 0.1624, 0.2587], + [1.9108, 0.1042, 0.3467], + [2.2941, 0.0719, 0.4085], + [2.9978, 0.037, 0.4886], + [3.7731, 0.0378, 0.4863], + [4.7575, 0.0516, 0.4342], + [5.5024, 0.011, 0.6275], + [6.2686, 0.0114, 0.6253], + [7.005, 0.0054, 0.6625], + [8.5207, 0.0063, 0.6563], + [10.0034, 0.003, 0.6844], + [11.5075, 0.0031, 0.6833], + [13.0327, 0.0032, 0.6829], + [14.5356, 0.0018, 0.7003], + [17.5447, 0.0014, 0.7063], + [20.5576, 0.0011, 0.7126], + [23.5624, 0.0007, 0.7197], + [26.5785, 0.0007, 0.721], + [29.593, 0.0005, 0.7248], + [32.6109, 0.0004, 0.7268], + [35.6384, 0.0004, 0.727], + [38.6439, 0.0002, 0.7343], + [41.6815, 0.0004, 0.7284], + [44.6895, 0.0002, 0.7372], + [47.6926, 0.0001, 0.7393], + [51.4567, 0.0001, 0.7382] + ] + } + ], + "plungerCurrent": { + "value": 1, + "min": 0.1, + "max": 1.5, + "units": "amps", + "type": "float" + }, + "dropTipCurrent": { + "value": 1, + "min": 0.1, + "max": 1.25, + "units": "amps", + "type": "float" + }, + "dropTipSpeed": { + "value": 10, + "min": 0.001, + "max": 30, + "units": "mm/sec", + "type": "float" + }, + "tipOverlap": { + "default": 10.5, + "opentrons/opentrons_96_tiprack_50ul/1": 10.5 + }, + "tipLength": { + "value": 78.3, + "units": "mm", + "type": "float", + "min": 0, + "max": 100 + }, + "quirks": [], + "returnTipHeight": 0.83, + "idleCurrent": 0.3 } }, "mutableConfigs": [ diff --git a/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_6.json b/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_6.json new file mode 100644 index 00000000000..a00dce8ef17 --- /dev/null +++ b/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_6.json @@ -0,0 +1,87 @@ +{ + "$otSharedSchema": "#/pipette/schemas/2/pipettePropertiesSchema.json", + "displayName": "Flex 96-Channel 1000 μL", + "model": "p1000", + "displayCategory": "FLEX", + "pickUpTipConfigurations": { + "pressFit": { + "presses": 1, + "increment": 0.0, + "speed": 10.0, + "distance": 13.0, + "currentByTipCount": { + "1": 0.2, + "2": 0.25, + "3": 0.3, + "4": 0.35, + "5": 0.4, + "6": 0.45, + "7": 0.5, + "8": 0.55, + "12": 0.19, + "16": 0.25, + "24": 0.38, + "48": 0.75 + } + }, + "camAction": { + "speed": 5.5, + "distance": 10.0, + "prep_move_distance": 8.25, + "prep_move_speed": 10.0, + "connectTiprackDistanceMM": 7.0, + "currentByTipCount": { + "96": 1.5 + } + } + }, + "dropTipConfigurations": { + "camAction": { + "current": 1.5, + "speed": 5.5, + "distance": 10.8, + "prep_move_distance": 19.0, + "prep_move_speed": 10.0 + } + }, + "plungerMotorConfigurations": { + "idle": 0.3, + "run": 0.8 + }, + "plungerPositionsConfigurations": { + "default": { + "top": 0.5, + "bottom": 68.5, + "blowout": 73.5, + "drop": 80 + } + }, + "availableSensors": { + "sensors": ["pressure", "capacitive", "environment"], + "pressure": { + "count": 2 + }, + "capacitive": { + "count": 2 + }, + "environment": { + "count": 1 + } + }, + "partialTipConfigurations": { + "partialTipSupported": true, + "availableConfigurations": [1, 8, 12, 16, 24, 48, 96] + }, + "backCompatNames": [], + "channels": 96, + "shaftDiameter": 4.5, + "shaftULperMM": 15.904, + "backlashDistance": 3.0, + "quirks": [], + "plungerHomingConfigurations": { + "current": 0.8, + "speed": 5 + }, + "tipPresenceCheckDistanceMM": 8.0, + "endTipActionRetractDistanceMM": 2.0 +} diff --git a/shared-data/pipette/definitions/2/geometry/ninety_six_channel/p1000/3_6.json b/shared-data/pipette/definitions/2/geometry/ninety_six_channel/p1000/3_6.json new file mode 100644 index 00000000000..da209a72907 --- /dev/null +++ b/shared-data/pipette/definitions/2/geometry/ninety_six_channel/p1000/3_6.json @@ -0,0 +1,295 @@ +{ + "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", + "pathTo3D": "pipette/definitions/2/geometry/ninety_six_channel/p1000/placeholder.gltf", + "nozzleOffset": [-36.0, -25.5, -259.15], + "pipetteBoundingBoxOffsets": { + "backLeftCorner": [-67.0, -3.5, -259.15], + "frontRightCorner": [94.0, -113.0, -259.15] + }, + "orderedRows": [ + { + "key": "A", + "orderedNozzles": [ + "A1", + "A2", + "A3", + "A4", + "A5", + "A6", + "A7", + "A8", + "A9", + "A10", + "A11", + "A12" + ] + }, + { + "key": "B", + "orderedNozzles": [ + "B1", + "B2", + "B3", + "B4", + "B5", + "B6", + "B7", + "B8", + "B9", + "B10", + "B11", + "B12" + ] + }, + { + "key": "C", + "orderedNozzles": [ + "C1", + "C2", + "C3", + "C4", + "C5", + "C6", + "C7", + "C8", + "C9", + "C10", + "C11", + "C12" + ] + }, + { + "key": "D", + "orderedNozzles": [ + "D1", + "D2", + "D3", + "D4", + "D5", + "D6", + "D7", + "D8", + "D9", + "D10", + "D11", + "D12" + ] + }, + { + "key": "E", + "orderedNozzles": [ + "E1", + "E2", + "E3", + "E4", + "E5", + "E6", + "E7", + "E8", + "E9", + "E10", + "E11", + "E12" + ] + }, + { + "key": "F", + "orderedNozzles": [ + "F1", + "F2", + "F3", + "F4", + "F5", + "F6", + "F7", + "F8", + "F9", + "F10", + "F11", + "F12" + ] + }, + { + "key": "G", + "orderedNozzles": [ + "G1", + "G2", + "G3", + "G4", + "G5", + "G6", + "G7", + "G8", + "G9", + "G10", + "G11", + "G12" + ] + }, + { + "key": "H", + "orderedNozzles": [ + "H1", + "H2", + "H3", + "H4", + "H5", + "H6", + "H7", + "H8", + "H9", + "H10", + "H11", + "H12" + ] + } + ], + "orderedColumns": [ + { + "key": "1", + "orderedNozzles": ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"] + }, + { + "key": "2", + "orderedNozzles": ["A2", "B2", "C2", "D2", "E2", "F2", "G2", "H2"] + }, + { + "key": "3", + "orderedNozzles": ["A3", "B3", "C3", "D3", "E3", "F3", "G3", "H3"] + }, + { + "key": "4", + "orderedNozzles": ["A4", "B4", "C4", "D4", "E4", "F4", "G4", "H4"] + }, + { + "key": "5", + "orderedNozzles": ["A5", "B5", "C5", "D5", "E5", "F5", "G5", "H5"] + }, + { + "key": "6", + "orderedNozzles": ["A6", "B6", "C6", "D6", "E6", "F6", "G6", "H6"] + }, + { + "key": "7", + "orderedNozzles": ["A7", "B7", "C7", "D7", "E7", "F7", "G7", "H7"] + }, + { + "key": "8", + "orderedNozzles": ["A8", "B8", "C8", "D8", "E8", "F8", "G8", "H8"] + }, + { + "key": "9", + "orderedNozzles": ["A9", "B9", "C9", "D9", "E9", "F9", "G9", "H9"] + }, + { + "key": "10", + "orderedNozzles": ["A10", "B10", "C10", "D10", "E10", "F10", "G10", "H10"] + }, + { + "key": "11", + "orderedNozzles": ["A11", "B11", "C11", "D11", "E11", "F11", "G11", "H11"] + }, + { + "key": "12", + "orderedNozzles": ["A12", "B12", "C12", "D12", "E12", "F12", "G12", "H12"] + } + ], + "nozzleMap": { + "A1": [-36.0, -25.5, -259.15], + "A2": [-27.0, -25.5, -259.15], + "A3": [-18.0, -25.5, -259.15], + "A4": [-9.0, -25.5, -259.15], + "A5": [0.0, -25.5, -259.15], + "A6": [9.0, -25.5, -259.15], + "A7": [18.0, -25.5, -259.15], + "A8": [27.0, -25.5, -259.15], + "A9": [36.0, -25.5, -259.15], + "A10": [45.0, -25.5, -259.15], + "A11": [54.0, -25.5, -259.15], + "A12": [63.0, -25.5, -259.15], + "B1": [-36.0, -34.5, -259.15], + "B2": [-27.0, -34.5, -259.15], + "B3": [-18.0, -34.5, -259.15], + "B4": [-9.0, -34.5, -259.15], + "B5": [0.0, -34.5, -259.15], + "B6": [9.0, -34.5, -259.15], + "B7": [18.0, -34.5, -259.15], + "B8": [27.0, -34.5, -259.15], + "B9": [36.0, -34.5, -259.15], + "B10": [45.0, -34.5, -259.15], + "B11": [54.0, -34.5, -259.15], + "B12": [63.0, -34.5, -259.15], + "C1": [-36.0, -43.5, -259.15], + "C2": [-27.0, -43.5, -259.15], + "C3": [-18.0, -43.5, -259.15], + "C4": [-9.0, -43.5, -259.15], + "C5": [0.0, -43.5, -259.15], + "C6": [9.0, -43.5, -259.15], + "C7": [18.0, -43.5, -259.15], + "C8": [27.0, -43.5, -259.15], + "C9": [36.0, -43.5, -259.15], + "C10": [45.0, -43.5, -259.15], + "C11": [54.0, -43.5, -259.15], + "C12": [63.0, -43.5, -259.15], + "D1": [-36.0, -52.5, -259.15], + "D2": [-27.0, -52.5, -259.15], + "D3": [-18.0, -52.5, -259.15], + "D4": [-9.0, -52.5, -259.15], + "D5": [0.0, -52.5, -259.15], + "D6": [9.0, -52.5, -259.15], + "D7": [18.0, -52.5, -259.15], + "D8": [27.0, -52.5, -259.15], + "D9": [36.0, -52.5, -259.15], + "D10": [45.0, -52.5, -259.15], + "D11": [54.0, -52.5, -259.15], + "D12": [63.0, -52.5, -259.15], + "E1": [-36.0, -61.5, -259.15], + "E2": [-27.0, -61.5, -259.15], + "E3": [-18.0, -61.5, -259.15], + "E4": [-9.0, -61.5, -259.15], + "E5": [0.0, -61.5, -259.15], + "E6": [9.0, -61.5, -259.15], + "E7": [18.0, -61.5, -259.15], + "E8": [27.0, -61.5, -259.15], + "E9": [36.0, -61.5, -259.15], + "E10": [45.0, -61.5, -259.15], + "E11": [54.0, -61.5, -259.15], + "E12": [63.0, -61.5, -259.15], + "F1": [-36.0, -70.5, -259.15], + "F2": [-27.0, -70.5, -259.15], + "F3": [-18.0, -70.5, -259.15], + "F4": [-9.0, -70.5, -259.15], + "F5": [0.0, -70.5, -259.15], + "F6": [9.0, -70.5, -259.15], + "F7": [18.0, -70.5, -259.15], + "F8": [27.0, -70.5, -259.15], + "F9": [36.0, -70.5, -259.15], + "F10": [45.0, -70.5, -259.15], + "F11": [54.0, -70.5, -259.15], + "F12": [63.0, -70.5, -259.15], + "G1": [-36.0, -79.5, -259.15], + "G2": [-27.0, -79.5, -259.15], + "G3": [-18.0, -79.5, -259.15], + "G4": [-9.0, -79.5, -259.15], + "G5": [0.0, -79.5, -259.15], + "G6": [9.0, -79.5, -259.15], + "G7": [18.0, -79.5, -259.15], + "G8": [27.0, -79.5, -259.15], + "G9": [36.0, -79.5, -259.15], + "G10": [45.0, -79.5, -259.15], + "G11": [54.0, -79.5, -259.15], + "G12": [63.0, -79.5, -259.15], + "H1": [-36.0, -88.5, -259.15], + "H2": [-27.0, -88.5, -259.15], + "H3": [-18.0, -88.5, -259.15], + "H4": [-9.0, -88.5, -259.15], + "H5": [0.0, -88.5, -259.15], + "H6": [9.0, -88.5, -259.15], + "H7": [18.0, -88.5, -259.15], + "H8": [27.0, -88.5, -259.15], + "H9": [36.0, -88.5, -259.15], + "H10": [45.0, -88.5, -259.15], + "H11": [54.0, -88.5, -259.15], + "H12": [63.0, -88.5, -259.15] + } +} diff --git a/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/3_6.json b/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/3_6.json new file mode 100644 index 00000000000..8ca9dc4ece4 --- /dev/null +++ b/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/3_6.json @@ -0,0 +1,188 @@ +{ + "$otSharedSchema": "#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json", + "supportedTips": { + "t50": { + "defaultAspirateFlowRate": { + "default": 6, + "valuesByApiLevel": { "2.14": 6 } + }, + "defaultDispenseFlowRate": { + "default": 6, + "valuesByApiLevel": { "2.14": 6 } + }, + "defaultBlowOutFlowRate": { + "default": 80, + "valuesByApiLevel": { "2.14": 80 } + }, + "defaultFlowAcceleration": 16000.0, + "defaultTipLength": 57.9, + "defaultReturnTipHeight": 0.2, + "aspirate": { + "default": { + "1": [ + [1.933333, 2.844459, 4.750159], + [2.833333, 1.12901, 8.066694], + [3.603333, 0.254744, 10.543779], + [4.836667, 1.101839, 7.491414], + [5.755, 0.277649, 11.47775], + [6.643333, 0.14813, 12.223126], + [7.548333, 0.145635, 12.239705], + [8.475, 0.15097, 12.199433], + [13.02, 0.071736, 12.870946], + [22.318333, 0.042305, 13.254131], + [36.463333, 0.021195, 13.725284], + [54.82, 0.001805, 14.43229] + ] + } + }, + "dispense": { + "default": { + "1": [ + [1.933333, 2.844459, 4.750159], + [2.833333, 1.12901, 8.066694], + [3.603333, 0.254744, 10.543779], + [4.836667, 1.101839, 7.491414], + [5.755, 0.277649, 11.47775], + [6.643333, 0.14813, 12.223126], + [7.548333, 0.145635, 12.239705], + [8.475, 0.15097, 12.199433], + [13.02, 0.071736, 12.870946], + [22.318333, 0.042305, 13.254131], + [36.463333, 0.021195, 13.725284], + [54.82, 0.001805, 14.43229] + ] + } + }, + "defaultPushOutVolume": 7 + }, + "t200": { + "defaultAspirateFlowRate": { + "default": 80, + "valuesByApiLevel": { "2.14": 80 } + }, + "defaultDispenseFlowRate": { + "default": 80, + "valuesByApiLevel": { "2.14": 80 } + }, + "defaultBlowOutFlowRate": { + "default": 80, + "valuesByApiLevel": { "2.14": 80 } + }, + "defaultFlowAcceleration": 16000.0, + "defaultTipLength": 58.35, + "defaultReturnTipHeight": 0.2, + "aspirate": { + "default": { + "1": [ + [1.39875, 4.681865, 0.866627], + [2.5225, 2.326382, 4.161359], + [3.625, 1.361424, 6.595466], + [4.69125, 0.848354, 8.455342], + [5.705, 0.519685, 9.997214], + [6.70625, 0.36981, 10.852249], + [7.69375, 0.267029, 11.541523], + [8.67875, 0.210129, 11.979299], + [47.05, 0.030309, 13.539909], + [95.24375, 0.003774, 14.78837], + [211.0225, 0.000928, 15.059476] + ] + } + }, + "dispense": { + "default": { + "1": [ + [1.39875, 4.681865, 0.866627], + [2.5225, 2.326382, 4.161359], + [3.625, 1.361424, 6.595466], + [4.69125, 0.848354, 8.455342], + [5.705, 0.519685, 9.997214], + [6.70625, 0.36981, 10.852249], + [7.69375, 0.267029, 11.541523], + [8.67875, 0.210129, 11.979299], + [47.05, 0.030309, 13.539909], + [95.24375, 0.003774, 14.78837], + [211.0225, 0.000928, 15.059476] + ] + } + }, + "defaultPushOutVolume": 5 + }, + "t1000": { + "defaultAspirateFlowRate": { + "default": 160, + "valuesByApiLevel": { "2.14": 160 } + }, + "defaultDispenseFlowRate": { + "default": 160, + "valuesByApiLevel": { "2.14": 160 } + }, + "defaultBlowOutFlowRate": { + "default": 80, + "valuesByApiLevel": { "2.14": 80 } + }, + "defaultFlowAcceleration": 16000.0, + "defaultTipLength": 95.6, + "defaultReturnTipHeight": 0.2, + "aspirate": { + "default": { + "1": [ + [3.76, 2.041301, 4.284751], + [5.684286, 0.49624, 10.09418], + [8.445714, 0.187358, 11.849952], + [12.981429, 0.073135, 12.814653], + [17.667143, 0.060853, 12.974083], + [46.515714, 0.025888, 13.591828], + [95.032857, 0.006561, 14.490827], + [114.488571, 0.00306, 14.823556], + [192.228571, 0.001447, 15.00822], + [309.921429, 0.000995, 15.095087], + [436.984286, 0.000322, 15.303634], + [632.861429, 0.000208, 15.353582], + [828.952857, 0.00013, 15.402544], + [976.118571, 0.000095, 15.431673], + [1005.275714, -0.000067, 15.589843], + [1024.768571, -0.000021, 15.543681], + [1049.145714, -0.000013, 15.535884] + ] + } + }, + "dispense": { + "default": { + "1": [ + [3.76, 2.041301, 4.284751], + [5.684286, 0.49624, 10.09418], + [8.445714, 0.187358, 11.849952], + [12.981429, 0.073135, 12.814653], + [17.667143, 0.060853, 12.974083], + [46.515714, 0.025888, 13.591828], + [95.032857, 0.006561, 14.490827], + [114.488571, 0.00306, 14.823556], + [192.228571, 0.001447, 15.00822], + [309.921429, 0.000995, 15.095087], + [436.984286, 0.000322, 15.303634], + [632.861429, 0.000208, 15.353582], + [828.952857, 0.00013, 15.402544], + [976.118571, 0.000095, 15.431673], + [1005.275714, -0.000067, 15.589843], + [1024.768571, -0.000021, 15.543681], + [1049.145714, -0.000013, 15.535884] + ] + } + }, + "defaultPushOutVolume": 20 + } + }, + "defaultTipOverlapDictionary": { + "default": 10.5, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.5, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.5, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 10.5 + }, + "maxVolume": 1000, + "minVolume": 5, + "defaultTipracks": [ + "opentrons/opentrons_flex_96_tiprack_1000ul/1", + "opentrons/opentrons_flex_96_tiprack_200ul/1", + "opentrons/opentrons_flex_96_tiprack_50ul/1" + ] +} From 4d3b566766cde289dafd694a0a83a8e560d348cd Mon Sep 17 00:00:00 2001 From: Ed Cormany Date: Thu, 28 Mar 2024 13:56:43 -0400 Subject: [PATCH 002/194] chore(release): 7.2.2 release notes (#14747) # Overview 7.2.2 hotfix release notes. # Changelog - Describes #14721 - No others so far # Review requests Anything else of note going into this release? # Risk assessment nil --- api/release-notes.md | 10 ++++++++++ app-shell/build/release-notes.md | 8 ++++++++ 2 files changed, 18 insertions(+) diff --git a/api/release-notes.md b/api/release-notes.md index ff193247459..a1db0c0e1f3 100644 --- a/api/release-notes.md +++ b/api/release-notes.md @@ -6,6 +6,16 @@ log][]. For a list of currently known issues, please see the [Opentrons issue tr --- +## Opentrons Robot Software Changes in 7.2.2 + +Welcome to the v7.2.2 release of the Opentrons robot software! + +### Improved Features + +- Improved the low-volume performance of recently produced Flex 96-Channel Pipettes. + +--- + ## Opentrons Robot Software Changes in 7.2.1 Welcome to the v7.2.1 release of the Opentrons robot software! diff --git a/app-shell/build/release-notes.md b/app-shell/build/release-notes.md index 97fa5f01b81..43db1bdfaf8 100644 --- a/app-shell/build/release-notes.md +++ b/app-shell/build/release-notes.md @@ -6,6 +6,14 @@ log][]. For a list of currently known issues, please see the [Opentrons issue tr --- +## Opentrons App Changes in 7.2.2 + +Welcome to the v7.2.2 release of the Opentrons App! + +There are no changes to the Opentrons App in v7.2.2, but it is required for updating the robot software to improve some features. + +--- + ## Opentrons App Changes in 7.2.1 Welcome to the v7.2.1 release of the Opentrons App! From 02b07d1039fa49ba6a848b6cc4bbb2fbad8601f1 Mon Sep 17 00:00:00 2001 From: Jethary Rader <66035149+jerader@users.noreply.github.com> Date: Fri, 29 Mar 2024 11:40:04 -0400 Subject: [PATCH 003/194] feat(step-generation): blowOut emits before touchTip (#14727) closes AUTH-3, RAUT-581 --- .../protocol/8/example_1_1_0MigratedToV8.json | 552 +++++++++--------- .../src/__tests__/consolidate.test.ts | 73 ++- .../src/__tests__/transfer.test.ts | 162 ++--- .../commandCreators/compound/consolidate.ts | 2 +- .../src/commandCreators/compound/transfer.ts | 2 +- 5 files changed, 394 insertions(+), 397 deletions(-) diff --git a/protocol-designer/fixtures/protocol/8/example_1_1_0MigratedToV8.json b/protocol-designer/fixtures/protocol/8/example_1_1_0MigratedToV8.json index 1beae49e74e..531adb047e9 100644 --- a/protocol-designer/fixtures/protocol/8/example_1_1_0MigratedToV8.json +++ b/protocol-designer/fixtures/protocol/8/example_1_1_0MigratedToV8.json @@ -6,7 +6,7 @@ "author": "Author name", "description": "Description here", "created": 1560957631666, - "lastModified": 1709309281554, + "lastModified": 1711650670235, "category": null, "subcategory": null, "tags": [] @@ -15,7 +15,7 @@ "name": "opentrons/protocol-designer", "version": "8.1.0", "data": { - "_internalAppBuildDate": "Fri, 01 Mar 2024 16:07:10 GMT", + "_internalAppBuildDate": "Thu, 28 Mar 2024 18:30:23 GMT", "defaultValues": { "aspirate_mmFromBottom": 1, "dispense_mmFromBottom": 0.5, @@ -114,7 +114,7 @@ "disposalVolume_checkbox": true, "disposalVolume_volume": "1", "blowout_checkbox": true, - "blowout_location": "d3181bae-ad9c-4c89-9df2-afb2d4ebc94d:trashBin", + "blowout_location": "a1a3a3ee-84f5-44f2-b6c5-015be69c0208:trashBin", "preWetTip": false, "aspirate_airGap_checkbox": false, "aspirate_airGap_volume": null, @@ -126,7 +126,7 @@ "dispense_delay_checkbox": false, "dispense_delay_seconds": "1", "dispense_delay_mmFromBottom": "0.5", - "dropTip_location": "d3181bae-ad9c-4c89-9df2-afb2d4ebc94d:trashBin", + "dropTip_location": "a1a3a3ee-84f5-44f2-b6c5-015be69c0208:trashBin", "nozzles": null, "id": "e7d36200-92a5-11e9-ac62-1b173f839d9e", "stepType": "moveLiquid", @@ -153,7 +153,7 @@ "dispense_delay_seconds": "1", "mix_touchTip_checkbox": true, "mix_touchTip_mmFromBottom": 30.5, - "dropTip_location": "d3181bae-ad9c-4c89-9df2-afb2d4ebc94d:trashBin", + "dropTip_location": "a1a3a3ee-84f5-44f2-b6c5-015be69c0208:trashBin", "nozzles": null, "tipRack": "c6f4ec70-92a5-11e9-ac62-1b173f839d9e:tiprack-10ul", "id": "18113c80-92a6-11e9-ac62-1b173f839d9e", @@ -3336,7 +3336,7 @@ "commandSchemaId": "opentronsCommandSchemaV8", "commands": [ { - "key": "da14f3fe-db58-4e04-b97e-9d3edc5ab33e", + "key": "6f4d2d94-4cab-4ead-9827-36a729f06652", "commandType": "loadPipette", "params": { "pipetteName": "p10_single", @@ -3345,7 +3345,7 @@ } }, { - "key": "58ea5ab7-32ea-4923-ae20-e0c91f1d8b3e", + "key": "fc0d5cb8-d53b-4629-abf6-b0935b8b4812", "commandType": "loadPipette", "params": { "pipetteName": "p50_single", @@ -3354,7 +3354,7 @@ } }, { - "key": "8f8828b7-6a4a-4762-873f-96331ea194ba", + "key": "a7c0b1ac-b2c6-4e2a-9e4a-b6a7787b48f9", "commandType": "loadLabware", "params": { "displayName": "tiprack 10ul (1)", @@ -3366,7 +3366,7 @@ } }, { - "key": "919d5eab-85ee-4129-89e2-5fcc8419c81a", + "key": "55d92dea-5339-4f0b-b771-fb1089f281ed", "commandType": "loadLabware", "params": { "displayName": "tiprack 200ul (1)", @@ -3378,7 +3378,7 @@ } }, { - "key": "4f7eef41-f93b-4a93-ac00-dd533553390b", + "key": "bfdd6d43-a127-48a5-9bd2-0e2693edf78e", "commandType": "loadLabware", "params": { "displayName": "96 deep well (1)", @@ -3391,7 +3391,7 @@ }, { "commandType": "loadLiquid", - "key": "9713cecc-3e57-49e8-85cf-5122cdaf00c8", + "key": "73a8dbd7-47e8-441a-8c8f-1dbeee57241d", "params": { "liquidId": "1", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -3400,7 +3400,7 @@ }, { "commandType": "loadLiquid", - "key": "edbcfbc3-e074-4df6-b637-245f3b5f9fb6", + "key": "c1c5b6cf-8bb5-49a0-b887-2a4b0cddfefc", "params": { "liquidId": "0", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -3415,7 +3415,7 @@ }, { "commandType": "pickUpTip", - "key": "6113c2d3-43ef-4412-9800-7659de75d37a", + "key": "ee2227a1-11d2-447c-b97b-9079725370ca", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "c6f4ec70-92a5-11e9-ac62-1b173f839d9e:tiprack-10ul", @@ -3424,7 +3424,7 @@ }, { "commandType": "aspirate", - "key": "06b603b3-104e-454f-83b8-7a3dbcfac8b4", + "key": "7dccc871-ae50-4281-a55d-71628dd2475d", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -3436,7 +3436,7 @@ }, { "commandType": "dispense", - "key": "c77041f4-0e07-46ff-81a4-12c40f7396f6", + "key": "4b42680f-634b-47b5-95b9-293d73ef6f4a", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -3448,7 +3448,7 @@ }, { "commandType": "aspirate", - "key": "56101ed9-70d5-4ce3-8380-0559ddc847df", + "key": "c3a5efec-5dfc-41ce-98c2-983f31ca659d", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -3460,7 +3460,7 @@ }, { "commandType": "dispense", - "key": "86d596a4-4023-4f56-920a-021924edbcfa", + "key": "5ce878dc-f20f-40fc-89d0-8b5551028f5a", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -3472,7 +3472,7 @@ }, { "commandType": "aspirate", - "key": "1f34249b-ed7b-498c-9fc3-e8b1e5254fe4", + "key": "7ee113bc-9d41-470d-a909-bffb2510d00f", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -3484,7 +3484,7 @@ }, { "commandType": "dispense", - "key": "18cab41b-1f94-4efe-a931-bbc5a1f4d2e8", + "key": "24cf716a-a105-4817-838c-817755dbb986", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -3496,7 +3496,7 @@ }, { "commandType": "aspirate", - "key": "249922dd-6e84-481a-9422-0b1f50a83e7c", + "key": "dbd9da16-cf9e-44ea-aabe-b6a0bf4f7a60", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, @@ -3508,7 +3508,7 @@ }, { "commandType": "touchTip", - "key": "3f553815-ec56-43df-bfe7-4fcaa2c51bb9", + "key": "7e8803d0-9788-4780-94fa-3a336747cb5a", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -3518,7 +3518,7 @@ }, { "commandType": "dispense", - "key": "48a46cfd-1569-4580-be11-f1b919e10528", + "key": "7af6bccd-f70d-40fd-9026-146d24f45606", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, @@ -3530,7 +3530,7 @@ }, { "commandType": "aspirate", - "key": "5772dac8-ab61-44ac-883b-b4a5d97a7c9a", + "key": "a2b37a9d-35ee-4151-ae60-221144efeaf9", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -3542,7 +3542,7 @@ }, { "commandType": "dispense", - "key": "2ec5b554-c0ad-498e-b71a-70985440b4d5", + "key": "85d9b492-381f-4412-b9de-9343fabd06e2", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -3554,7 +3554,7 @@ }, { "commandType": "aspirate", - "key": "116d9aaa-b681-443e-a949-4f272868d031", + "key": "3eab4b4b-ff8f-4e99-a98e-0e8f181aaca1", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -3566,7 +3566,7 @@ }, { "commandType": "dispense", - "key": "02729f1d-ed43-4b0a-9dac-548a5d25b7b2", + "key": "83f8b202-581a-416b-863d-5cbc19d5cfdb", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -3576,19 +3576,9 @@ "flowRate": 10 } }, - { - "commandType": "touchTip", - "key": "9e1d3a6f-a85a-47db-8ea9-85a7426687f8", - "params": { - "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", - "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", - "wellName": "E8", - "wellLocation": { "origin": "bottom", "offset": { "z": 40.3 } } - } - }, { "commandType": "moveToAddressableArea", - "key": "16e37e6c-72d1-4cc9-8d60-967cf40defe3", + "key": "0f0d93dc-f745-46e5-a75d-7d939503d930", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -3597,15 +3587,25 @@ }, { "commandType": "blowOutInPlace", - "key": "7a7c4da7-4f95-49d7-b897-e64febe9879c", + "key": "d3468c15-f33f-4bb2-aed2-571abb2a0195", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "flowRate": 1000 } }, + { + "commandType": "touchTip", + "key": "243f8f03-d546-48f3-8641-439e79bfef83", + "params": { + "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", + "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", + "wellName": "E8", + "wellLocation": { "origin": "bottom", "offset": { "z": 40.3 } } + } + }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "93068973-6f5a-418c-8dbd-819c23cec732", + "key": "631c7562-faa3-4ee2-95d7-6bdbefaec4bb", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -3615,12 +3615,12 @@ }, { "commandType": "dropTipInPlace", - "key": "cbff3df7-e4d5-45c6-88bf-1819361578c2", + "key": "fc391196-ed70-44b5-ba11-8abae97462eb", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e" } }, { "commandType": "pickUpTip", - "key": "96e12b27-5cf4-4cdc-9d6d-6c7cc8e93796", + "key": "9e8eb31c-3e34-4281-aa18-5de6d0cd195e", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "c6f4ec70-92a5-11e9-ac62-1b173f839d9e:tiprack-10ul", @@ -3629,7 +3629,7 @@ }, { "commandType": "aspirate", - "key": "d1e6016b-4a1f-4728-acef-99b54b6716cb", + "key": "c5e81bdb-1992-40f7-869a-ff0325c199de", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -3641,7 +3641,7 @@ }, { "commandType": "dispense", - "key": "0f99777e-a204-4011-8f2c-a991440d57b0", + "key": "8c0865e8-0d42-4f15-8b70-845e5d9b45fa", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -3653,7 +3653,7 @@ }, { "commandType": "aspirate", - "key": "03a113dc-1617-48d4-8c9e-6e248a748727", + "key": "97c816e1-3045-4f09-bc33-150e256cde65", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -3665,7 +3665,7 @@ }, { "commandType": "dispense", - "key": "f1cb2096-a65d-4d00-9558-3d9d0869d9fe", + "key": "06d2d102-35f5-468d-b23c-900bd1df2789", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -3677,7 +3677,7 @@ }, { "commandType": "aspirate", - "key": "1df11cf1-eea7-4789-a177-41fd33acb76a", + "key": "7556aad7-86b4-4606-a5cc-5f7f7b56f0d9", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -3689,7 +3689,7 @@ }, { "commandType": "dispense", - "key": "c432ee2b-ff8a-4eb7-a2da-7d58b5b34567", + "key": "edbbed85-7ab1-4aad-a603-06654028c9d0", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -3701,7 +3701,7 @@ }, { "commandType": "aspirate", - "key": "51bc3818-c02f-4904-b501-e4ca399160d8", + "key": "6ccfd5ad-d683-48e6-a4db-fd911a6803be", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, @@ -3713,7 +3713,7 @@ }, { "commandType": "touchTip", - "key": "b57fbe11-7a9d-4e21-8315-bb6d59d7bfd4", + "key": "6b77c1fa-dbb6-4933-b04c-c043b8f183ac", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -3723,7 +3723,7 @@ }, { "commandType": "dispense", - "key": "a91f20ef-4880-4a83-8f84-a6da8bdb4950", + "key": "1ab3a31c-ee75-4918-a89c-443b6a160d9b", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, @@ -3735,7 +3735,7 @@ }, { "commandType": "aspirate", - "key": "ff8555b1-e3bb-4678-a90b-f5dd5fc3c513", + "key": "d38199b9-9ea1-4994-8124-af29d5bacd69", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -3747,7 +3747,7 @@ }, { "commandType": "dispense", - "key": "27068318-99da-452b-9ee8-5698a998b297", + "key": "39df2363-edb6-4b3f-9226-3e1e40f49a83", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -3759,7 +3759,7 @@ }, { "commandType": "aspirate", - "key": "7eb27547-3200-4030-bebb-f367b887ade4", + "key": "05046dbb-2bd5-4d5f-9029-592630619967", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -3771,7 +3771,7 @@ }, { "commandType": "dispense", - "key": "377193a3-3f10-4cda-8ea6-b0f32f211017", + "key": "04bd6a9a-012a-49cb-ba87-e96e3b42febc", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -3781,19 +3781,9 @@ "flowRate": 10 } }, - { - "commandType": "touchTip", - "key": "79e72c26-f013-49ff-bf88-7a3831d9bd91", - "params": { - "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", - "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", - "wellName": "D8", - "wellLocation": { "origin": "bottom", "offset": { "z": 40.3 } } - } - }, { "commandType": "moveToAddressableArea", - "key": "e61cf837-ec5a-4c04-abee-fcc16e429ca4", + "key": "4381a5c3-9f62-44f0-9030-cecbd7116762", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -3802,15 +3792,25 @@ }, { "commandType": "blowOutInPlace", - "key": "5347bceb-47e7-481e-ba78-a049ff87192b", + "key": "bca261f2-6071-4457-a47c-2bb76109e746", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "flowRate": 1000 } }, + { + "commandType": "touchTip", + "key": "5f923682-3cae-4d33-9dfc-29ac10adb4ae", + "params": { + "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", + "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", + "wellName": "D8", + "wellLocation": { "origin": "bottom", "offset": { "z": 40.3 } } + } + }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "d59e503f-f61f-474e-b2b2-daf6880ae0cd", + "key": "65c6a620-3fbb-41f0-b185-91c6fa6dbda6", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -3820,12 +3820,12 @@ }, { "commandType": "dropTipInPlace", - "key": "31533601-0084-4abe-b9e2-1628b134cd86", + "key": "d063d2b8-234c-4e38-b66a-85a4011cbf94", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e" } }, { "commandType": "pickUpTip", - "key": "6d29b61d-1df9-4366-8395-04d0a5286e83", + "key": "432b72d6-f0c8-4cea-8bc2-b98fdae69445", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "c6f4ec70-92a5-11e9-ac62-1b173f839d9e:tiprack-10ul", @@ -3834,7 +3834,7 @@ }, { "commandType": "aspirate", - "key": "32095f51-4943-4c29-96b4-f291bed0f26f", + "key": "62c975b5-3adc-4900-9119-a87d8f7098b6", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -3846,7 +3846,7 @@ }, { "commandType": "dispense", - "key": "c144dd6b-1dd0-4e9c-a170-09f948d0d6b5", + "key": "10139a39-fb4a-4080-88ca-ebe511cb2d56", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -3858,7 +3858,7 @@ }, { "commandType": "aspirate", - "key": "0c94efb7-d072-4d76-a9d9-2a2b2d79a5e1", + "key": "2bc81ba4-9b04-4e2c-88b7-f75f6c3dd3ec", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -3870,7 +3870,7 @@ }, { "commandType": "dispense", - "key": "00390b97-a020-4cf8-afe5-e09556ff5b8b", + "key": "bf7df5f4-1c18-46b7-b8f1-cc0853d1244a", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -3882,7 +3882,7 @@ }, { "commandType": "aspirate", - "key": "15509964-d974-41b5-979c-299295b3ea38", + "key": "b86464a1-ee5d-4fce-b073-f14730bff0aa", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -3894,7 +3894,7 @@ }, { "commandType": "dispense", - "key": "0eb459c1-44e4-4bcc-8089-220e81e81b6d", + "key": "9e4fb406-bf5e-4571-b4e5-dfb1ff8f2b98", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -3906,7 +3906,7 @@ }, { "commandType": "aspirate", - "key": "9c736eea-7d18-486c-98f0-377641a33f4d", + "key": "fb8ce4d1-79f9-4ddf-b11e-ebed2414333b", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, @@ -3918,7 +3918,7 @@ }, { "commandType": "touchTip", - "key": "9a209558-3ee7-4922-a173-c897a79679c3", + "key": "4be36f15-e8e3-4d6b-84b7-fe64db61ead3", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -3928,7 +3928,7 @@ }, { "commandType": "dispense", - "key": "7bf46386-39b4-4a31-82a8-f6433ea11856", + "key": "e3fdb442-d127-4b6d-8829-b688b55397a6", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, @@ -3940,7 +3940,7 @@ }, { "commandType": "aspirate", - "key": "3ae51fed-c35f-4a45-843e-b17cfba906e3", + "key": "55c1e1fa-78a6-4605-b6b5-8953cbbf7010", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -3952,7 +3952,7 @@ }, { "commandType": "dispense", - "key": "eec16e92-d503-4e00-ba67-4f0d0a8ebac1", + "key": "61620e17-2c1f-4a35-a64c-ef224b5b2a52", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -3964,7 +3964,7 @@ }, { "commandType": "aspirate", - "key": "48546952-3edc-4410-a5ef-fe0689a2780a", + "key": "39adc386-6ab8-4664-a0f4-5196f475e19f", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -3976,7 +3976,7 @@ }, { "commandType": "dispense", - "key": "4b9a4e01-514c-4677-b143-08709ac99a6f", + "key": "e5349e3a-6d1f-481a-8f37-6716b88d93a5", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -3986,19 +3986,9 @@ "flowRate": 10 } }, - { - "commandType": "touchTip", - "key": "20eb9f79-6e87-4cf7-b0dd-32c19ad43337", - "params": { - "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", - "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", - "wellName": "C8", - "wellLocation": { "origin": "bottom", "offset": { "z": 40.3 } } - } - }, { "commandType": "moveToAddressableArea", - "key": "dc0e2e7a-b286-483b-80c6-5a6a654013bf", + "key": "89ae9ed8-0d2c-4b64-af9c-cf0c7bda3fd9", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -4007,15 +3997,25 @@ }, { "commandType": "blowOutInPlace", - "key": "3a789643-0839-492b-a4c8-30a14db14c16", + "key": "6ac0f84b-1da9-41e7-a9e7-e5d7c5823077", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "flowRate": 1000 } }, + { + "commandType": "touchTip", + "key": "0e94a3f7-0bc1-42ea-bf18-b03b600ec548", + "params": { + "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", + "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", + "wellName": "C8", + "wellLocation": { "origin": "bottom", "offset": { "z": 40.3 } } + } + }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "11d69a61-0591-4f42-88af-3c74e7da0475", + "key": "4e04ac60-3844-4f1c-afcb-753d8efa8073", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -4025,12 +4025,12 @@ }, { "commandType": "dropTipInPlace", - "key": "ee65d091-f308-420b-9db9-fbcd28d17f4e", + "key": "9c27a051-f55a-4859-9ee0-12cb2e4cc127", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e" } }, { "commandType": "pickUpTip", - "key": "995d9fa9-55eb-4fa4-b6de-0131a0402975", + "key": "83b37a71-e721-4454-98ba-a0e4c3311b06", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "c6f4ec70-92a5-11e9-ac62-1b173f839d9e:tiprack-10ul", @@ -4039,7 +4039,7 @@ }, { "commandType": "aspirate", - "key": "aa4c8295-708f-4a5b-b879-505fb559032c", + "key": "193df488-664a-4f29-8d62-4165930cde80", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -4051,7 +4051,7 @@ }, { "commandType": "dispense", - "key": "5d947f6d-15d8-4918-a96c-38aa1198232c", + "key": "81ab7f7a-4c7b-4b74-9749-9cd2d146716c", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -4063,7 +4063,7 @@ }, { "commandType": "aspirate", - "key": "0c8ac8ba-5067-49c1-9d5b-172acf744fe5", + "key": "96680762-7d73-4c16-98d4-6ae783afd729", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -4075,7 +4075,7 @@ }, { "commandType": "dispense", - "key": "a72962af-a3e9-43cd-9c47-7f01b32e9ab0", + "key": "1be89f92-b2bd-4e14-b230-4e72ebc6fc77", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -4087,7 +4087,7 @@ }, { "commandType": "aspirate", - "key": "16815e9f-6894-401b-8f7c-034faa7a92a8", + "key": "f693e0d2-1aff-4dc7-b6e3-cdd6ef614c01", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -4099,7 +4099,7 @@ }, { "commandType": "dispense", - "key": "e82647e5-24cc-48cf-ac77-b832a4473f5f", + "key": "7918d2ba-e312-438c-8f15-ca28e8724bae", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -4111,7 +4111,7 @@ }, { "commandType": "aspirate", - "key": "b68ec85a-84c2-4e38-98c5-eb1417959b8c", + "key": "8ac0c540-ff45-4cfd-995b-3fb6870ba09f", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, @@ -4123,7 +4123,7 @@ }, { "commandType": "touchTip", - "key": "4daf45f1-1952-444e-a702-c951cd34171d", + "key": "4b930577-57af-4907-859a-f54bc71dc58d", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -4133,7 +4133,7 @@ }, { "commandType": "dispense", - "key": "7fd7054a-5321-47cb-b32e-0b6f6be27269", + "key": "cdb0573e-5982-40c7-95c2-4d884d69a313", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, @@ -4145,7 +4145,7 @@ }, { "commandType": "aspirate", - "key": "c58c6f6d-89e9-4b89-a5a4-b4fbb5df01e7", + "key": "6a3055ee-44ca-43fd-b1ea-caac89343321", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -4157,7 +4157,7 @@ }, { "commandType": "dispense", - "key": "d534677b-d743-463e-a3d5-9e665d8c42ee", + "key": "d7d8b056-6979-4840-aa44-b527e116aeff", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -4169,7 +4169,7 @@ }, { "commandType": "aspirate", - "key": "a521c336-3377-4474-8ef5-0fe5bcfb9856", + "key": "54dcd384-1fad-4071-bd6f-8f09a4eebb3d", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -4181,7 +4181,7 @@ }, { "commandType": "dispense", - "key": "b6eff799-e266-4972-8b74-87acd8ae4b20", + "key": "4710bca2-6bb3-4d86-8a27-192c431b525a", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -4191,19 +4191,9 @@ "flowRate": 10 } }, - { - "commandType": "touchTip", - "key": "81fe26dd-f249-45f8-81b6-ffe8df296ef3", - "params": { - "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", - "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", - "wellName": "E7", - "wellLocation": { "origin": "bottom", "offset": { "z": 40.3 } } - } - }, { "commandType": "moveToAddressableArea", - "key": "aacbe8ea-f7e7-46ad-bc9a-80e982202e71", + "key": "d3d0b4cd-c86a-43b2-99b0-9c9818dca0f3", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -4212,15 +4202,25 @@ }, { "commandType": "blowOutInPlace", - "key": "c4e0ef3e-1e4e-463d-a821-8c4864eb4f0e", + "key": "621e6320-03b1-4d3a-82f9-000c120042ce", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "flowRate": 1000 } }, + { + "commandType": "touchTip", + "key": "23c285de-7aa1-4a16-a457-015e2fb7abb7", + "params": { + "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", + "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", + "wellName": "E7", + "wellLocation": { "origin": "bottom", "offset": { "z": 40.3 } } + } + }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "c3aff53f-c4b4-460d-aca6-e45d8dfb7fd5", + "key": "b0268cc2-ae71-4f29-94cb-032b56e36252", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -4230,12 +4230,12 @@ }, { "commandType": "dropTipInPlace", - "key": "e779958f-851f-4276-82f6-18879c620bf4", + "key": "980de7a4-b9ad-40c5-af04-a989bf3ff807", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e" } }, { "commandType": "pickUpTip", - "key": "e96e028f-f470-42f4-a1b8-9e155d575fcc", + "key": "3e68bb44-ba33-484e-88cc-c931435e0c48", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "c6f4ec70-92a5-11e9-ac62-1b173f839d9e:tiprack-10ul", @@ -4244,7 +4244,7 @@ }, { "commandType": "aspirate", - "key": "1815c794-0370-4460-9a03-fbae6c084404", + "key": "b02f553c-c223-4fb7-8899-7db1a60186d0", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -4256,7 +4256,7 @@ }, { "commandType": "dispense", - "key": "1c12be09-a4f5-4844-8fd5-957ceefb2404", + "key": "b012ac38-0070-4ff4-acfe-d42b6c5f9674", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -4268,7 +4268,7 @@ }, { "commandType": "aspirate", - "key": "89c92dde-3580-4d90-b7ab-48906a3595b6", + "key": "537bf097-77dd-43bd-a67b-77a146f5349e", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -4280,7 +4280,7 @@ }, { "commandType": "dispense", - "key": "981553e8-1c52-4612-bd66-ee532c4a027f", + "key": "2931c986-44ae-4ba2-bb36-bd705feb875c", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -4292,7 +4292,7 @@ }, { "commandType": "aspirate", - "key": "dff7548c-c2ae-48fc-8fb4-33a96f2578d0", + "key": "bc5af19c-0dd5-4791-a5b1-34002997cb3d", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -4304,7 +4304,7 @@ }, { "commandType": "dispense", - "key": "cf1c3f7f-ad9f-453e-ba12-d1be98e49699", + "key": "dc5d2b3e-8efd-41a9-b84e-d7debee06ac9", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -4316,7 +4316,7 @@ }, { "commandType": "aspirate", - "key": "12bf910d-0bde-4a4c-b613-437e873a4078", + "key": "0de5e018-4a9f-4cfe-9f58-f50901663c3c", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, @@ -4328,7 +4328,7 @@ }, { "commandType": "touchTip", - "key": "3afb66c3-10ee-437f-b6d4-3bf8783ce9cc", + "key": "af42fb71-74da-41b8-9b50-41048e949434", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -4338,7 +4338,7 @@ }, { "commandType": "dispense", - "key": "d66cf63b-f856-4293-9140-0b9d0df28f61", + "key": "72efd216-e92f-4103-a71e-85be208865ec", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, @@ -4350,7 +4350,7 @@ }, { "commandType": "aspirate", - "key": "b7e62341-466a-4089-96dd-3e33ab8abfac", + "key": "0387ffa2-40c4-4280-86d1-8c1fd39b6356", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -4362,7 +4362,7 @@ }, { "commandType": "dispense", - "key": "706802e5-ecfa-4db8-817f-4cda1d3461fe", + "key": "c09fbe67-3e6c-4f82-8bc5-25db6a3d5a50", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -4374,7 +4374,7 @@ }, { "commandType": "aspirate", - "key": "fc706917-6d88-4d15-a8dc-f8e533470099", + "key": "1939bb32-88c2-4d55-bb3f-7d31535a3403", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -4386,7 +4386,7 @@ }, { "commandType": "dispense", - "key": "0c8589ba-0894-4df5-8927-48572ff6c401", + "key": "b3bdd7bd-5cc2-42fa-b938-24fbc32931d6", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -4396,19 +4396,9 @@ "flowRate": 10 } }, - { - "commandType": "touchTip", - "key": "00d269b5-7481-4c43-b054-c57e3fbfe605", - "params": { - "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", - "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", - "wellName": "D7", - "wellLocation": { "origin": "bottom", "offset": { "z": 40.3 } } - } - }, { "commandType": "moveToAddressableArea", - "key": "e15167b0-1b6c-40cb-bafb-1be42e155529", + "key": "ff827e3d-8136-44c1-a29a-33e0a0abf081", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -4417,15 +4407,25 @@ }, { "commandType": "blowOutInPlace", - "key": "8ff90ef0-42a8-4300-b1d3-cc894e476029", + "key": "ee65b14e-529d-4116-81a7-ff50f28bc1a4", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "flowRate": 1000 } }, + { + "commandType": "touchTip", + "key": "e31dd584-a774-41c7-9176-62749596b7e6", + "params": { + "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", + "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", + "wellName": "D7", + "wellLocation": { "origin": "bottom", "offset": { "z": 40.3 } } + } + }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "021423f6-e2ed-40ee-8305-7da59c111dc0", + "key": "cd354a43-9b7d-48d5-8e2a-f6c369ac10f4", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -4435,12 +4435,12 @@ }, { "commandType": "dropTipInPlace", - "key": "c3883abe-ef2d-42a7-9eb5-a32f7d81ca28", + "key": "a435f546-520d-4e38-bc22-f5f084f95d5d", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e" } }, { "commandType": "pickUpTip", - "key": "787b0eb7-866d-4230-8932-5683d2db4143", + "key": "ba2e7ee3-715f-4588-93e8-05d4b1eed1cc", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "c6f4ec70-92a5-11e9-ac62-1b173f839d9e:tiprack-10ul", @@ -4449,7 +4449,7 @@ }, { "commandType": "aspirate", - "key": "d037b353-7b41-4311-a36f-f1aab11d6ac8", + "key": "1c2d5f90-6dbd-4b61-b97e-a4bf38f056d9", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -4461,7 +4461,7 @@ }, { "commandType": "dispense", - "key": "10363067-39c5-42b0-a620-3ee6a2774a9b", + "key": "2fbc684b-57c7-4e89-8d53-85c7f6f806de", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -4473,7 +4473,7 @@ }, { "commandType": "aspirate", - "key": "15eb4102-e34c-4d6e-916f-42ce00375aa7", + "key": "ef0f4077-3692-41b0-ad2d-0bcf94a1a075", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -4485,7 +4485,7 @@ }, { "commandType": "dispense", - "key": "46711650-9279-4031-b5ea-c0820a32d961", + "key": "a386c011-855b-4f41-be57-623647498c1a", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -4497,7 +4497,7 @@ }, { "commandType": "aspirate", - "key": "9e34e43e-89da-4b7e-be2e-a6042b3ef954", + "key": "9dd98cc7-2557-48bd-baf9-2e54ab47883c", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -4509,7 +4509,7 @@ }, { "commandType": "dispense", - "key": "8114f067-59e2-4011-82da-08c8d2f9aa68", + "key": "3d57ac48-bf99-498b-b523-1be901efcc1e", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -4521,7 +4521,7 @@ }, { "commandType": "aspirate", - "key": "716278f3-86c2-46c8-96a9-ab31e9b8a8f2", + "key": "e64a4b94-cd07-4eb2-9edc-ab83093fc4bc", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, @@ -4533,7 +4533,7 @@ }, { "commandType": "touchTip", - "key": "165b08fc-9663-4e9c-b49f-194a81ba56c4", + "key": "99567709-ebe8-4244-8252-dedb5aeb666c", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -4543,7 +4543,7 @@ }, { "commandType": "dispense", - "key": "61113794-7f55-4925-94e4-6ac1e9d0b5c0", + "key": "94901710-b6db-4d27-b893-71108cc6186c", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, @@ -4555,7 +4555,7 @@ }, { "commandType": "aspirate", - "key": "3195d674-6f23-41e7-968f-5978f4423b11", + "key": "0467798b-8ec8-4d1e-afee-2a73a8422bcd", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -4567,7 +4567,7 @@ }, { "commandType": "dispense", - "key": "7e2df534-36d9-4c78-8cff-9894b305aa56", + "key": "fc858419-1723-4c54-85d1-2d2ef53637ee", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -4579,7 +4579,7 @@ }, { "commandType": "aspirate", - "key": "2f1d06ba-9586-4e14-8ab7-5747aa14d47c", + "key": "b30572d5-f396-41f3-8662-a4285508710d", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -4591,7 +4591,7 @@ }, { "commandType": "dispense", - "key": "5358b164-56c2-4042-a8b8-1645e3f8c0c9", + "key": "5da67cc2-d056-4d4c-abc5-3a70269c38bc", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -4601,19 +4601,9 @@ "flowRate": 10 } }, - { - "commandType": "touchTip", - "key": "2643cf2a-5373-43bd-bd37-dd1e62c4c548", - "params": { - "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", - "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", - "wellName": "C7", - "wellLocation": { "origin": "bottom", "offset": { "z": 40.3 } } - } - }, { "commandType": "moveToAddressableArea", - "key": "97fc08d9-59ee-46ee-99d5-20e33acbb2f2", + "key": "c847fdfb-bb85-4335-9821-8fded3c15f0e", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -4622,15 +4612,25 @@ }, { "commandType": "blowOutInPlace", - "key": "4f2e4f39-dea7-444f-b6d5-e7cfd6c1bcb2", + "key": "d269fea9-b30e-488f-a2b2-37ae88547251", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "flowRate": 1000 } }, + { + "commandType": "touchTip", + "key": "da6ec212-7d12-411a-9f2b-2beff5ed197d", + "params": { + "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", + "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", + "wellName": "C7", + "wellLocation": { "origin": "bottom", "offset": { "z": 40.3 } } + } + }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "252afdc4-bebe-47fb-ad4f-e10766436a23", + "key": "d8bef8c0-954a-4293-a75f-2589a37fc982", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -4640,12 +4640,12 @@ }, { "commandType": "dropTipInPlace", - "key": "7a36277b-7c2a-401f-8802-2af031444e22", + "key": "784c0470-5513-4f60-bb7e-f039db7b170f", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e" } }, { "commandType": "pickUpTip", - "key": "e5e61410-a679-4caf-94d0-1234a7337bcc", + "key": "5729228a-64f0-443e-91fe-31179efbdd1a", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "c6f4ec70-92a5-11e9-ac62-1b173f839d9e:tiprack-10ul", @@ -4654,7 +4654,7 @@ }, { "commandType": "aspirate", - "key": "231f4239-1e72-4f15-b393-5103d62197a8", + "key": "288895f0-14c7-4909-a300-178801bd08b4", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -4666,7 +4666,7 @@ }, { "commandType": "dispense", - "key": "91f9fd1e-7690-4ba3-aa5b-24bfadde94f3", + "key": "2f718ed4-0d72-45c4-bb4d-cc8265cbbf9a", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -4678,7 +4678,7 @@ }, { "commandType": "aspirate", - "key": "d329cf02-bb5a-441b-9433-b6ed36e4b16a", + "key": "7fde7d76-b68e-43d2-a00f-3203fdcfd95e", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -4690,7 +4690,7 @@ }, { "commandType": "dispense", - "key": "61c6428b-d0ad-4aa1-8fba-0983fac42a1e", + "key": "ca2845d1-33bf-49cf-8bfe-48bbe544419e", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -4702,7 +4702,7 @@ }, { "commandType": "aspirate", - "key": "f022bc59-c825-444d-bf31-dbc784e657ba", + "key": "b5b15b72-dce1-430e-8050-e5e4b6fd9d54", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -4714,7 +4714,7 @@ }, { "commandType": "dispense", - "key": "b99963d8-d11e-4d4b-bbfe-7d1dd46a385f", + "key": "c3bc77de-b5d0-43fa-a7a5-bc9e6b6fd765", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -4726,7 +4726,7 @@ }, { "commandType": "aspirate", - "key": "032ce0d1-c61e-4a49-bcd6-e05715ea01a1", + "key": "c08f49f2-c0c8-488b-beab-160ad57f46c5", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, @@ -4738,7 +4738,7 @@ }, { "commandType": "touchTip", - "key": "f507c53d-b959-4d2a-88a4-3d760ec0d5a4", + "key": "1d53f469-6c0b-4264-bb92-abb8299f650d", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -4748,7 +4748,7 @@ }, { "commandType": "dispense", - "key": "c4768870-6ea0-47d5-bd01-821f76484851", + "key": "122d4fc9-e63d-430e-8ea0-6c1b17c3f1a7", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, @@ -4760,7 +4760,7 @@ }, { "commandType": "aspirate", - "key": "5f0ceccf-c18b-4d38-a67d-227de289baa3", + "key": "7d9df411-c0a1-4e91-8716-c80643cbd868", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -4772,7 +4772,7 @@ }, { "commandType": "dispense", - "key": "eb34a3a3-2163-4780-a3f9-0c2c27b266fd", + "key": "73a6bb03-d083-475d-99de-452fb093e44b", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -4784,7 +4784,7 @@ }, { "commandType": "aspirate", - "key": "aa5b4672-0f67-4f1f-af47-f40cf91dc2a6", + "key": "818098d4-ddd1-4853-875f-eeaf28898e12", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -4796,7 +4796,7 @@ }, { "commandType": "dispense", - "key": "55640978-689e-46d6-8d5f-10ba8e970d00", + "key": "4b43d7c0-d2cc-4721-8675-98c0357889fd", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -4806,19 +4806,9 @@ "flowRate": 10 } }, - { - "commandType": "touchTip", - "key": "213949a7-feed-4fe0-95bb-57495a558334", - "params": { - "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", - "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", - "wellName": "E6", - "wellLocation": { "origin": "bottom", "offset": { "z": 40.3 } } - } - }, { "commandType": "moveToAddressableArea", - "key": "32e64358-369f-4a0f-b7f7-cdac58b9e1a6", + "key": "4461238e-6823-489c-9b95-59529d34c5e6", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -4827,15 +4817,25 @@ }, { "commandType": "blowOutInPlace", - "key": "d4358e84-b66b-4f58-917c-87ebf2f804cb", + "key": "abcefb59-b32e-4b9e-8ac3-fb8589565405", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "flowRate": 1000 } }, + { + "commandType": "touchTip", + "key": "6e1c8052-ebab-401a-a3de-1a20d61a1b40", + "params": { + "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", + "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", + "wellName": "E6", + "wellLocation": { "origin": "bottom", "offset": { "z": 40.3 } } + } + }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "d6bcd44a-459c-40f5-b48b-7f66c056f593", + "key": "72caf8d6-745c-4bb8-997b-c6b2685935b6", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -4845,12 +4845,12 @@ }, { "commandType": "dropTipInPlace", - "key": "afef5a4a-3808-4f78-a62d-daef9b85293f", + "key": "e4f6c6e4-58b0-466c-972a-56ee8b56735c", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e" } }, { "commandType": "pickUpTip", - "key": "1fee685b-03b1-4a68-88bf-746d83c1f734", + "key": "a5de52b2-a015-4377-9adc-2e784a8a3514", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "c6f4ec70-92a5-11e9-ac62-1b173f839d9e:tiprack-10ul", @@ -4859,7 +4859,7 @@ }, { "commandType": "aspirate", - "key": "c4ef2258-e356-463a-9f47-50288c93896b", + "key": "f46ecf37-8a53-4f96-87b4-45b58807c754", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -4871,7 +4871,7 @@ }, { "commandType": "dispense", - "key": "3645a8be-8872-47af-9a30-07afcb9ae234", + "key": "cbedd7bd-637c-4767-a6a6-694b76138850", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -4883,7 +4883,7 @@ }, { "commandType": "aspirate", - "key": "02580ed2-f298-4da2-9ccb-e751d09f3015", + "key": "a200a845-574f-4f0b-9ad7-39f095b6d732", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -4895,7 +4895,7 @@ }, { "commandType": "dispense", - "key": "988739b0-1ff9-4c51-9d5e-86abaeaf7f09", + "key": "96a8c1d7-bd45-44a7-ba7c-44b4d1067f4e", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -4907,7 +4907,7 @@ }, { "commandType": "aspirate", - "key": "62e7e213-b5b3-40fb-b3aa-a13d035e44f1", + "key": "b1aae64c-98fe-402a-8a6e-38046dc2d375", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -4919,7 +4919,7 @@ }, { "commandType": "dispense", - "key": "97b34c42-511a-4d68-afee-09c493088796", + "key": "9ec5ab4b-5da0-4859-b713-e849f806a4c7", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -4931,7 +4931,7 @@ }, { "commandType": "aspirate", - "key": "01ea3e16-49c4-4c23-9123-7f1ade690342", + "key": "f6849f46-5724-4643-92bf-b526f5e263fa", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, @@ -4943,7 +4943,7 @@ }, { "commandType": "touchTip", - "key": "fe523115-3e72-4623-81eb-414836ec000b", + "key": "9dc1c842-947a-4e0f-8601-bae5edf58bd0", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -4953,7 +4953,7 @@ }, { "commandType": "dispense", - "key": "134b1437-05ae-4c9c-ba9e-3a8e87b1b2f3", + "key": "f9c66ebe-764a-4d16-975a-b9d275f7e6e3", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, @@ -4965,7 +4965,7 @@ }, { "commandType": "aspirate", - "key": "d6167e35-ef8c-4b1a-800f-240c30ac60af", + "key": "ccf1ab0e-c50f-4c41-9eb9-5f84ec9c8d8c", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -4977,7 +4977,7 @@ }, { "commandType": "dispense", - "key": "94d06fc3-2155-423d-bbbe-2702134d0b66", + "key": "0f734cf9-c9d8-40ee-82f7-a34d97e43ed9", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -4989,7 +4989,7 @@ }, { "commandType": "aspirate", - "key": "a8f19aa5-d7f1-4a3e-9647-1b552cfc39aa", + "key": "0139e4ec-529e-4080-8926-37c140621866", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -5001,7 +5001,7 @@ }, { "commandType": "dispense", - "key": "06b8e0be-cc31-46f9-8e82-02b25241bf9b", + "key": "e3a5a1bf-0a24-4787-b3fe-2f60075de339", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -5011,19 +5011,9 @@ "flowRate": 10 } }, - { - "commandType": "touchTip", - "key": "e3ed68db-2b25-4ae1-802c-5b4a41f7ee68", - "params": { - "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", - "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", - "wellName": "D6", - "wellLocation": { "origin": "bottom", "offset": { "z": 40.3 } } - } - }, { "commandType": "moveToAddressableArea", - "key": "63cea2eb-fde2-4bca-976e-30df41c074b7", + "key": "7b9216cc-c1d4-469e-a5d8-7683a943bb0c", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -5032,15 +5022,25 @@ }, { "commandType": "blowOutInPlace", - "key": "24f20d09-4f31-4745-9c13-56294033a7cd", + "key": "cdeb2bad-74b0-4160-9984-ebb55bb04bc3", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "flowRate": 1000 } }, + { + "commandType": "touchTip", + "key": "8d938cfa-0484-4692-bac6-143f3f52e75b", + "params": { + "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", + "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", + "wellName": "D6", + "wellLocation": { "origin": "bottom", "offset": { "z": 40.3 } } + } + }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "ccc5d7fe-9806-484e-b4a4-d9bb456e7c04", + "key": "ec4eb309-173d-452e-a601-6ea966a7254e", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -5050,12 +5050,12 @@ }, { "commandType": "dropTipInPlace", - "key": "9837b26a-92cb-4b2c-928a-09f96213ba44", + "key": "f92c4c88-0208-44f5-81b9-056546a45e49", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e" } }, { "commandType": "pickUpTip", - "key": "f962386f-842e-454f-ade8-0ef08bbcbd43", + "key": "1ede6001-67c7-4d54-b866-4eb2d9b1d82b", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "c6f4ec70-92a5-11e9-ac62-1b173f839d9e:tiprack-10ul", @@ -5064,7 +5064,7 @@ }, { "commandType": "aspirate", - "key": "af9c739b-acf9-4db3-ba58-b34a0d90c70e", + "key": "ccf06eed-b517-4b14-b31d-736dcdc8c3b4", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -5076,7 +5076,7 @@ }, { "commandType": "dispense", - "key": "f872765c-27c0-4507-90f3-4259560ca9a4", + "key": "49f837ff-5dd9-4f54-b4a0-ffd492c4c969", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -5088,7 +5088,7 @@ }, { "commandType": "aspirate", - "key": "f3025d61-4322-463e-83ec-e47182b2725d", + "key": "530ffdc6-b112-4f88-b25e-745bb9c86516", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -5100,7 +5100,7 @@ }, { "commandType": "dispense", - "key": "7611a735-e1c2-4cc1-82c2-053c63c6ab10", + "key": "515f7c58-c506-4bad-95c4-4adfcdadea5d", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -5112,7 +5112,7 @@ }, { "commandType": "aspirate", - "key": "a3074ec0-f736-4837-99d4-3b37f0a7ee22", + "key": "fc5e49e8-cadb-4a8a-addb-4525a0640254", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -5124,7 +5124,7 @@ }, { "commandType": "dispense", - "key": "f043597b-a221-4670-9ced-5bda15cd7c4e", + "key": "26838934-eaf7-4a76-bb3b-070e3aab3bcb", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, @@ -5136,7 +5136,7 @@ }, { "commandType": "aspirate", - "key": "d02ca9b9-43ef-4826-af1b-5b2f1b668378", + "key": "eef5c160-b9b0-43cf-8e8e-9b836431a606", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, @@ -5148,7 +5148,7 @@ }, { "commandType": "touchTip", - "key": "b16f9a78-8c9a-4701-8ce9-a68d549705ff", + "key": "4e84dbb8-53b6-400f-8530-eb2ee326dc13", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -5158,7 +5158,7 @@ }, { "commandType": "dispense", - "key": "27208993-c49c-4ed2-a58f-fd1c9726da35", + "key": "19c3e661-d308-4544-babf-fd4cefd23331", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, @@ -5170,7 +5170,7 @@ }, { "commandType": "aspirate", - "key": "4e6d7f1b-bf01-4dc8-9804-db5891de458d", + "key": "2bfda325-1526-4178-8fa5-338c9dc9d92b", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -5182,7 +5182,7 @@ }, { "commandType": "dispense", - "key": "0f2634c6-4557-4f70-aeab-aa557d43d63e", + "key": "adabc3a8-3e76-423d-949e-8d5146862421", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -5194,7 +5194,7 @@ }, { "commandType": "aspirate", - "key": "8df3e7e6-c44f-48d8-98cd-49ae2bdceb74", + "key": "957dda98-4628-4029-90bd-d1a2e0c280c3", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -5206,7 +5206,7 @@ }, { "commandType": "dispense", - "key": "4cead8f3-508b-49fc-843c-47708304ac93", + "key": "538985a7-8e67-4abd-94cf-68387fd80e7d", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, @@ -5216,19 +5216,9 @@ "flowRate": 10 } }, - { - "commandType": "touchTip", - "key": "edb37370-7199-459b-a925-17ed47861588", - "params": { - "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", - "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", - "wellName": "C6", - "wellLocation": { "origin": "bottom", "offset": { "z": 40.3 } } - } - }, { "commandType": "moveToAddressableArea", - "key": "7005887b-2511-4f79-aeb3-855150844387", + "key": "71eaf4c8-c8a5-400b-b094-46bdcaa60daf", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -5237,15 +5227,25 @@ }, { "commandType": "blowOutInPlace", - "key": "2aeaea31-84e5-4b17-a085-d3eb62c3e89e", + "key": "f935fe77-d02e-4bb8-95e2-5f25e8312dad", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "flowRate": 1000 } }, + { + "commandType": "touchTip", + "key": "6cbaaafd-f358-4779-9cb2-3622e3285ae1", + "params": { + "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", + "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", + "wellName": "C6", + "wellLocation": { "origin": "bottom", "offset": { "z": 40.3 } } + } + }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "39262fc1-a0f9-4155-9db8-0628b2e013b7", + "key": "756c761d-66fe-4fc7-8e53-cf258c4b95c4", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -5255,12 +5255,12 @@ }, { "commandType": "dropTipInPlace", - "key": "4653d001-f682-415e-ae31-c70dca6ce4f7", + "key": "bfb11f03-bef0-4d98-a569-b21249c1f447", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e" } }, { "commandType": "pickUpTip", - "key": "686a2200-9d23-4a25-bdb7-fd9a32d1c9ac", + "key": "7dec52bf-9c68-42ca-838b-1ebb9c4f325f", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "c6f4ec70-92a5-11e9-ac62-1b173f839d9e:tiprack-10ul", @@ -5269,7 +5269,7 @@ }, { "commandType": "aspirate", - "key": "b9112647-1963-4a42-9d9f-3294d3962fbe", + "key": "83e518c4-7a06-439f-b7f8-175feb33b528", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 5.5, @@ -5281,7 +5281,7 @@ }, { "commandType": "dispense", - "key": "1131307b-8c81-45b6-9395-b1b7f5568708", + "key": "a79b3bc6-6e2c-4800-adf2-72f5b221e2d4", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 5.5, @@ -5293,7 +5293,7 @@ }, { "commandType": "aspirate", - "key": "baa2f965-8f3d-41ff-a124-a045a975a9d8", + "key": "3fd83532-cf51-43d6-bd74-ca3fcd09f175", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 5.5, @@ -5305,7 +5305,7 @@ }, { "commandType": "dispense", - "key": "ee62e490-95d0-45b5-9e8a-1d810de9759e", + "key": "48d023af-e120-4d61-8eb0-76a9433258a4", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 5.5, @@ -5317,7 +5317,7 @@ }, { "commandType": "aspirate", - "key": "75129558-a345-4881-95ef-2989836e833d", + "key": "54f4aba0-c8f0-463d-8bff-8e3311db6765", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 5.5, @@ -5329,7 +5329,7 @@ }, { "commandType": "dispense", - "key": "fe55ef54-d044-44cf-890d-6990f8c2c546", + "key": "1589b195-68ec-47a4-baee-f27de214ef10", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 5.5, @@ -5341,7 +5341,7 @@ }, { "commandType": "blowout", - "key": "ef6a39e5-1820-498e-82ef-1ccf5f8bf183", + "key": "0a4211db-4a8c-496a-9098-0a8547f4e39f", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -5352,7 +5352,7 @@ }, { "commandType": "touchTip", - "key": "c6189400-48b1-42ce-9071-6521503ad70e", + "key": "0575a144-4887-4ccd-b64a-a1a18094a2f5", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -5362,7 +5362,7 @@ }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "a3327fbf-7028-4a4b-adae-90a79f19dcfe", + "key": "2551d68a-3a19-4283-84d9-fd285ee0f745", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -5372,12 +5372,12 @@ }, { "commandType": "dropTipInPlace", - "key": "5706a987-1067-4a6f-b0d2-72e4e2efd853", + "key": "981b6c74-860e-4c14-bb74-25c66d110508", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e" } }, { "commandType": "waitForDuration", - "key": "3e17b047-d94f-4476-a51d-5a50b40bf65b", + "key": "a45e4cd0-d4b1-4042-9295-396f0e6b92df", "params": { "seconds": 3723, "message": "Delay plz" } } ], diff --git a/step-generation/src/__tests__/consolidate.test.ts b/step-generation/src/__tests__/consolidate.test.ts index 219c7b51c54..e43e31c4463 100644 --- a/step-generation/src/__tests__/consolidate.test.ts +++ b/step-generation/src/__tests__/consolidate.test.ts @@ -1350,6 +1350,8 @@ describe('consolidate single-channel', () => { seconds: 12, }, }, + // Blowout to trash + ...blowoutInPlaceHelper(), // Touch tip (disp) { commandType: 'touchTip', @@ -1370,8 +1372,6 @@ describe('consolidate single-channel', () => { // No Dispense > Air Gap here because we're re-using the tip // for the next chunk - // Blowout to trash - ...blowoutInPlaceHelper(), // Second chunk: source well A3 // pre-wet { @@ -1596,6 +1596,8 @@ describe('consolidate single-channel', () => { seconds: 12, }, }, + // Blowout to trash + ...blowoutInPlaceHelper(), // Touch tip (disp) { commandType: 'touchTip', @@ -1612,9 +1614,6 @@ describe('consolidate single-channel', () => { }, }, }, - - // Blowout to trash - ...blowoutInPlaceHelper(), // Dispense > air gap in dest well { commandType: 'aspirate', @@ -1992,44 +1991,43 @@ describe('consolidate single-channel', () => { seconds: 12, }, }, - // Touch tip (disp) + // Blowout to dest well { - commandType: 'touchTip', + commandType: 'blowout', key: expect.any(String), params: { pipetteId: 'p300SingleId', labwareId: 'destPlateId', wellName: 'B1', + flowRate: 2.3, wellLocation: { origin: 'bottom', offset: { - z: 3.4, + z: 13.84, }, }, }, }, - - // No Dispense > Air Gap here because we're re-using the tip - // for the next chunk - - // Blowout to dest well + // Touch tip (disp) { - commandType: 'blowout', + commandType: 'touchTip', key: expect.any(String), params: { pipetteId: 'p300SingleId', labwareId: 'destPlateId', wellName: 'B1', - flowRate: 2.3, wellLocation: { origin: 'bottom', offset: { - z: 13.84, + z: 3.4, }, }, }, }, + // No Dispense > Air Gap here because we're re-using the tip + // for the next chunk + // Second chunk: source well A3 // pre-wet { @@ -2254,35 +2252,35 @@ describe('consolidate single-channel', () => { seconds: 12, }, }, - // Touch tip (disp) + // Blowout to dest { - commandType: 'touchTip', + commandType: 'blowout', key: expect.any(String), params: { pipetteId: 'p300SingleId', labwareId: 'destPlateId', wellName: 'B1', + flowRate: 2.3, wellLocation: { origin: 'bottom', offset: { - z: 3.4, + z: 13.84, }, }, }, }, - // Blowout to dest + // Touch tip (disp) { - commandType: 'blowout', + commandType: 'touchTip', key: expect.any(String), params: { pipetteId: 'p300SingleId', labwareId: 'destPlateId', wellName: 'B1', - flowRate: 2.3, wellLocation: { origin: 'bottom', offset: { - z: 13.84, + z: 3.4, }, }, }, @@ -2661,36 +2659,35 @@ describe('consolidate single-channel', () => { seconds: 12, }, }, - // Touch tip (disp) + // Blowout to dest well { - commandType: 'touchTip', + commandType: 'blowout', key: expect.any(String), params: { pipetteId: 'p300SingleId', labwareId: 'destPlateId', wellName: 'B1', + flowRate: 2.3, wellLocation: { origin: 'bottom', offset: { - z: 3.4, + z: 13.84, }, }, }, }, - - // Blowout to dest well + // Touch tip (disp) { - commandType: 'blowout', + commandType: 'touchTip', key: expect.any(String), params: { pipetteId: 'p300SingleId', labwareId: 'destPlateId', wellName: 'B1', - flowRate: 2.3, wellLocation: { origin: 'bottom', offset: { - z: 13.84, + z: 3.4, }, }, }, @@ -2958,35 +2955,35 @@ describe('consolidate single-channel', () => { seconds: 12, }, }, - // Touch tip (disp) + // Blowout to dest { - commandType: 'touchTip', + commandType: 'blowout', key: expect.any(String), params: { pipetteId: 'p300SingleId', labwareId: 'destPlateId', wellName: 'B1', + flowRate: 2.3, wellLocation: { origin: 'bottom', offset: { - z: 3.4, + z: 13.84, }, }, }, }, - // Blowout to dest + // Touch tip (disp) { - commandType: 'blowout', + commandType: 'touchTip', key: expect.any(String), params: { pipetteId: 'p300SingleId', labwareId: 'destPlateId', wellName: 'B1', - flowRate: 2.3, wellLocation: { origin: 'bottom', offset: { - z: 13.84, + z: 3.4, }, }, }, diff --git a/step-generation/src/__tests__/transfer.test.ts b/step-generation/src/__tests__/transfer.test.ts index 43b33ce0ca3..49319bfe2ea 100644 --- a/step-generation/src/__tests__/transfer.test.ts +++ b/step-generation/src/__tests__/transfer.test.ts @@ -1383,22 +1383,6 @@ describe('advanced options', () => { seconds: 12, }, }, - // touch tip (disp) - { - commandType: 'touchTip', - key: expect.any(String), - params: { - pipetteId: 'p300SingleId', - labwareId: 'destPlateId', - wellName: 'B1', - wellLocation: { - origin: 'bottom', - offset: { - z: 3.4, - }, - }, - }, - }, // no dispense > air gap, because tip will be reused // blowout { @@ -1418,6 +1402,22 @@ describe('advanced options', () => { flowRate: 2.3, }, }, + // touch tip (disp) + { + commandType: 'touchTip', + key: expect.any(String), + params: { + pipetteId: 'p300SingleId', + labwareId: 'destPlateId', + wellName: 'B1', + wellLocation: { + origin: 'bottom', + offset: { + z: 3.4, + }, + }, + }, + }, // next chunk from A1: remaining volume // do not pre-wet // mix (asp) @@ -1669,37 +1669,37 @@ describe('advanced options', () => { seconds: 12, }, }, - // touch tip (disp) { - commandType: 'touchTip', + commandType: 'moveToAddressableArea', key: expect.any(String), params: { pipetteId: 'p300SingleId', - labwareId: 'destPlateId', - wellName: 'B1', - wellLocation: { - origin: 'bottom', - offset: { - z: 3.4, - }, - }, + addressableAreaName: 'movableTrashA3', + offset: { x: 0, y: 0, z: 0 }, }, }, { - commandType: 'moveToAddressableArea', + commandType: 'blowOutInPlace', key: expect.any(String), params: { pipetteId: 'p300SingleId', - addressableAreaName: 'movableTrashA3', - offset: { x: 0, y: 0, z: 0 }, + flowRate: 2.3, }, }, + // touch tip (disp) { - commandType: 'blowOutInPlace', + commandType: 'touchTip', key: expect.any(String), params: { pipetteId: 'p300SingleId', - flowRate: 2.3, + labwareId: 'destPlateId', + wellName: 'B1', + wellLocation: { + origin: 'bottom', + offset: { + z: 3.4, + }, + }, }, }, // use the dispense > air gap here before moving to trash @@ -2041,35 +2041,35 @@ describe('advanced options', () => { seconds: 12, }, }, - // touch tip (disp) + // blowout { - commandType: 'touchTip', + commandType: 'blowout', key: expect.any(String), params: { pipetteId: 'p300SingleId', labwareId: 'destPlateId', wellName: 'B1', + flowRate: 2.3, wellLocation: { origin: 'bottom', offset: { - z: 3.4, + z: 13.84, }, }, }, }, - // blowout + // touch tip (disp) { - commandType: 'blowout', + commandType: 'touchTip', key: expect.any(String), params: { pipetteId: 'p300SingleId', labwareId: 'destPlateId', wellName: 'B1', - flowRate: 2.3, wellLocation: { origin: 'bottom', offset: { - z: 13.84, + z: 3.4, }, }, }, @@ -2326,35 +2326,35 @@ describe('advanced options', () => { seconds: 12, }, }, - // touch tip (disp) + // blowout to dest well { - commandType: 'touchTip', + commandType: 'blowout', key: expect.any(String), params: { pipetteId: 'p300SingleId', labwareId: 'destPlateId', wellName: 'B1', + flowRate: 2.3, wellLocation: { origin: 'bottom', offset: { - z: 3.4, + z: 13.84, }, }, }, }, - // blowout to dest well + // touch tip (disp) { - commandType: 'blowout', + commandType: 'touchTip', key: expect.any(String), params: { pipetteId: 'p300SingleId', labwareId: 'destPlateId', wellName: 'B1', - flowRate: 2.3, wellLocation: { origin: 'bottom', offset: { - z: 13.84, + z: 3.4, }, }, }, @@ -2727,35 +2727,35 @@ describe('advanced options', () => { seconds: 12, }, }, - // touch tip (disp) + // blowout { - commandType: 'touchTip', + commandType: 'blowout', key: expect.any(String), params: { pipetteId: 'p300SingleId', labwareId: 'destPlateId', wellName: 'B1', + flowRate: 2.3, wellLocation: { origin: 'bottom', offset: { - z: 3.4, + z: 13.84, }, }, }, }, - // blowout + // touch tip (disp) { - commandType: 'blowout', + commandType: 'touchTip', key: expect.any(String), params: { pipetteId: 'p300SingleId', labwareId: 'destPlateId', wellName: 'B1', - flowRate: 2.3, wellLocation: { origin: 'bottom', offset: { - z: 13.84, + z: 3.4, }, }, }, @@ -3013,35 +3013,35 @@ describe('advanced options', () => { seconds: 12, }, }, - // touch tip (disp) + // blowout { - commandType: 'touchTip', + commandType: 'blowout', key: expect.any(String), params: { pipetteId: 'p300SingleId', labwareId: 'destPlateId', wellName: 'B1', + flowRate: 2.3, wellLocation: { origin: 'bottom', offset: { - z: 3.4, + z: 13.84, }, }, }, }, - // blowout + // touch tip (disp) { - commandType: 'blowout', + commandType: 'touchTip', key: expect.any(String), params: { pipetteId: 'p300SingleId', labwareId: 'destPlateId', wellName: 'B1', - flowRate: 2.3, wellLocation: { origin: 'bottom', offset: { - z: 13.84, + z: 3.4, }, }, }, @@ -3412,35 +3412,35 @@ describe('advanced options', () => { seconds: 12, }, }, - // touch tip (disp) + // blowout { - commandType: 'touchTip', + commandType: 'blowout', key: expect.any(String), params: { pipetteId: 'p300SingleId', - labwareId: 'destPlateId', - wellName: 'B1', + labwareId: 'sourcePlateId', + wellName: 'A1', + flowRate: 2.3, wellLocation: { origin: 'bottom', offset: { - z: 3.4, + z: 13.84, }, }, }, }, - // blowout + // touch tip (disp) { - commandType: 'blowout', + commandType: 'touchTip', key: expect.any(String), params: { pipetteId: 'p300SingleId', - labwareId: 'sourcePlateId', - wellName: 'A1', - flowRate: 2.3, + labwareId: 'destPlateId', + wellName: 'B1', wellLocation: { origin: 'bottom', offset: { - z: 13.84, + z: 3.4, }, }, }, @@ -3747,35 +3747,35 @@ describe('advanced options', () => { seconds: 12, }, }, - // touch tip (disp) + // blowout { - commandType: 'touchTip', + commandType: 'blowout', key: expect.any(String), params: { pipetteId: 'p300SingleId', - labwareId: 'destPlateId', - wellName: 'B1', + labwareId: 'sourcePlateId', + wellName: 'A1', + flowRate: 2.3, wellLocation: { origin: 'bottom', offset: { - z: 3.4, + z: 13.84, }, }, }, }, - // blowout + // touch tip (disp) { - commandType: 'blowout', + commandType: 'touchTip', key: expect.any(String), params: { pipetteId: 'p300SingleId', - labwareId: 'sourcePlateId', - wellName: 'A1', - flowRate: 2.3, + labwareId: 'destPlateId', + wellName: 'B1', wellLocation: { origin: 'bottom', offset: { - z: 13.84, + z: 3.4, }, }, }, diff --git a/step-generation/src/commandCreators/compound/consolidate.ts b/step-generation/src/commandCreators/compound/consolidate.ts index 09c1b02a9ae..6507f9227f2 100644 --- a/step-generation/src/commandCreators/compound/consolidate.ts +++ b/step-generation/src/commandCreators/compound/consolidate.ts @@ -496,8 +496,8 @@ export const consolidate: CommandCreator = ( ...dispenseCommands, ...delayAfterDispenseCommands, ...mixAfterCommands, - ...touchTipAfterDispenseCommands, ...blowoutCommand, + ...touchTipAfterDispenseCommands, ...airGapAfterDispenseCommands, ...dropTipAfterDispenseAirGap, ] diff --git a/step-generation/src/commandCreators/compound/transfer.ts b/step-generation/src/commandCreators/compound/transfer.ts index 6d57f7ee457..d7f4ec5e181 100644 --- a/step-generation/src/commandCreators/compound/transfer.ts +++ b/step-generation/src/commandCreators/compound/transfer.ts @@ -602,8 +602,8 @@ export const transfer: CommandCreator = ( ...dispenseCommand, ...delayAfterDispenseCommands, ...mixInDestinationCommands, - ...touchTipAfterDispenseCommands, ...blowoutCommand, + ...touchTipAfterDispenseCommands, ...airGapAfterDispenseCommands, ...dropTipAfterDispenseAirGap, ] From 0128834c9a62ac7b927c3df327e8ab9670a081c9 Mon Sep 17 00:00:00 2001 From: koji Date: Fri, 29 Mar 2024 14:31:16 -0400 Subject: [PATCH 004/194] feat(components, app): update Chip component for unification (#14708) * feat(components, app): update Chip component for unification --- app/src/atoms/Chip/__tests__/Chip.test.tsx | 224 --------- .../InlineNotification.stories.tsx | 4 +- app/src/atoms/ListItem/ListItem.stories.tsx | 4 +- app/src/atoms/Snackbar/Snackbar.stories.tsx | 4 +- .../CustomKeyboard/CustomKeyboard.stories.tsx | 4 +- .../NormalKeyboard/NormalKeyboard.stories.tsx | 6 +- .../Numpad/Numpad.stories.tsx | 4 +- app/src/atoms/Toast/ODDToast.stories.tsx | 4 +- .../buttons/FloatingActionButton.stories.tsx | 5 +- app/src/atoms/buttons/LargeButton.stories.tsx | 4 +- .../atoms/buttons/MediumButton.stories.tsx | 5 +- app/src/atoms/buttons/RadioButton.stories.tsx | 4 +- app/src/atoms/buttons/SmallButton.stories.tsx | 4 +- .../atoms/buttons/TabbedButton.stories.tsx | 4 +- .../BackgroundOverlay.stories.tsx | 10 +- .../CardButton/CardButton.stories.tsx | 5 +- app/src/molecules/Modal/Modal.stories.tsx | 5 +- .../molecules/Modal/ModalHeader.stories.tsx | 5 +- .../Modal/SmallModalChildren.stories.tsx | 4 +- .../ODDBackButton/ODDBackButton.stories.tsx | 4 +- .../ChildNavigation.stories.tsx | 4 +- .../AddFixtureModal.stories.tsx | 4 +- ...nfigurationDiscardChangesModal.stories.tsx | 4 +- ...ckFixtureSetupInstructionModal.stories.tsx | 4 +- .../ProtocolRunRunTimeParameters.tsx | 14 +- .../EmergencyStop/EstopPressedModal.tsx | 2 +- .../TouchscreenEstopMissingModal.stories.tsx | 5 +- .../TouchscreenEstopPressedModal.stories.tsx | 5 +- .../TerseOffsetTable.stories.tsx | 6 +- .../RobotDashboard/RecentRunProtocolCard.tsx | 2 +- .../FixtureTable.tsx | 2 +- .../ModuleTable.tsx | 2 +- .../AnalysisFailed.stories.tsx | 4 +- .../ResetValuesModal.stories.tsx | 5 +- .../ViewOnlyParameters.tsx | 2 +- .../NetworkSettings/index.tsx | 5 +- app/src/pages/ProtocolDetails/index.tsx | 2 +- .../src/atoms/Chip/Chip.stories.tsx | 10 +- .../src/atoms/Chip/__tests__/Chip.test.tsx | 465 ++++++++++++++++++ {app => components}/src/atoms/Chip/index.tsx | 85 ++-- components/src/atoms/index.ts | 2 + components/src/ui-style-constants/index.ts | 3 +- .../src/ui-style-constants/viewport.ts | 0 43 files changed, 613 insertions(+), 337 deletions(-) delete mode 100644 app/src/atoms/Chip/__tests__/Chip.test.tsx rename {app => components}/src/atoms/Chip/Chip.stories.tsx (83%) create mode 100644 components/src/atoms/Chip/__tests__/Chip.test.tsx rename {app => components}/src/atoms/Chip/index.tsx (56%) rename app/src/DesignTokens/constants.ts => components/src/ui-style-constants/viewport.ts (100%) diff --git a/app/src/atoms/Chip/__tests__/Chip.test.tsx b/app/src/atoms/Chip/__tests__/Chip.test.tsx deleted file mode 100644 index 7f3b75f13c3..00000000000 --- a/app/src/atoms/Chip/__tests__/Chip.test.tsx +++ /dev/null @@ -1,224 +0,0 @@ -import * as React from 'react' -import { describe, it, expect } from 'vitest' -import { screen } from '@testing-library/react' -import { BORDERS, COLORS, SPACING } from '@opentrons/components' -import { renderWithProviders } from '../../../__testing-utils__' -import { Chip } from '..' - -const render = (props: React.ComponentProps) => { - return renderWithProviders() -} - -describe('Chip', () => { - let props: React.ComponentProps - - it('should render text, no icon with basic colors', () => { - props = { - text: 'mockBasic', - type: 'basic', - } - render(props) - const chip = screen.getByTestId('Chip_basic') - const chipText = screen.getByText('mockBasic') - expect(chip).toHaveStyle( - `background-color: ${COLORS.black90}${COLORS.opacity20HexCode}` - ) - expect(chipText).toHaveStyle(`color: ${COLORS.grey60}`) - expect(screen.queryByLabelText('icon_mockBasic')).not.toBeInTheDocument() - }) - - it('should render text, icon, bgcolor with success colors', () => { - props = { - text: 'mockSuccess', - type: 'success', - } - render(props) - const chip = screen.getByTestId('Chip_success') - const chipText = screen.getByText('mockSuccess') - expect(chip).toHaveStyle(`background-color: ${COLORS.green35}`) - expect(chip).toHaveStyle(`border-radius: ${BORDERS.borderRadius40}`) - expect(chipText).toHaveStyle(`color: ${COLORS.green60}`) - const icon = screen.getByLabelText('icon_mockSuccess') - expect(icon).toHaveStyle(`color: ${COLORS.green60}`) - expect(icon).toHaveStyle(`width: 1.5rem`) - }) - - it('should render text, icon, no bgcolor with success colors and bg false', () => { - props = { - background: false, - text: 'mockSuccess', - type: 'success', - } - render(props) - const chip = screen.getByTestId('Chip_success') - const chipText = screen.getByText('mockSuccess') - expect(chip).toHaveStyle(`background-color: ${COLORS.transparent}`) - expect(chip).toHaveStyle(`border-radius: ${BORDERS.borderRadius40}`) - expect(chipText).toHaveStyle(`color: ${COLORS.green60}`) - const icon = screen.getByLabelText('icon_mockSuccess') - expect(icon).toHaveStyle(`color: ${COLORS.green60}`) - }) - - it('should render text, icon, bgcolor with warning colors', () => { - props = { - text: 'mockWarning', - type: 'warning', - } - render(props) - const chip = screen.getByTestId('Chip_warning') - const chipText = screen.getByText('mockWarning') - expect(chip).toHaveStyle(`background-color: ${COLORS.yellow35}`) - expect(chip).toHaveStyle(`border-radius: ${BORDERS.borderRadius40}`) - expect(chipText).toHaveStyle(`color: ${COLORS.yellow60}`) - const icon = screen.getByLabelText('icon_mockWarning') - expect(icon).toHaveStyle(`color: ${COLORS.yellow60}`) - }) - - it('should render text, icon, no bgcolor with warning colors and bg false', () => { - props = { - background: false, - text: 'mockWarning', - type: 'warning', - } - render(props) - const chip = screen.getByTestId('Chip_warning') - const chipText = screen.getByText('mockWarning') - expect(chip).toHaveStyle(`background-color: ${COLORS.transparent}`) - expect(chip).toHaveStyle(`border-radius: ${BORDERS.borderRadius40}`) - expect(chipText).toHaveStyle(`color: ${COLORS.yellow60}`) - const icon = screen.getByLabelText('icon_mockWarning') - expect(icon).toHaveStyle(`color: ${COLORS.yellow60}`) - }) - - it('should render text, icon, bgcolor with neutral colors', () => { - props = { - text: 'mockNeutral', - type: 'neutral', - } - render(props) - const chip = screen.getByTestId('Chip_neutral') - const chipText = screen.getByText('mockNeutral') - expect(chip).toHaveStyle( - `background-color: ${COLORS.black90}${COLORS.opacity20HexCode}` - ) - expect(chip).toHaveStyle(`border-radius: ${BORDERS.borderRadius40}`) - expect(chipText).toHaveStyle(`color: ${COLORS.grey60}`) - const icon = screen.getByLabelText('icon_mockNeutral') - expect(icon).toHaveStyle(`color: ${COLORS.grey60}`) - }) - - it('should render text, icon, no bgcolor with neutral colors and bg false', () => { - props = { - background: false, - text: 'mockNeutral', - type: 'neutral', - } - render(props) - const chip = screen.getByTestId('Chip_neutral') - const chipText = screen.getByText('mockNeutral') - expect(chip).toHaveStyle(`background-color: ${COLORS.transparent}`) - expect(chip).toHaveStyle(`border-radius: ${BORDERS.borderRadius40}`) - expect(chipText).toHaveStyle(`color: ${COLORS.grey60}`) - const icon = screen.getByLabelText('icon_mockNeutral') - expect(icon).toHaveStyle(`color: ${COLORS.grey60}`) - }) - - it('should render text, icon, bgcolor with error colors', () => { - props = { - text: 'mockError', - type: 'error', - } - render(props) - const chip = screen.getByTestId('Chip_error') - const chipText = screen.getByText('mockError') - expect(chip).toHaveStyle(`background-color: ${COLORS.red35}`) - expect(chip).toHaveStyle(`border-radius: ${BORDERS.borderRadius40}`) - expect(chipText).toHaveStyle(`color: ${COLORS.red60}`) - const icon = screen.getByLabelText('icon_mockError') - expect(icon).toHaveStyle(`color: ${COLORS.red60}`) - }) - - it('should render text, icon, no bgcolor with error colors and bg false', () => { - props = { - background: false, - text: 'mockError', - type: 'error', - } - render(props) - const chip = screen.getByTestId('Chip_error') - const chipText = screen.getByText('mockError') - expect(chip).toHaveStyle(`background-color: ${COLORS.transparent}`) - expect(chip).toHaveStyle(`border-radius: ${BORDERS.borderRadius40}`) - expect(chipText).toHaveStyle(`color: ${COLORS.red60}`) - const icon = screen.getByLabelText('icon_mockError') - expect(icon).toHaveStyle(`color: ${COLORS.red60}`) - }) - - it('should render text, icon, bgcolor with info colors', () => { - props = { - text: 'mockInfo', - type: 'info', - } - render(props) - const chip = screen.getByTestId('Chip_info') - const chipText = screen.getByText('mockInfo') - expect(chip).toHaveStyle(`background-color: ${COLORS.blue35}`) - expect(chip).toHaveStyle(`border-radius: ${BORDERS.borderRadius40}`) - expect(chipText).toHaveStyle(`color: ${COLORS.blue60}`) - const icon = screen.getByLabelText('icon_mockInfo') - expect(icon).toHaveStyle(`color: ${COLORS.blue60}`) - }) - - it('should render text, icon, no bgcolor with info colors and bg false', () => { - props = { - background: false, - text: 'mockInfo', - type: 'info', - } - render(props) - const chip = screen.getByTestId('Chip_info') - const chipText = screen.getByText('mockInfo') - expect(chip).toHaveStyle(`background-color: ${COLORS.transparent}`) - expect(chip).toHaveStyle(`border-radius: ${BORDERS.borderRadius40}`) - expect(chipText).toHaveStyle(`color: ${COLORS.blue60}`) - const icon = screen.getByLabelText('icon_mockInfo') - expect(icon).toHaveStyle(`color: ${COLORS.blue60}`) - }) - it('renders no icon when hasIcon is false', () => { - props = { - text: 'mockInfo', - hasIcon: false, - type: 'info', - } - render(props) - expect(screen.queryByText('icon_mockInfo')).not.toBeInTheDocument() - }) - - it('render text with smaller padding and smaller icon when chip size is small and background is false', () => { - props = { - background: false, - text: 'mockInfo', - type: 'info', - chipSize: 'small', - } - render(props) - const chip = screen.getByTestId('Chip_info') - expect(chip).toHaveStyle(`padding: ${SPACING.spacing4} 0`) - const icon = screen.getByLabelText('icon_mockInfo') - expect(icon).toHaveStyle(`width: 1.25rem`) - }) - - it('render text with smaller padding and smaller icon when chip size is small and background is true', () => { - props = { - background: true, - text: 'mockInfo', - type: 'info', - chipSize: 'small', - } - render(props) - const chip = screen.getByTestId('Chip_info') - expect(chip).toHaveStyle(`padding: ${SPACING.spacing4} ${SPACING.spacing8}`) - const icon = screen.getByLabelText('icon_mockInfo') - expect(icon).toHaveStyle(`width: 1.25rem`) - }) -}) diff --git a/app/src/atoms/InlineNotification/InlineNotification.stories.tsx b/app/src/atoms/InlineNotification/InlineNotification.stories.tsx index 313d278c0fa..ec3af22be3e 100644 --- a/app/src/atoms/InlineNotification/InlineNotification.stories.tsx +++ b/app/src/atoms/InlineNotification/InlineNotification.stories.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { touchScreenViewport } from '../../DesignTokens/constants' +import { VIEWPORT } from '@opentrons/components' import { InlineNotification } from '.' import type { Story, Meta } from '@storybook/react' @@ -26,7 +26,7 @@ export default { defaultValue: true, }, }, - parameters: touchScreenViewport, + parameters: VIEWPORT.touchScreenViewport, } as Meta const Template: Story< diff --git a/app/src/atoms/ListItem/ListItem.stories.tsx b/app/src/atoms/ListItem/ListItem.stories.tsx index 0380c5ddb13..1e7704af9d4 100644 --- a/app/src/atoms/ListItem/ListItem.stories.tsx +++ b/app/src/atoms/ListItem/ListItem.stories.tsx @@ -3,9 +3,9 @@ import { DIRECTION_COLUMN, Flex, SPACING, + VIEWPORT, StyledText, } from '@opentrons/components' -import { touchScreenViewport } from '../../DesignTokens/constants' import { ListItem } from '.' import type { Story, Meta } from '@storybook/react' @@ -19,7 +19,7 @@ export default { }, }, }, - parameters: touchScreenViewport, + parameters: VIEWPORT.touchScreenViewport, } as Meta const ListItemTemplate: Story> = args => ( diff --git a/app/src/atoms/Snackbar/Snackbar.stories.tsx b/app/src/atoms/Snackbar/Snackbar.stories.tsx index 1d42d193d64..db73e22d947 100644 --- a/app/src/atoms/Snackbar/Snackbar.stories.tsx +++ b/app/src/atoms/Snackbar/Snackbar.stories.tsx @@ -8,8 +8,8 @@ import { PrimaryButton, SPACING, StyledText, + VIEWPORT, } from '@opentrons/components' -import { touchScreenViewport } from '../../DesignTokens/constants' import { Snackbar } from './index' import type { Story, Meta } from '@storybook/react' @@ -17,7 +17,7 @@ import type { Story, Meta } from '@storybook/react' export default { title: 'ODD/Atoms/Snackbar', component: Snackbar, - parameters: touchScreenViewport, + parameters: VIEWPORT.touchScreenViewport, } as Meta const DefaultTemplate: Story> = args => { diff --git a/app/src/atoms/SoftwareKeyboard/CustomKeyboard/CustomKeyboard.stories.tsx b/app/src/atoms/SoftwareKeyboard/CustomKeyboard/CustomKeyboard.stories.tsx index e298911ee0f..f6e72c00bf9 100644 --- a/app/src/atoms/SoftwareKeyboard/CustomKeyboard/CustomKeyboard.stories.tsx +++ b/app/src/atoms/SoftwareKeyboard/CustomKeyboard/CustomKeyboard.stories.tsx @@ -4,8 +4,8 @@ import { DIRECTION_COLUMN, POSITION_ABSOLUTE, SPACING, + VIEWPORT, } from '@opentrons/components' -import { touchScreenViewport } from '../../../DesignTokens/constants' import { InputField } from '../../InputField' import { CustomKeyboard } from './' import '../index.css' @@ -16,7 +16,7 @@ import type { Story, Meta } from '@storybook/react' export default { title: 'ODD/Atoms/SoftwareKeyboard/CustomKeyboard', component: CustomKeyboard, - parameters: touchScreenViewport, + parameters: VIEWPORT.touchScreenViewport, } as Meta const Template: Story> = args => { diff --git a/app/src/atoms/SoftwareKeyboard/NormalKeyboard/NormalKeyboard.stories.tsx b/app/src/atoms/SoftwareKeyboard/NormalKeyboard/NormalKeyboard.stories.tsx index c245ca23be9..7883d6fbdd0 100644 --- a/app/src/atoms/SoftwareKeyboard/NormalKeyboard/NormalKeyboard.stories.tsx +++ b/app/src/atoms/SoftwareKeyboard/NormalKeyboard/NormalKeyboard.stories.tsx @@ -1,11 +1,11 @@ import * as React from 'react' import { - Flex, DIRECTION_COLUMN, + Flex, POSITION_ABSOLUTE, SPACING, + VIEWPORT, } from '@opentrons/components' -import { touchScreenViewport } from '../../../DesignTokens/constants' import { InputField } from '../../InputField' import { NormalKeyboard } from '.' @@ -17,7 +17,7 @@ import type { Story, Meta } from '@storybook/react' export default { title: 'ODD/Atoms/SoftwareKeyboard/NormalKeyboard', component: NormalKeyboard, - parameters: touchScreenViewport, + parameters: VIEWPORT.touchScreenViewport, } as Meta const Template: Story> = args => { diff --git a/app/src/atoms/SoftwareKeyboard/Numpad/Numpad.stories.tsx b/app/src/atoms/SoftwareKeyboard/Numpad/Numpad.stories.tsx index f87ca54481b..d5a569cd284 100644 --- a/app/src/atoms/SoftwareKeyboard/Numpad/Numpad.stories.tsx +++ b/app/src/atoms/SoftwareKeyboard/Numpad/Numpad.stories.tsx @@ -4,8 +4,8 @@ import { DIRECTION_COLUMN, POSITION_ABSOLUTE, SPACING, + VIEWPORT, } from '@opentrons/components' -import { touchScreenViewport } from '../../../DesignTokens/constants' import { InputField } from '../../InputField' import { Numpad } from './' import '../index.css' @@ -16,7 +16,7 @@ import type { Story, Meta } from '@storybook/react' export default { title: 'ODD/Atoms/SoftwareKeyboard/Numpad', component: Numpad, - parameters: touchScreenViewport, + parameters: VIEWPORT.touchScreenViewport, } as Meta const Template: Story> = args => { diff --git a/app/src/atoms/Toast/ODDToast.stories.tsx b/app/src/atoms/Toast/ODDToast.stories.tsx index e70500bc960..9a0fe8db4e9 100644 --- a/app/src/atoms/Toast/ODDToast.stories.tsx +++ b/app/src/atoms/Toast/ODDToast.stories.tsx @@ -8,15 +8,15 @@ import { PrimaryButton, SPACING, StyledText, + VIEWPORT, } from '@opentrons/components' -import { touchScreenViewport } from '../../DesignTokens/constants' import { Toast } from '.' import type { Story, Meta } from '@storybook/react' export default { title: 'ODD/Atoms/Toast', component: Toast, - parameters: touchScreenViewport, + parameters: VIEWPORT.touchScreenViewport, } as Meta const Template: Story> = args => { diff --git a/app/src/atoms/buttons/FloatingActionButton.stories.tsx b/app/src/atoms/buttons/FloatingActionButton.stories.tsx index 820f1ec9618..a7526805a20 100644 --- a/app/src/atoms/buttons/FloatingActionButton.stories.tsx +++ b/app/src/atoms/buttons/FloatingActionButton.stories.tsx @@ -1,6 +1,5 @@ import * as React from 'react' -import { ICON_DATA_BY_NAME } from '@opentrons/components' -import { touchScreenViewport } from '../../DesignTokens/constants' +import { ICON_DATA_BY_NAME, VIEWPORT } from '@opentrons/components' import { FloatingActionButton } from './' import type { Story, Meta } from '@storybook/react' @@ -17,7 +16,7 @@ export default { }, onClick: { action: 'clicked' }, }, - parameters: touchScreenViewport, + parameters: VIEWPORT.touchScreenViewport, } as Meta const FloatingActionButtonTemplate: Story< diff --git a/app/src/atoms/buttons/LargeButton.stories.tsx b/app/src/atoms/buttons/LargeButton.stories.tsx index 737dada7656..f1f9427a4cf 100644 --- a/app/src/atoms/buttons/LargeButton.stories.tsx +++ b/app/src/atoms/buttons/LargeButton.stories.tsx @@ -1,12 +1,12 @@ import * as React from 'react' -import { touchScreenViewport } from '../../DesignTokens/constants' +import { VIEWPORT } from '@opentrons/components' import { LargeButton } from './' import type { Story, Meta } from '@storybook/react' export default { title: 'ODD/Atoms/Buttons/LargeButton', argTypes: { onClick: { action: 'clicked' } }, - parameters: touchScreenViewport, + parameters: VIEWPORT.touchScreenViewport, } as Meta const LargeButtonTemplate: Story< diff --git a/app/src/atoms/buttons/MediumButton.stories.tsx b/app/src/atoms/buttons/MediumButton.stories.tsx index 17d67f76093..667947b7e08 100644 --- a/app/src/atoms/buttons/MediumButton.stories.tsx +++ b/app/src/atoms/buttons/MediumButton.stories.tsx @@ -1,6 +1,5 @@ import * as React from 'react' -import { ICON_DATA_BY_NAME } from '@opentrons/components' -import { touchScreenViewport } from '../../DesignTokens/constants' +import { ICON_DATA_BY_NAME, VIEWPORT } from '@opentrons/components' import { MediumButton } from './' import type { Story, Meta } from '@storybook/react' @@ -29,7 +28,7 @@ export default { defaultValue: undefined, }, }, - parameters: touchScreenViewport, + parameters: VIEWPORT.touchScreenViewport, } as Meta const MediumButtonTemplate: Story< diff --git a/app/src/atoms/buttons/RadioButton.stories.tsx b/app/src/atoms/buttons/RadioButton.stories.tsx index 7bb570ffae9..3869cb70cc7 100644 --- a/app/src/atoms/buttons/RadioButton.stories.tsx +++ b/app/src/atoms/buttons/RadioButton.stories.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { touchScreenViewport } from '../../DesignTokens/constants' +import { VIEWPORT } from '@opentrons/components' import { RadioButton } from './' import type { Story, Meta } from '@storybook/react' @@ -16,7 +16,7 @@ export default { }, onClick: { action: 'clicked' }, }, - parameters: touchScreenViewport, + parameters: VIEWPORT.touchScreenViewport, } as Meta const RadioButtonTemplate: Story< diff --git a/app/src/atoms/buttons/SmallButton.stories.tsx b/app/src/atoms/buttons/SmallButton.stories.tsx index cb1263f8a6c..f587f7f4e13 100644 --- a/app/src/atoms/buttons/SmallButton.stories.tsx +++ b/app/src/atoms/buttons/SmallButton.stories.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { touchScreenViewport } from '../../DesignTokens/constants' +import { VIEWPORT } from '@opentrons/components' import { SmallButton } from './' import type { Story, Meta } from '@storybook/react' @@ -8,7 +8,7 @@ export default { title: 'ODD/Atoms/Buttons/SmallButton', argTypes: { onClick: { action: 'clicked' } }, component: SmallButton, - parameters: touchScreenViewport, + parameters: VIEWPORT.touchScreenViewport, } as Meta const Template: Story> = args => ( diff --git a/app/src/atoms/buttons/TabbedButton.stories.tsx b/app/src/atoms/buttons/TabbedButton.stories.tsx index 27efbc36a87..60c5131da3b 100644 --- a/app/src/atoms/buttons/TabbedButton.stories.tsx +++ b/app/src/atoms/buttons/TabbedButton.stories.tsx @@ -1,12 +1,12 @@ import * as React from 'react' -import { touchScreenViewport } from '../../DesignTokens/constants' +import { VIEWPORT } from '@opentrons/components' import { TabbedButton } from './' import type { Story, Meta } from '@storybook/react' export default { title: 'ODD/Atoms/Buttons/TabbedButton', argTypes: { onClick: { action: 'clicked' } }, - parameters: touchScreenViewport, + parameters: VIEWPORT.touchScreenViewport, } as Meta const TabbedButtonTemplate: Story< diff --git a/app/src/molecules/BackgroundOverlay/BackgroundOverlay.stories.tsx b/app/src/molecules/BackgroundOverlay/BackgroundOverlay.stories.tsx index 38c9e62baf1..b915e6be59b 100644 --- a/app/src/molecules/BackgroundOverlay/BackgroundOverlay.stories.tsx +++ b/app/src/molecules/BackgroundOverlay/BackgroundOverlay.stories.tsx @@ -1,12 +1,16 @@ import * as React from 'react' -import { Flex, PrimaryButton, StyledText } from '@opentrons/components' -import { touchScreenViewport } from '../../DesignTokens/constants' +import { + Flex, + PrimaryButton, + StyledText, + VIEWPORT, +} from '@opentrons/components' import { BackgroundOverlay } from './index' import type { Story, Meta } from '@storybook/react' export default { title: 'ODD/Molecules/BackgroundOverlay', - parameters: touchScreenViewport, + parameters: VIEWPORT.touchScreenViewport, } as Meta const Template: Story< diff --git a/app/src/molecules/CardButton/CardButton.stories.tsx b/app/src/molecules/CardButton/CardButton.stories.tsx index 38ce4a0f609..3ac71a8e3bf 100644 --- a/app/src/molecules/CardButton/CardButton.stories.tsx +++ b/app/src/molecules/CardButton/CardButton.stories.tsx @@ -1,7 +1,6 @@ import * as React from 'react' import { MemoryRouter } from 'react-router-dom' -import { Flex, SPACING } from '@opentrons/components' -import { touchScreenViewport } from '../../DesignTokens/constants' +import { Flex, SPACING, VIEWPORT } from '@opentrons/components' import { GlobalStyle } from '../../atoms/GlobalStyle' import { CardButton } from '.' @@ -10,7 +9,7 @@ import type { Story, Meta } from '@storybook/react' export default { title: 'ODD/Molecules/CardButton', component: CardButton, - parameters: touchScreenViewport, + parameters: VIEWPORT.touchScreenViewport, decorators: [ Story => ( <> diff --git a/app/src/molecules/Modal/Modal.stories.tsx b/app/src/molecules/Modal/Modal.stories.tsx index e29a6197224..09456d77828 100644 --- a/app/src/molecules/Modal/Modal.stories.tsx +++ b/app/src/molecules/Modal/Modal.stories.tsx @@ -1,6 +1,5 @@ import * as React from 'react' -import { COLORS, Flex, BORDERS, SPACING } from '@opentrons/components' -import { touchScreenViewport } from '../../DesignTokens/constants' +import { COLORS, Flex, BORDERS, SPACING, VIEWPORT } from '@opentrons/components' import { Modal } from './Modal' import type { Story, Meta } from '@storybook/react' @@ -13,7 +12,7 @@ export default { }, onOutsideClick: { action: 'clicked' }, }, - parameters: touchScreenViewport, + parameters: VIEWPORT.touchScreenViewport, } as Meta const Template: Story> = args => ( diff --git a/app/src/molecules/Modal/ModalHeader.stories.tsx b/app/src/molecules/Modal/ModalHeader.stories.tsx index 0beabe6ba1b..92e9c83f9b4 100644 --- a/app/src/molecules/Modal/ModalHeader.stories.tsx +++ b/app/src/molecules/Modal/ModalHeader.stories.tsx @@ -1,6 +1,5 @@ import * as React from 'react' -import { COLORS } from '@opentrons/components' -import { touchScreenViewport } from '../../DesignTokens/constants' +import { COLORS, VIEWPORT } from '@opentrons/components' import { ModalHeader } from './ModalHeader' import type { Story, Meta } from '@storybook/react' @@ -24,7 +23,7 @@ export default { }, onClick: { action: 'clicked' }, }, - parameters: touchScreenViewport, + parameters: VIEWPORT.touchScreenViewport, } as Meta const Template: Story> = args => ( diff --git a/app/src/molecules/Modal/SmallModalChildren.stories.tsx b/app/src/molecules/Modal/SmallModalChildren.stories.tsx index cdea430b18f..c1889ca718e 100644 --- a/app/src/molecules/Modal/SmallModalChildren.stories.tsx +++ b/app/src/molecules/Modal/SmallModalChildren.stories.tsx @@ -1,12 +1,12 @@ import * as React from 'react' -import { touchScreenViewport } from '../../DesignTokens/constants' +import { VIEWPORT } from '@opentrons/components' import { SmallModalChildren } from './SmallModalChildren' import type { Story, Meta } from '@storybook/react' export default { title: 'ODD/Molecules/Modals/SmallModalChildren', argTypes: { onClick: { action: 'clicked' } }, - parameters: touchScreenViewport, + parameters: VIEWPORT.touchScreenViewport, } as Meta const Template: Story< diff --git a/app/src/molecules/ODDBackButton/ODDBackButton.stories.tsx b/app/src/molecules/ODDBackButton/ODDBackButton.stories.tsx index 14a0d050ba5..6fad4d7ae4a 100644 --- a/app/src/molecules/ODDBackButton/ODDBackButton.stories.tsx +++ b/app/src/molecules/ODDBackButton/ODDBackButton.stories.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { touchScreenViewport } from '../../DesignTokens/constants' +import { VIEWPORT } from '@opentrons/components' import { ODDBackButton } from '.' import type { Story, Meta } from '@storybook/react' @@ -8,7 +8,7 @@ export default { argTypes: { onClick: { action: 'clicked' }, }, - parameters: touchScreenViewport, + parameters: VIEWPORT.touchScreenViewport, } as Meta const ODDBackButtonTemplate: Story< diff --git a/app/src/organisms/ChildNavigation/ChildNavigation.stories.tsx b/app/src/organisms/ChildNavigation/ChildNavigation.stories.tsx index c39b4b20dc1..da15b3af90e 100644 --- a/app/src/organisms/ChildNavigation/ChildNavigation.stories.tsx +++ b/app/src/organisms/ChildNavigation/ChildNavigation.stories.tsx @@ -1,12 +1,12 @@ import * as React from 'react' -import { touchScreenViewport } from '../../DesignTokens/constants' +import { VIEWPORT } from '@opentrons/components' import { SmallButton } from '../../atoms/buttons' import { ChildNavigation } from '.' import type { Story, Meta } from '@storybook/react' export default { title: 'ODD/Organisms/ChildNavigation', - parameters: touchScreenViewport, + parameters: VIEWPORT.touchScreenViewport, } as Meta const Template: Story> = args => ( diff --git a/app/src/organisms/DeviceDetailsDeckConfiguration/AddFixtureModal.stories.tsx b/app/src/organisms/DeviceDetailsDeckConfiguration/AddFixtureModal.stories.tsx index cc5ddd4f4e7..034a18c1e77 100644 --- a/app/src/organisms/DeviceDetailsDeckConfiguration/AddFixtureModal.stories.tsx +++ b/app/src/organisms/DeviceDetailsDeckConfiguration/AddFixtureModal.stories.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { QueryClient, QueryClientProvider } from 'react-query' -import { touchScreenViewport } from '../../DesignTokens/constants' +import { VIEWPORT } from '@opentrons/components' import { AddFixtureModal } from './AddFixtureModal' import type { Story, Meta } from '@storybook/react' @@ -13,7 +13,7 @@ export default { }, onOutsideClick: { action: 'clicked' }, }, - parameters: touchScreenViewport, + parameters: VIEWPORT.touchScreenViewport, } as Meta const queryClient = new QueryClient() diff --git a/app/src/organisms/DeviceDetailsDeckConfiguration/DeckConfigurationDiscardChangesModal.stories.tsx b/app/src/organisms/DeviceDetailsDeckConfiguration/DeckConfigurationDiscardChangesModal.stories.tsx index d6b26521619..0fdee52a94e 100644 --- a/app/src/organisms/DeviceDetailsDeckConfiguration/DeckConfigurationDiscardChangesModal.stories.tsx +++ b/app/src/organisms/DeviceDetailsDeckConfiguration/DeckConfigurationDiscardChangesModal.stories.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { touchScreenViewport } from '../../DesignTokens/constants' +import { VIEWPORT } from '@opentrons/components' import { DeckConfigurationDiscardChangesModal } from './DeckConfigurationDiscardChangesModal' import type { Story, Meta } from '@storybook/react' @@ -12,7 +12,7 @@ export default { }, onOutsideClick: { action: 'clicked' }, }, - parameters: touchScreenViewport, + parameters: VIEWPORT.touchScreenViewport, } as Meta const Template: Story< diff --git a/app/src/organisms/DeviceDetailsDeckConfiguration/TouchScreenDeckFixtureSetupInstructionModal.stories.tsx b/app/src/organisms/DeviceDetailsDeckConfiguration/TouchScreenDeckFixtureSetupInstructionModal.stories.tsx index 5fcc8d339a9..ec078d74eea 100644 --- a/app/src/organisms/DeviceDetailsDeckConfiguration/TouchScreenDeckFixtureSetupInstructionModal.stories.tsx +++ b/app/src/organisms/DeviceDetailsDeckConfiguration/TouchScreenDeckFixtureSetupInstructionModal.stories.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { touchScreenViewport } from '../../DesignTokens/constants' +import { VIEWPORT } from '@opentrons/components' import { DeckFixtureSetupInstructionsModal } from './DeckFixtureSetupInstructionsModal' import type { Story, Meta } from '@storybook/react' @@ -12,7 +12,7 @@ export default { }, onOutsideClick: { action: 'clicked' }, }, - parameters: touchScreenViewport, + parameters: VIEWPORT.touchScreenViewport, } as Meta const Template: Story< diff --git a/app/src/organisms/Devices/ProtocolRun/ProtocolRunRunTimeParameters.tsx b/app/src/organisms/Devices/ProtocolRun/ProtocolRunRunTimeParameters.tsx index 0b3ccb5c141..e3153e39a85 100644 --- a/app/src/organisms/Devices/ProtocolRun/ProtocolRunRunTimeParameters.tsx +++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunRunTimeParameters.tsx @@ -6,6 +6,7 @@ import { formatRunTimeParameterValue } from '@opentrons/shared-data' import { ALIGN_CENTER, BORDERS, + Chip, COLORS, DIRECTION_COLUMN, DIRECTION_ROW, @@ -18,7 +19,6 @@ import { import { Banner } from '../../../atoms/Banner' import { Divider } from '../../../atoms/structure' -// import { Chip } from '../../../atoms/Chip' import { useMostRecentCompletedAnalysis } from '../../LabwarePositionCheck/useMostRecentCompletedAnalysis' import type { RunTimeParameter } from '@opentrons/shared-data' @@ -257,10 +257,14 @@ export function ProtocolRunRuntimeParameters({ {formatRunTimeParameterValue(parameter, t)} - {/* ToDo (kk:03/19/2024) chip will be here with conditional render */} - {/* {index % 2 === 0 ? ( - - ) : null} */} + {/* ToDo (kk:03/19/2024) need to implement a logic when be is ready */} + {index % 2 === 0 ? ( + + ) : null} diff --git a/app/src/organisms/EmergencyStop/EstopPressedModal.tsx b/app/src/organisms/EmergencyStop/EstopPressedModal.tsx index dfec8424ed0..cb32ae550b9 100644 --- a/app/src/organisms/EmergencyStop/EstopPressedModal.tsx +++ b/app/src/organisms/EmergencyStop/EstopPressedModal.tsx @@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next' import { ALIGN_CENTER, BORDERS, + Chip, COLORS, DIRECTION_COLUMN, DIRECTION_ROW, @@ -22,7 +23,6 @@ import { useAcknowledgeEstopDisengageMutation } from '@opentrons/react-api-clien import { getTopPortalEl } from '../../App/portal' import { Banner } from '../../atoms/Banner' -import { Chip } from '../../atoms/Chip' import { ListItem } from '../../atoms/ListItem' import { SmallButton } from '../../atoms/buttons' import { LegacyModal } from '../../molecules/LegacyModal' diff --git a/app/src/organisms/EmergencyStop/TouchscreenEstopMissingModal.stories.tsx b/app/src/organisms/EmergencyStop/TouchscreenEstopMissingModal.stories.tsx index 0dd2f63e1d3..f2bb0cf2e7f 100644 --- a/app/src/organisms/EmergencyStop/TouchscreenEstopMissingModal.stories.tsx +++ b/app/src/organisms/EmergencyStop/TouchscreenEstopMissingModal.stories.tsx @@ -2,7 +2,8 @@ import * as React from 'react' import { Provider } from 'react-redux' import { createStore } from 'redux' -import { touchScreenViewport } from '../../DesignTokens/constants' +import { VIEWPORT } from '@opentrons/components' + import { configReducer } from '../../redux/config/reducer' import { EstopMissingModal } from '.' @@ -12,7 +13,7 @@ import type { Story, Meta } from '@storybook/react' export default { title: 'ODD/Organisms/EstopMissingModal', component: EstopMissingModal, - parameters: touchScreenViewport, + parameters: VIEWPORT.touchScreenViewport, } as Meta const dummyConfig = { diff --git a/app/src/organisms/EmergencyStop/TouchscreenEstopPressedModal.stories.tsx b/app/src/organisms/EmergencyStop/TouchscreenEstopPressedModal.stories.tsx index c2dcf554f65..7ea8618203d 100644 --- a/app/src/organisms/EmergencyStop/TouchscreenEstopPressedModal.stories.tsx +++ b/app/src/organisms/EmergencyStop/TouchscreenEstopPressedModal.stories.tsx @@ -3,7 +3,8 @@ import { Provider } from 'react-redux' import { createStore } from 'redux' import { QueryClient, QueryClientProvider } from 'react-query' -import { touchScreenViewport } from '../../DesignTokens/constants' +import { VIEWPORT } from '@opentrons/components' + import { configReducer } from '../../redux/config/reducer' import { EstopPressedModal } from '.' @@ -13,7 +14,7 @@ import type { Story, Meta } from '@storybook/react' export default { title: 'ODD/Organisms/EstopPressedModal', component: EstopPressedModal, - parameters: touchScreenViewport, + parameters: VIEWPORT.touchScreenViewport, } as Meta const dummyConfig = { diff --git a/app/src/organisms/LabwarePositionCheck/TerseOffsetTable.stories.tsx b/app/src/organisms/LabwarePositionCheck/TerseOffsetTable.stories.tsx index 2077ce88598..8acb76eee45 100644 --- a/app/src/organisms/LabwarePositionCheck/TerseOffsetTable.stories.tsx +++ b/app/src/organisms/LabwarePositionCheck/TerseOffsetTable.stories.tsx @@ -5,24 +5,24 @@ import { Flex, JUSTIFY_SPACE_BETWEEN, SPACING, + VIEWPORT, } from '@opentrons/components' import { fixture12Trough, fixtureTiprack10ul, - LabwareDefinition2, getLabwareDefURI, } from '@opentrons/shared-data' -import { touchScreenViewport } from '../../DesignTokens/constants' import { SmallButton } from '../../atoms/buttons' import { TerseOffsetTable } from './ResultsSummary' import type { Story, Meta } from '@storybook/react' +import type { LabwareDefinition2 } from '@opentrons/shared-data' export default { title: 'ODD/Organisms/TerseOffsetTable', component: TerseOffsetTable, - parameters: touchScreenViewport, + parameters: VIEWPORT.touchScreenViewport, } as Meta // Note: 59rem(944px) is the size of ODD diff --git a/app/src/organisms/OnDeviceDisplay/RobotDashboard/RecentRunProtocolCard.tsx b/app/src/organisms/OnDeviceDisplay/RobotDashboard/RecentRunProtocolCard.tsx index 6120614f954..df77e460792 100644 --- a/app/src/organisms/OnDeviceDisplay/RobotDashboard/RecentRunProtocolCard.tsx +++ b/app/src/organisms/OnDeviceDisplay/RobotDashboard/RecentRunProtocolCard.tsx @@ -7,6 +7,7 @@ import { formatDistance } from 'date-fns' import { BORDERS, COLORS, + Chip, DIRECTION_COLUMN, Flex, Icon, @@ -26,7 +27,6 @@ import { RunStatus, } from '@opentrons/api-client' -import { Chip } from '../../../atoms/Chip' import { ODD_FOCUS_VISIBLE } from '../../../atoms/buttons//constants' import { useTrackEvent } from '../../../redux/analytics' import { Skeleton } from '../../../atoms/Skeleton' diff --git a/app/src/organisms/ProtocolSetupModulesAndDeck/FixtureTable.tsx b/app/src/organisms/ProtocolSetupModulesAndDeck/FixtureTable.tsx index 46d774f3857..e2dbb107379 100644 --- a/app/src/organisms/ProtocolSetupModulesAndDeck/FixtureTable.tsx +++ b/app/src/organisms/ProtocolSetupModulesAndDeck/FixtureTable.tsx @@ -4,6 +4,7 @@ import { ALIGN_CENTER, BORDERS, COLORS, + Chip, DIRECTION_COLUMN, DIRECTION_ROW, Flex, @@ -21,7 +22,6 @@ import { } from '@opentrons/shared-data' import { SmallButton } from '../../atoms/buttons' -import { Chip } from '../../atoms/Chip' import { useDeckConfigurationCompatibility } from '../../resources/deck_configuration/hooks' import { getRequiredDeckConfig } from '../../resources/deck_configuration/utils' import { LocationConflictModal } from '../Devices/ProtocolRun/SetupModuleAndDeck/LocationConflictModal' diff --git a/app/src/organisms/ProtocolSetupModulesAndDeck/ModuleTable.tsx b/app/src/organisms/ProtocolSetupModulesAndDeck/ModuleTable.tsx index 15116f33518..a39edf62ed1 100644 --- a/app/src/organisms/ProtocolSetupModulesAndDeck/ModuleTable.tsx +++ b/app/src/organisms/ProtocolSetupModulesAndDeck/ModuleTable.tsx @@ -6,6 +6,7 @@ import { ALIGN_CENTER, BORDERS, COLORS, + Chip, DIRECTION_COLUMN, Flex, Icon, @@ -29,7 +30,6 @@ import { } from '@opentrons/shared-data' import { SmallButton } from '../../atoms/buttons' -import { Chip } from '../../atoms/Chip' import { getModulePrepCommands } from '../../organisms/Devices/getModulePrepCommands' import { getModuleTooHot } from '../../organisms/Devices/getModuleTooHot' import { useRunCalibrationStatus } from '../../organisms/Devices/hooks' diff --git a/app/src/organisms/ProtocolSetupParameters/AnalysisFailed.stories.tsx b/app/src/organisms/ProtocolSetupParameters/AnalysisFailed.stories.tsx index 9360f1532a1..2b865e5fb9c 100644 --- a/app/src/organisms/ProtocolSetupParameters/AnalysisFailed.stories.tsx +++ b/app/src/organisms/ProtocolSetupParameters/AnalysisFailed.stories.tsx @@ -1,6 +1,6 @@ import * as React from 'react' -import { touchScreenViewport } from '../../DesignTokens/constants' +import { VIEWPORT } from '../../../../components/src/ui-style-constants' import { AnalysisFailedModal } from './AnalysisFailedModal' import type { Story, Meta } from '@storybook/react' @@ -8,7 +8,7 @@ import type { Story, Meta } from '@storybook/react' export default { title: 'ODD/Organisms/AnalysisFailedModal', component: AnalysisFailedModal, - parameters: touchScreenViewport, + parameters: VIEWPORT.touchScreenViewport, } as Meta const Template: Story< diff --git a/app/src/organisms/ProtocolSetupParameters/ResetValuesModal.stories.tsx b/app/src/organisms/ProtocolSetupParameters/ResetValuesModal.stories.tsx index ae7454efc47..975d8104a26 100644 --- a/app/src/organisms/ProtocolSetupParameters/ResetValuesModal.stories.tsx +++ b/app/src/organisms/ProtocolSetupParameters/ResetValuesModal.stories.tsx @@ -1,6 +1,5 @@ import * as React from 'react' - -import { touchScreenViewport } from '../../DesignTokens/constants' +import { VIEWPORT } from '@opentrons/components' import { ResetValuesModal } from './ResetValuesModal' import type { Story, Meta } from '@storybook/react' @@ -8,7 +7,7 @@ import type { Story, Meta } from '@storybook/react' export default { title: 'ODD/Organisms/ResetValuesModal', component: ResetValuesModal, - parameters: touchScreenViewport, + parameters: VIEWPORT.touchScreenViewport, } as Meta const Template: Story> = args => ( diff --git a/app/src/organisms/ProtocolSetupParameters/ViewOnlyParameters.tsx b/app/src/organisms/ProtocolSetupParameters/ViewOnlyParameters.tsx index 8eea44ba0cd..e8aca7d8c9c 100644 --- a/app/src/organisms/ProtocolSetupParameters/ViewOnlyParameters.tsx +++ b/app/src/organisms/ProtocolSetupParameters/ViewOnlyParameters.tsx @@ -4,6 +4,7 @@ import { formatRunTimeParameterValue } from '@opentrons/shared-data' import { ALIGN_CENTER, BORDERS, + Chip, COLORS, DIRECTION_COLUMN, DIRECTION_ROW, @@ -14,7 +15,6 @@ import { } from '@opentrons/components' import { useMostRecentCompletedAnalysis } from '../LabwarePositionCheck/useMostRecentCompletedAnalysis' import { ChildNavigation } from '../ChildNavigation' -import { Chip } from '../../atoms/Chip' import { useToaster } from '../ToasterOven' import { mockData } from './index' diff --git a/app/src/organisms/RobotSettingsDashboard/NetworkSettings/index.tsx b/app/src/organisms/RobotSettingsDashboard/NetworkSettings/index.tsx index 9fdd651eb5d..11c2a13d783 100644 --- a/app/src/organisms/RobotSettingsDashboard/NetworkSettings/index.tsx +++ b/app/src/organisms/RobotSettingsDashboard/NetworkSettings/index.tsx @@ -6,6 +6,7 @@ import { ALIGN_CENTER, BORDERS, Btn, + Chip, COLORS, DIRECTION_COLUMN, DIRECTION_ROW, @@ -16,12 +17,10 @@ import { TYPOGRAPHY, } from '@opentrons/components' -import { Chip } from '../../../atoms/Chip' import { ChildNavigation } from '../../../organisms/ChildNavigation' -import type { IconName } from '@opentrons/components' +import type { IconName, ChipType } from '@opentrons/components' import type { NetworkConnection } from '../../../resources/networking/hooks/useNetworkConnection' -import type { ChipType } from '../../../atoms/Chip' import type { SetSettingOption } from '../../../pages/RobotSettingsDashboard' export type ConnectionType = 'wifi' | 'ethernet' | 'usb' diff --git a/app/src/pages/ProtocolDetails/index.tsx b/app/src/pages/ProtocolDetails/index.tsx index a919df19e9d..e44e3f7015b 100644 --- a/app/src/pages/ProtocolDetails/index.tsx +++ b/app/src/pages/ProtocolDetails/index.tsx @@ -9,6 +9,7 @@ import { ALIGN_CENTER, BORDERS, Btn, + Chip, COLORS, DIRECTION_COLUMN, DIRECTION_ROW, @@ -31,7 +32,6 @@ import { } from '@opentrons/react-api-client' import { MAXIMUM_PINNED_PROTOCOLS } from '../../App/constants' import { MediumButton, SmallButton, TabbedButton } from '../../atoms/buttons' -import { Chip } from '../../atoms/Chip' import { ProtocolDetailsHeaderChipSkeleton, ProcotolDetailsHeaderTitleSkeleton, diff --git a/app/src/atoms/Chip/Chip.stories.tsx b/components/src/atoms/Chip/Chip.stories.tsx similarity index 83% rename from app/src/atoms/Chip/Chip.stories.tsx rename to components/src/atoms/Chip/Chip.stories.tsx index 26cb9025911..2868d7246f7 100644 --- a/app/src/atoms/Chip/Chip.stories.tsx +++ b/components/src/atoms/Chip/Chip.stories.tsx @@ -1,11 +1,13 @@ import * as React from 'react' -import { Flex, COLORS, SPACING } from '@opentrons/components' -import { touchScreenViewport } from '../../DesignTokens/constants' + +import { Flex } from '../../primitives' +import { COLORS } from '../../helix-design-system' +import { SPACING, VIEWPORT } from '../../ui-style-constants' import { Chip } from '.' import type { Meta, StoryObj } from '@storybook/react' const meta: Meta = { - title: 'ODD/Atoms/Chip', + title: 'Library/Atoms/Chip', argTypes: { type: { options: ['basic', 'error', 'info', 'neutral', 'success', 'warning'], @@ -36,7 +38,7 @@ const meta: Meta = { }, }, component: Chip, - parameters: touchScreenViewport, + parameters: VIEWPORT.touchScreenViewport, decorators: [ Story => ( ) => { + return renderWithProviders() +} + +describe('Chip Touchscreen', () => { + let props: React.ComponentProps + + it('should render text, no icon with basic colors', () => { + props = { + text: 'mockBasic', + type: 'basic', + } + render(props) + const chip = screen.getByTestId('Chip_basic') + const chipText = screen.getByText('mockBasic') + expect(chip).toHaveStyle( + `background-color: ${COLORS.black90}${COLORS.opacity20HexCode}` + ) + expect(chipText).toHaveStyle(`color: ${COLORS.grey60}`) + // ToDo (kk:03/28/2024) seems that jsdom doesn't support switching via media query + // I will keep investigating this + // expect(chipText).toHaveStyle( + // `padding: ${SPACING.spacing8} ${SPACING.spacing16}` + // ) + expect(screen.queryByLabelText('icon_mockBasic')).not.toBeInTheDocument() + }) + + it('should render text, icon, bgcolor with success colors', () => { + props = { + text: 'mockSuccess', + type: 'success', + } + render(props) + const chip = screen.getByTestId('Chip_success') + const chipText = screen.getByText('mockSuccess') + expect(chip).toHaveStyle(`background-color: ${COLORS.green35}`) + expect(chip).toHaveStyle(`border-radius: ${BORDERS.borderRadius40}`) + expect(chipText).toHaveStyle(`color: ${COLORS.green60}`) + const icon = screen.getByLabelText('icon_mockSuccess') + expect(icon).toHaveStyle(`color: ${COLORS.green60}`) + // ToDo (kk:03/28/2024) seems that jsdom doesn't support switching via media query + // I will keep investigating this + // expect(icon).toHaveStyle(`width: 1.5rem`) + }) + + it('should render text, icon, no bgcolor with success colors and bg false', () => { + props = { + background: false, + text: 'mockSuccess', + type: 'success', + } + render(props) + const chip = screen.getByTestId('Chip_success') + const chipText = screen.getByText('mockSuccess') + expect(chip).toHaveStyle(`background-color: ${COLORS.transparent}`) + expect(chip).toHaveStyle(`border-radius: ${BORDERS.borderRadius40}`) + expect(chipText).toHaveStyle(`color: ${COLORS.green60}`) + const icon = screen.getByLabelText('icon_mockSuccess') + expect(icon).toHaveStyle(`color: ${COLORS.green60}`) + }) + + it('should render text, icon, bgcolor with warning colors', () => { + props = { + text: 'mockWarning', + type: 'warning', + } + render(props) + const chip = screen.getByTestId('Chip_warning') + const chipText = screen.getByText('mockWarning') + expect(chip).toHaveStyle(`background-color: ${COLORS.yellow35}`) + expect(chip).toHaveStyle(`border-radius: ${BORDERS.borderRadius40}`) + expect(chipText).toHaveStyle(`color: ${COLORS.yellow60}`) + const icon = screen.getByLabelText('icon_mockWarning') + expect(icon).toHaveStyle(`color: ${COLORS.yellow60}`) + }) + + it('should render text, icon, no bgcolor with warning colors and bg false', () => { + props = { + background: false, + text: 'mockWarning', + type: 'warning', + } + render(props) + const chip = screen.getByTestId('Chip_warning') + const chipText = screen.getByText('mockWarning') + expect(chip).toHaveStyle(`background-color: ${COLORS.transparent}`) + expect(chip).toHaveStyle(`border-radius: ${BORDERS.borderRadius40}`) + expect(chipText).toHaveStyle(`color: ${COLORS.yellow60}`) + const icon = screen.getByLabelText('icon_mockWarning') + expect(icon).toHaveStyle(`color: ${COLORS.yellow60}`) + }) + + it('should render text, icon, bgcolor with neutral colors', () => { + props = { + text: 'mockNeutral', + type: 'neutral', + } + render(props) + const chip = screen.getByTestId('Chip_neutral') + const chipText = screen.getByText('mockNeutral') + expect(chip).toHaveStyle( + `background-color: ${COLORS.black90}${COLORS.opacity20HexCode}` + ) + expect(chip).toHaveStyle(`border-radius: ${BORDERS.borderRadius40}`) + expect(chipText).toHaveStyle(`color: ${COLORS.grey60}`) + const icon = screen.getByLabelText('icon_mockNeutral') + expect(icon).toHaveStyle(`color: ${COLORS.grey60}`) + }) + + it('should render text, icon, no bgcolor with neutral colors and bg false', () => { + props = { + background: false, + text: 'mockNeutral', + type: 'neutral', + } + render(props) + const chip = screen.getByTestId('Chip_neutral') + const chipText = screen.getByText('mockNeutral') + expect(chip).toHaveStyle(`background-color: ${COLORS.transparent}`) + expect(chip).toHaveStyle(`border-radius: ${BORDERS.borderRadius40}`) + expect(chipText).toHaveStyle(`color: ${COLORS.grey60}`) + const icon = screen.getByLabelText('icon_mockNeutral') + expect(icon).toHaveStyle(`color: ${COLORS.grey60}`) + }) + + it('should render text, icon, bgcolor with error colors', () => { + props = { + text: 'mockError', + type: 'error', + } + render(props) + const chip = screen.getByTestId('Chip_error') + const chipText = screen.getByText('mockError') + expect(chip).toHaveStyle(`background-color: ${COLORS.red35}`) + expect(chip).toHaveStyle(`border-radius: ${BORDERS.borderRadius40}`) + expect(chipText).toHaveStyle(`color: ${COLORS.red60}`) + const icon = screen.getByLabelText('icon_mockError') + expect(icon).toHaveStyle(`color: ${COLORS.red60}`) + }) + + it('should render text, icon, no bgcolor with error colors and bg false', () => { + props = { + background: false, + text: 'mockError', + type: 'error', + } + render(props) + const chip = screen.getByTestId('Chip_error') + const chipText = screen.getByText('mockError') + expect(chip).toHaveStyle(`background-color: ${COLORS.transparent}`) + expect(chip).toHaveStyle(`border-radius: ${BORDERS.borderRadius40}`) + expect(chipText).toHaveStyle(`color: ${COLORS.red60}`) + const icon = screen.getByLabelText('icon_mockError') + expect(icon).toHaveStyle(`color: ${COLORS.red60}`) + }) + + it('should render text, icon, bgcolor with info colors', () => { + props = { + text: 'mockInfo', + type: 'info', + } + render(props) + const chip = screen.getByTestId('Chip_info') + const chipText = screen.getByText('mockInfo') + expect(chip).toHaveStyle(`background-color: ${COLORS.blue35}`) + expect(chip).toHaveStyle(`border-radius: ${BORDERS.borderRadius40}`) + expect(chipText).toHaveStyle(`color: ${COLORS.blue60}`) + const icon = screen.getByLabelText('icon_mockInfo') + expect(icon).toHaveStyle(`color: ${COLORS.blue60}`) + }) + + it('should render text, icon, no bgcolor with info colors and bg false', () => { + props = { + background: false, + text: 'mockInfo', + type: 'info', + } + render(props) + const chip = screen.getByTestId('Chip_info') + const chipText = screen.getByText('mockInfo') + expect(chip).toHaveStyle(`background-color: ${COLORS.transparent}`) + expect(chip).toHaveStyle(`border-radius: ${BORDERS.borderRadius40}`) + expect(chipText).toHaveStyle(`color: ${COLORS.blue60}`) + const icon = screen.getByLabelText('icon_mockInfo') + expect(icon).toHaveStyle(`color: ${COLORS.blue60}`) + }) + it('renders no icon when hasIcon is false', () => { + props = { + text: 'mockInfo', + hasIcon: false, + type: 'info', + } + render(props) + expect(screen.queryByText('icon_mockInfo')).not.toBeInTheDocument() + }) + + it('render text with smaller padding and smaller icon when chip size is small and background is false', () => { + props = { + background: false, + text: 'mockInfo', + type: 'info', + chipSize: 'small', + } + render(props) + const chip = screen.getByTestId('Chip_info') + expect(chip).toHaveStyle(`padding: ${SPACING.spacing4} 0`) + const icon = screen.getByLabelText('icon_mockInfo') + expect(icon).toHaveStyle(`width: 0.75rem`) + }) + + // ToDo (kk:03/28/2024) seems that jsdom doesn't support switching via media query + // I will keep investigating this + // it('render text with smaller padding and smaller icon when chip size is small and background is true', () => { + // props = { + // background: true, + // text: 'mockInfo', + // type: 'info', + // chipSize: 'small', + // } + // render(props) + // const chip = screen.getByTestId('Chip_info') + // expect(chip).toHaveStyle(`padding: ${SPACING.spacing4} ${SPACING.spacing8}`) + // const icon = screen.getByLabelText('icon_mockInfo') + // expect(icon).toHaveStyle(`width: 1.25rem`) + // }) +}) + +describe('Chip Web', () => { + let props: React.ComponentProps + + beforeEach(() => { + Object.defineProperty(window, 'innerWidth', { + writable: true, + configurable: true, + value: 1024, + }) + + Object.defineProperty(window, 'innerHeight', { + writable: true, + configurable: true, + value: 768, + }) + }) + + it('should render text, no icon with basic colors', () => { + props = { + text: 'mockBasic', + type: 'basic', + } + render(props) + const chip = screen.getByTestId('Chip_basic') + const chipText = screen.getByText('mockBasic') + expect(chip).toHaveStyle( + `background-color: ${COLORS.black90}${COLORS.opacity20HexCode}` + ) + expect(chipText).toHaveStyle(`color: ${COLORS.grey60}`) + expect(screen.queryByLabelText('icon_mockBasic')).not.toBeInTheDocument() + }) + + it('should render text, icon, bgcolor with success colors', () => { + props = { + text: 'mockSuccess', + type: 'success', + } + render(props) + const chip = screen.getByTestId('Chip_success') + const chipText = screen.getByText('mockSuccess') + expect(chip).toHaveStyle(`background-color: ${COLORS.green35}`) + expect(chip).toHaveStyle(`border-radius: ${BORDERS.borderRadius40}`) + expect(chipText).toHaveStyle(`color: ${COLORS.green60}`) + // expect(chipText).toHaveStyle( + // `padding: ${SPACING.spacing2} ${SPACING.spacing8}` + // ) + const icon = screen.getByLabelText('icon_mockSuccess') + expect(icon).toHaveStyle(`color: ${COLORS.green60}`) + expect(icon).toHaveStyle(`width: 1rem`) + }) + + it('should render text, icon, no bgcolor with success colors and bg false', () => { + props = { + background: false, + text: 'mockSuccess', + type: 'success', + } + render(props) + const chip = screen.getByTestId('Chip_success') + const chipText = screen.getByText('mockSuccess') + expect(chip).toHaveStyle(`background-color: ${COLORS.transparent}`) + expect(chip).toHaveStyle(`border-radius: ${BORDERS.borderRadius40}`) + expect(chipText).toHaveStyle(`color: ${COLORS.green60}`) + const icon = screen.getByLabelText('icon_mockSuccess') + expect(icon).toHaveStyle(`color: ${COLORS.green60}`) + }) + + it('should render text, icon, bgcolor with warning colors', () => { + props = { + text: 'mockWarning', + type: 'warning', + } + render(props) + const chip = screen.getByTestId('Chip_warning') + const chipText = screen.getByText('mockWarning') + expect(chip).toHaveStyle(`background-color: ${COLORS.yellow35}`) + expect(chip).toHaveStyle(`border-radius: ${BORDERS.borderRadius40}`) + expect(chipText).toHaveStyle(`color: ${COLORS.yellow60}`) + const icon = screen.getByLabelText('icon_mockWarning') + expect(icon).toHaveStyle(`color: ${COLORS.yellow60}`) + }) + + it('should render text, icon, no bgcolor with warning colors and bg false', () => { + props = { + background: false, + text: 'mockWarning', + type: 'warning', + } + render(props) + const chip = screen.getByTestId('Chip_warning') + const chipText = screen.getByText('mockWarning') + expect(chip).toHaveStyle(`background-color: ${COLORS.transparent}`) + expect(chip).toHaveStyle(`border-radius: ${BORDERS.borderRadius40}`) + expect(chipText).toHaveStyle(`color: ${COLORS.yellow60}`) + const icon = screen.getByLabelText('icon_mockWarning') + expect(icon).toHaveStyle(`color: ${COLORS.yellow60}`) + }) + + it('should render text, icon, bgcolor with neutral colors', () => { + props = { + text: 'mockNeutral', + type: 'neutral', + } + render(props) + const chip = screen.getByTestId('Chip_neutral') + const chipText = screen.getByText('mockNeutral') + expect(chip).toHaveStyle( + `background-color: ${COLORS.black90}${COLORS.opacity20HexCode}` + ) + expect(chip).toHaveStyle(`border-radius: ${BORDERS.borderRadius40}`) + expect(chipText).toHaveStyle(`color: ${COLORS.grey60}`) + const icon = screen.getByLabelText('icon_mockNeutral') + expect(icon).toHaveStyle(`color: ${COLORS.grey60}`) + }) + + it('should render text, icon, no bgcolor with neutral colors and bg false', () => { + props = { + background: false, + text: 'mockNeutral', + type: 'neutral', + } + render(props) + const chip = screen.getByTestId('Chip_neutral') + const chipText = screen.getByText('mockNeutral') + expect(chip).toHaveStyle(`background-color: ${COLORS.transparent}`) + expect(chip).toHaveStyle(`border-radius: ${BORDERS.borderRadius40}`) + expect(chipText).toHaveStyle(`color: ${COLORS.grey60}`) + const icon = screen.getByLabelText('icon_mockNeutral') + expect(icon).toHaveStyle(`color: ${COLORS.grey60}`) + }) + + it('should render text, icon, bgcolor with error colors', () => { + props = { + text: 'mockError', + type: 'error', + } + render(props) + const chip = screen.getByTestId('Chip_error') + const chipText = screen.getByText('mockError') + expect(chip).toHaveStyle(`background-color: ${COLORS.red35}`) + expect(chip).toHaveStyle(`border-radius: ${BORDERS.borderRadius40}`) + expect(chipText).toHaveStyle(`color: ${COLORS.red60}`) + const icon = screen.getByLabelText('icon_mockError') + expect(icon).toHaveStyle(`color: ${COLORS.red60}`) + }) + + it('should render text, icon, no bgcolor with error colors and bg false', () => { + props = { + background: false, + text: 'mockError', + type: 'error', + } + render(props) + const chip = screen.getByTestId('Chip_error') + const chipText = screen.getByText('mockError') + expect(chip).toHaveStyle(`background-color: ${COLORS.transparent}`) + expect(chip).toHaveStyle(`border-radius: ${BORDERS.borderRadius40}`) + expect(chipText).toHaveStyle(`color: ${COLORS.red60}`) + const icon = screen.getByLabelText('icon_mockError') + expect(icon).toHaveStyle(`color: ${COLORS.red60}`) + }) + + it('should render text, icon, bgcolor with info colors', () => { + props = { + text: 'mockInfo', + type: 'info', + } + render(props) + const chip = screen.getByTestId('Chip_info') + const chipText = screen.getByText('mockInfo') + expect(chip).toHaveStyle(`background-color: ${COLORS.blue35}`) + expect(chip).toHaveStyle(`border-radius: ${BORDERS.borderRadius40}`) + expect(chipText).toHaveStyle(`color: ${COLORS.blue60}`) + const icon = screen.getByLabelText('icon_mockInfo') + expect(icon).toHaveStyle(`color: ${COLORS.blue60}`) + }) + + it('should render text, icon, no bgcolor with info colors and bg false', () => { + props = { + background: false, + text: 'mockInfo', + type: 'info', + } + render(props) + const chip = screen.getByTestId('Chip_info') + const chipText = screen.getByText('mockInfo') + expect(chip).toHaveStyle(`background-color: ${COLORS.transparent}`) + expect(chip).toHaveStyle(`border-radius: ${BORDERS.borderRadius40}`) + expect(chipText).toHaveStyle(`color: ${COLORS.blue60}`) + const icon = screen.getByLabelText('icon_mockInfo') + expect(icon).toHaveStyle(`color: ${COLORS.blue60}`) + }) + it('renders no icon when hasIcon is false', () => { + props = { + text: 'mockInfo', + hasIcon: false, + type: 'info', + } + render(props) + expect(screen.queryByText('icon_mockInfo')).not.toBeInTheDocument() + }) + + it('render text with smaller padding and smaller icon when chip size is small and background is false', () => { + props = { + background: false, + text: 'mockInfo', + type: 'info', + chipSize: 'small', + } + render(props) + const chip = screen.getByTestId('Chip_info') + expect(chip).toHaveStyle(`padding: ${SPACING.spacing4} 0`) + const icon = screen.getByLabelText('icon_mockInfo') + expect(icon).toHaveStyle(`width: 0.75rem`) + }) + + it('render text with smaller padding and smaller icon when chip size is small and background is true', () => { + props = { + background: true, + text: 'mockInfo', + type: 'info', + chipSize: 'small', + } + render(props) + const chip = screen.getByTestId('Chip_info') + expect(chip).toHaveStyle(`padding: ${SPACING.spacing4} ${SPACING.spacing6}`) + const icon = screen.getByLabelText('icon_mockInfo') + expect(icon).toHaveStyle(`width: 0.75rem`) + }) +}) diff --git a/app/src/atoms/Chip/index.tsx b/components/src/atoms/Chip/index.tsx similarity index 56% rename from app/src/atoms/Chip/index.tsx rename to components/src/atoms/Chip/index.tsx index 06d26cf21c7..36a10bc3a90 100644 --- a/app/src/atoms/Chip/index.tsx +++ b/components/src/atoms/Chip/index.tsx @@ -1,19 +1,16 @@ import * as React from 'react' import { css } from 'styled-components' -import { - ALIGN_CENTER, - BORDERS, - COLORS, - DIRECTION_ROW, - Flex, - Icon, - SPACING, - StyledText, - TYPOGRAPHY, -} from '@opentrons/components' +import { BORDERS, COLORS } from '../../helix-design-system' +import { Flex } from '../../primitives' +import { StyledText } from '../StyledText' +import { ALIGN_CENTER, DIRECTION_ROW } from '../../styles' +import { RESPONSIVENESS, SPACING, TYPOGRAPHY } from '../../ui-style-constants' +import { Icon } from '../../icons' -import type { IconName, StyleProps } from '@opentrons/components' +import type { IconName } from '../../icons' +import type { StyleProps } from '../../primitives' +// ToDo (kk:03/26/2024) basic will be removed when we add Tag component export type ChipType = | 'basic' | 'error' @@ -103,14 +100,42 @@ export function Chip(props: ChipProps): JSX.Element { : CHIP_PROPS_BY_TYPE[type].backgroundColor const icon = iconName ?? CHIP_PROPS_BY_TYPE[type].iconName ?? 'ot-alert' - const TOUCHSCREEN_MEDIUM_CONTAINER_STYLE = css` - padding: ${SPACING.spacing8} ${background === false ? 0 : SPACING.spacing16}; - grid-gap: ${SPACING.spacing8}; + const MEDIUM_CONTAINER_STYLE = css` + padding: ${SPACING.spacing2} ${background === false ? 0 : SPACING.spacing8}; + grid-gap: ${SPACING.spacing4}; + @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { + padding: ${SPACING.spacing8} + ${background === false ? 0 : SPACING.spacing16}; + grid-gap: ${SPACING.spacing8}; + } ` - const TOUCHSCREEN_SMALL_CONTAINER_STYLE = css` - padding: ${SPACING.spacing4} ${background === false ? 0 : SPACING.spacing8}; + const SMALL_CONTAINER_STYLE = css` + padding: ${SPACING.spacing4} ${background === false ? 0 : SPACING.spacing6}; grid-gap: ${SPACING.spacing4}; + @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { + padding: ${SPACING.spacing4} + ${background === false ? 0 : SPACING.spacing8}; + grid-gap: ${SPACING.spacing4}; + } + ` + + const ICON_STYLE = css` + width: ${chipSize === 'medium' ? '1rem' : '0.75rem'}; + height: ${chipSize === 'medium' ? '1rem' : '0.75rem'}; + @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { + width: ${chipSize === 'medium' ? '1.5rem' : '1.25rem'}; + height: ${chipSize === 'medium' ? '1.5rem' : '1.25rem'}; + } + ` + + const TEXT_STYLE = css` + ${chipSize === 'medium' ? WEB_MEDIUM_TEXT_STYLE : WEB_SMALL_TEXT_STYLE} + @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { + ${chipSize === 'medium' + ? TYPOGRAPHY.bodyTextSemiBold + : TYPOGRAPHY.smallBodyTextSemiBold} + } ` return ( @@ -120,9 +145,7 @@ export function Chip(props: ChipProps): JSX.Element { borderRadius={CHIP_PROPS_BY_TYPE[type].borderRadius} flexDirection={DIRECTION_ROW} css={ - chipSize === 'medium' - ? TOUCHSCREEN_MEDIUM_CONTAINER_STYLE - : TOUCHSCREEN_SMALL_CONTAINER_STYLE + chipSize === 'medium' ? MEDIUM_CONTAINER_STYLE : SMALL_CONTAINER_STYLE } data-testid={`Chip_${type}`} {...styleProps} @@ -132,19 +155,23 @@ export function Chip(props: ChipProps): JSX.Element { name={icon} color={CHIP_PROPS_BY_TYPE[type].iconColor} aria-label={`icon_${text}`} - size={chipSize === 'medium' ? '1.5rem' : '1.25rem'} + css={ICON_STYLE} /> ) : null} - + {text} ) } + +const WEB_MEDIUM_TEXT_STYLE = css` + font-size: ${TYPOGRAPHY.fontSizeH4}; + line-height: ${TYPOGRAPHY.lineHeight20}; + font-weight: ${TYPOGRAPHY.fontWeightSemiBold}; +` +const WEB_SMALL_TEXT_STYLE = css` + font-size: ${TYPOGRAPHY.fontSizeLabel}; + line-height: ${TYPOGRAPHY.lineHeight12}; + font-weight: ${TYPOGRAPHY.fontWeightSemiBold}; +` diff --git a/components/src/atoms/index.ts b/components/src/atoms/index.ts index 345d50ac38c..93a5eb64f26 100644 --- a/components/src/atoms/index.ts +++ b/components/src/atoms/index.ts @@ -1,4 +1,6 @@ export * from './buttons' export * from './CheckboxField' +export * from './Chip' +export * from './StepMeter' export * from './StepMeter' export * from './StyledText' diff --git a/components/src/ui-style-constants/index.ts b/components/src/ui-style-constants/index.ts index 21a599f031c..e61234d0e96 100644 --- a/components/src/ui-style-constants/index.ts +++ b/components/src/ui-style-constants/index.ts @@ -1,3 +1,4 @@ export * as RESPONSIVENESS from './responsiveness' -export * as TYPOGRAPHY from './typography' export * as SPACING from './spacing' +export * as TYPOGRAPHY from './typography' +export * as VIEWPORT from './viewport' diff --git a/app/src/DesignTokens/constants.ts b/components/src/ui-style-constants/viewport.ts similarity index 100% rename from app/src/DesignTokens/constants.ts rename to components/src/ui-style-constants/viewport.ts From 8f8872814a96ca9026c2074ddd95068694d3161e Mon Sep 17 00:00:00 2001 From: koji Date: Fri, 29 Mar 2024 18:41:25 -0400 Subject: [PATCH 005/194] fix(app): Align software keyboard with latest design (#14700) * fix(app): Align software keyboard with latest design --- .../AlphanumericKeyboard.stories.tsx} | 18 +- .../__tests__/CustomKeyboard.test.tsx | 80 ++++++-- .../AlphanumericKeyboard/index.css | 71 +++++++ .../index.tsx | 41 ++-- .../SoftwareKeyboard/CustomKeyboard/index.css | 33 ---- .../FullKeyboard.stories.tsx} | 14 +- .../__tests__/FullKeyboard.test.tsx} | 36 ++-- .../SoftwareKeyboard/FullKeyboard/index.css | 110 +++++++++++ .../index.tsx | 40 +--- .../IndividualKey.stories.tsx} | 23 ++- .../__tests__/IndividualKey.test.tsx | 36 ++++ .../SoftwareKeyboard/IndividualKey/index.css | 12 ++ .../{Numpad => IndividualKey}/index.tsx | 18 +- .../SoftwareKeyboard/NormalKeyboard/index.css | 26 --- .../NumericalKeyboard.stories.tsx | 80 ++++++++ .../__tests__/NumericalKeyboard.test.tsx | 178 ++++++++++++++++++ .../NumericalKeyboard/index.css | 52 +++++ .../NumericalKeyboard/index.tsx | 39 ++++ .../Numpad/__tests__/Numpad.test.tsx | 53 ------ .../atoms/SoftwareKeyboard/Numpad/index.css | 7 - app/src/atoms/SoftwareKeyboard/constants.ts | 65 ++++++- app/src/atoms/SoftwareKeyboard/index.ts | 7 +- app/src/index.tsx | 8 +- .../organisms/NetworkSettings/SetWifiCred.tsx | 4 +- .../organisms/NetworkSettings/SetWifiSsid.tsx | 4 +- .../__tests__/SetWifiCred.test.tsx | 2 +- app/src/pages/NameRobot/index.tsx | 4 +- app/src/styles.global.module.css | 7 +- 28 files changed, 811 insertions(+), 257 deletions(-) rename app/src/atoms/SoftwareKeyboard/{CustomKeyboard/CustomKeyboard.stories.tsx => AlphanumericKeyboard/AlphanumericKeyboard.stories.tsx} (72%) rename app/src/atoms/SoftwareKeyboard/{CustomKeyboard => AlphanumericKeyboard}/__tests__/CustomKeyboard.test.tsx (59%) create mode 100644 app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/index.css rename app/src/atoms/SoftwareKeyboard/{CustomKeyboard => AlphanumericKeyboard}/index.tsx (52%) delete mode 100644 app/src/atoms/SoftwareKeyboard/CustomKeyboard/index.css rename app/src/atoms/SoftwareKeyboard/{NormalKeyboard/NormalKeyboard.stories.tsx => FullKeyboard/FullKeyboard.stories.tsx} (74%) rename app/src/atoms/SoftwareKeyboard/{NormalKeyboard/__tests__/NormalKeyboard.test.tsx => FullKeyboard/__tests__/FullKeyboard.test.tsx} (87%) create mode 100644 app/src/atoms/SoftwareKeyboard/FullKeyboard/index.css rename app/src/atoms/SoftwareKeyboard/{NormalKeyboard => FullKeyboard}/index.tsx (59%) rename app/src/atoms/SoftwareKeyboard/{Numpad/Numpad.stories.tsx => IndividualKey/IndividualKey.stories.tsx} (67%) create mode 100644 app/src/atoms/SoftwareKeyboard/IndividualKey/__tests__/IndividualKey.test.tsx create mode 100644 app/src/atoms/SoftwareKeyboard/IndividualKey/index.css rename app/src/atoms/SoftwareKeyboard/{Numpad => IndividualKey}/index.tsx (65%) delete mode 100644 app/src/atoms/SoftwareKeyboard/NormalKeyboard/index.css create mode 100644 app/src/atoms/SoftwareKeyboard/NumericalKeyboard/NumericalKeyboard.stories.tsx create mode 100644 app/src/atoms/SoftwareKeyboard/NumericalKeyboard/__tests__/NumericalKeyboard.test.tsx create mode 100644 app/src/atoms/SoftwareKeyboard/NumericalKeyboard/index.css create mode 100644 app/src/atoms/SoftwareKeyboard/NumericalKeyboard/index.tsx delete mode 100644 app/src/atoms/SoftwareKeyboard/Numpad/__tests__/Numpad.test.tsx delete mode 100644 app/src/atoms/SoftwareKeyboard/Numpad/index.css diff --git a/app/src/atoms/SoftwareKeyboard/CustomKeyboard/CustomKeyboard.stories.tsx b/app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/AlphanumericKeyboard.stories.tsx similarity index 72% rename from app/src/atoms/SoftwareKeyboard/CustomKeyboard/CustomKeyboard.stories.tsx rename to app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/AlphanumericKeyboard.stories.tsx index f6e72c00bf9..6d30005ad9e 100644 --- a/app/src/atoms/SoftwareKeyboard/CustomKeyboard/CustomKeyboard.stories.tsx +++ b/app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/AlphanumericKeyboard.stories.tsx @@ -1,25 +1,27 @@ import * as React from 'react' import { - Flex, DIRECTION_COLUMN, + Flex, POSITION_ABSOLUTE, SPACING, VIEWPORT, } from '@opentrons/components' import { InputField } from '../../InputField' -import { CustomKeyboard } from './' +import { AlphanumericKeyboard } from '.' import '../index.css' import './index.css' import type { Story, Meta } from '@storybook/react' export default { - title: 'ODD/Atoms/SoftwareKeyboard/CustomKeyboard', - component: CustomKeyboard, + title: 'ODD/Atoms/SoftwareKeyboard/AlphanumericKeyboard', + component: AlphanumericKeyboard, parameters: VIEWPORT.touchScreenViewport, } as Meta -const Template: Story> = args => { +const Template: Story< + React.ComponentProps +> = args => { const [showKeyboard, setShowKeyboard] = React.useState(false) const [value, setValue] = React.useState('') const keyboardRef = React.useRef(null) @@ -33,9 +35,9 @@ const Template: Story> = args => { onFocus={() => setShowKeyboard(true)} /> - + {showKeyboard && ( - e != null && setValue(String(e))} keyboardRef={keyboardRef} /> @@ -45,4 +47,4 @@ const Template: Story> = args => { ) } -export const CustomSoftwareKeyboard = Template.bind({}) +export const AlphanumericSoftwareKeyboard = Template.bind({}) diff --git a/app/src/atoms/SoftwareKeyboard/CustomKeyboard/__tests__/CustomKeyboard.test.tsx b/app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/__tests__/CustomKeyboard.test.tsx similarity index 59% rename from app/src/atoms/SoftwareKeyboard/CustomKeyboard/__tests__/CustomKeyboard.test.tsx rename to app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/__tests__/CustomKeyboard.test.tsx index c4c38fad53b..336e0c86026 100644 --- a/app/src/atoms/SoftwareKeyboard/CustomKeyboard/__tests__/CustomKeyboard.test.tsx +++ b/app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/__tests__/CustomKeyboard.test.tsx @@ -3,14 +3,14 @@ import { describe, it, expect, vi } from 'vitest' import '@testing-library/jest-dom/vitest' import { fireEvent, renderHook, screen } from '@testing-library/react' import { renderWithProviders } from '../../../../__testing-utils__' -import { CustomKeyboard } from '..' +import { AlphanumericKeyboard } from '..' -const render = (props: React.ComponentProps) => { - return renderWithProviders()[0] +const render = (props: React.ComponentProps) => { + return renderWithProviders()[0] } -describe('CustomKeyboard', () => { - it('should render the custom keyboards lower case', () => { +describe('AlphanumericKeyboard', () => { + it('should render alphanumeric keyboard - lower case', () => { const { result } = renderHook(() => React.useRef(null)) const props = { onChange: vi.fn(), @@ -29,6 +29,7 @@ describe('CustomKeyboard', () => { 'i', 'o', 'p', + '123', 'a', 's', 'd', @@ -38,7 +39,7 @@ describe('CustomKeyboard', () => { 'j', 'k', 'l', - 'SHIFT', + 'ABC', 'z', 'x', 'c', @@ -47,21 +48,20 @@ describe('CustomKeyboard', () => { 'n', 'm', 'del', - '123', ] buttons.forEach((button, index) => { const expectedName = expectedButtonNames[index] expect(button).toHaveTextContent(expectedName) }) }) - it('should render the custom keyboards upper case, when clicking shift key', () => { + it('should render alphanumeric keyboard - upper case, when clicking ABC key', () => { const { result } = renderHook(() => React.useRef(null)) const props = { onChange: vi.fn(), keyboardRef: result.current, } render(props) - const shiftKey = screen.getByRole('button', { name: 'SHIFT' }) + const shiftKey = screen.getByRole('button', { name: 'ABC' }) fireEvent.click(shiftKey) const buttons = screen.getAllByRole('button') @@ -76,6 +76,7 @@ describe('CustomKeyboard', () => { 'I', 'O', 'P', + '123', 'A', 'S', 'D', @@ -94,7 +95,6 @@ describe('CustomKeyboard', () => { 'N', 'M', 'del', - '123', ] buttons.forEach((button, index) => { const expectedName = expectedButtonNames[index] @@ -102,7 +102,7 @@ describe('CustomKeyboard', () => { }) }) - it('should render the custom keyboards numbers, when clicking number key', () => { + it('should render alphanumeric keyboard - numbers, when clicking number key', () => { const { result } = renderHook(() => React.useRef(null)) const props = { onChange: vi.fn(), @@ -132,7 +132,7 @@ describe('CustomKeyboard', () => { }) }) - it('should render the custom keyboards lower case, when clicking number key then abc key', () => { + it('should render alphanumeric keyboard - lower case when layout is numbers and clicking abc ', () => { const { result } = renderHook(() => React.useRef(null)) const props = { onChange: vi.fn(), @@ -140,9 +140,63 @@ describe('CustomKeyboard', () => { } render(props) const numberKey = screen.getByRole('button', { name: '123' }) - screen.getByRole('button', { name: 'a' }) + fireEvent.click(numberKey) + const abcKey = screen.getByRole('button', { name: 'abc' }) + fireEvent.click(abcKey) + const buttons = screen.getAllByRole('button') + const expectedButtonNames = [ + 'q', + 'w', + 'e', + 'r', + 't', + 'y', + 'u', + 'i', + 'o', + 'p', + '123', + 'a', + 's', + 'd', + 'f', + 'g', + 'h', + 'j', + 'k', + 'l', + 'ABC', + 'z', + 'x', + 'c', + 'v', + 'b', + 'n', + 'm', + 'del', + ] + buttons.forEach((button, index) => { + const expectedName = expectedButtonNames[index] + expect(button).toHaveTextContent(expectedName) + }) + }) + + it('should switch each alphanumeric keyboard properly', () => { + const { result } = renderHook(() => React.useRef(null)) + const props = { + onChange: vi.fn(), + keyboardRef: result.current, + } + render(props) + // lower case keyboard -> upper case keyboard + const ABCKey = screen.getByRole('button', { name: 'ABC' }) + fireEvent.click(ABCKey) + screen.getByRole('button', { name: 'A' }) + // upper case keyboard -> number keyboard + const numberKey = screen.getByRole('button', { name: '123' }) fireEvent.click(numberKey) screen.getByRole('button', { name: '1' }) + // number keyboard -> lower case keyboard const abcKey = screen.getByRole('button', { name: 'abc' }) fireEvent.click(abcKey) screen.getByRole('button', { name: 'a' }) diff --git a/app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/index.css b/app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/index.css new file mode 100644 index 00000000000..8816853e595 --- /dev/null +++ b/app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/index.css @@ -0,0 +1,71 @@ +/* stylelint-disable */ + +/* Alphanumeric Keyboard has 3 layouts + 1. lower letter keys: hg-layout-default + 2. upper letter keys: hg-layout-shift + 3. number keys: hg-layout-numbers + 1, 2 are using the same style but 3 has own style. + */ + +.simple-keyboard.oddTheme1.hg-theme-default .hg-layout-default { + width: 100%; + height: 100%; + background-color: #cbcccc; /* grey35 */ + font-family: 'Public Sans', sans-serif; + padding: 8px; +} + +.hg-layout-default .hg-row .hg-button, +.hg-layout-shift .hg-row .hg-button, +.hg-layout-numbers .hg-row .hg-button { + color: #16212d; + font-size: 20px; + font-style: normal; + font-weight: 600; + line-height: 24px; + background-color: #ffffff; + padding: 10px 22px; +} + +.simple-keyboard .hg-button:active { + color: #16212d; + background-color: #dedede; /* grey30 */ +} + +.hg-layout-default .hg-row .hg-button, +.hg-layout-shift .hg-row .hg-button { + height: 62.3px; +} + +/* first row and second row */ +.hg-layout-default .hg-row:not(:last-child), +.hg-layout-shift .hg-row:not(:last-child) { + grid-column: 8px; +} +.hg-row:not(:last-child) .hg-button { + width: 94px; +} + +/* third row first button and last button are the same size +the rest is the same */ +.hg-layout-default .hg-row:last-child, +.hg-layout-shift .hg-row:last-child, +.hg-layout-numbers .hg-row:last-child { + /* adding 3px because package's css add margin-right:5px */ + grid-gap: 3px; +} +.hg-layout-default .hg-row:last-child .hg-button, +.hg-layout-shift .hg-row:last-child .hg-button { + width: 97px; +} +.hg-layout-default .hg-row:last-child .hg-button:first-child, +.hg-layout-default .hg-row:last-child .hg-button:last-child, +.hg-layout-shift .hg-row:last-child .hg-button:first-child, +.hg-layout-shift .hg-row:last-child .hg-button:last-child { + width: 132px; +} + +.hg-layout-numbers .hg-row .hg-button { + height: 44.75px; + width: 330px !important; +} diff --git a/app/src/atoms/SoftwareKeyboard/CustomKeyboard/index.tsx b/app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/index.tsx similarity index 52% rename from app/src/atoms/SoftwareKeyboard/CustomKeyboard/index.tsx rename to app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/index.tsx index ddf9215a874..af02f09b31f 100644 --- a/app/src/atoms/SoftwareKeyboard/CustomKeyboard/index.tsx +++ b/app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/index.tsx @@ -1,36 +1,22 @@ import * as React from 'react' import Keyboard from 'react-simple-keyboard' -import { customDisplay } from '../constants' +import { alphanumericKeyboardLayout, customDisplay } from '../constants' -interface CustomKeyboardProps { +interface AlphanumericKeyboardProps { onChange: (input: string) => void keyboardRef: React.MutableRefObject } -const customLayout = { - default: [ - 'q w e r t y u i o p', - 'a s d f g h j k l', - '{shift} z x c v b n m {backspace}', - '{numbers}', - ], - shift: [ - 'Q W E R T Y U I O P', - 'A S D F G H J K L', - '{abc} Z X C V B N M {backspace}', - '{numbers}', - ], - numbers: ['1 2 3', '4 5 6', '7 8 9', '{abc} 0 {backspace}'], -} - -export function CustomKeyboard({ +export function AlphanumericKeyboard({ onChange, keyboardRef, -}: CustomKeyboardProps): JSX.Element { +}: AlphanumericKeyboardProps): JSX.Element { const [layoutName, setLayoutName] = React.useState('default') const onKeyPress = (button: string): void => { - if (button === '{shift}' || button === '{lock}') handleShift() - if (button === '{numbers}' || button === '{abc}') handleNumber() + console.log(button) + if (button === '{ABC}') handleShift() + if (button === '{numbers}') handleNumber() + if (button === '{abc}') handleUnShift() } const handleShift = (): void => { @@ -38,7 +24,13 @@ export function CustomKeyboard({ } const handleNumber = (): void => { - setLayoutName(layoutName === 'default' ? 'numbers' : 'default') + setLayoutName( + layoutName === 'default' || layoutName === 'shift' ? 'numbers' : 'default' + ) + } + + const handleUnShift = (): void => { + setLayoutName('default') } return ( @@ -48,11 +40,12 @@ export function CustomKeyboard({ onChange={onChange} onKeyPress={onKeyPress} layoutName={layoutName} - layout={customLayout} + layout={alphanumericKeyboardLayout} display={customDisplay} mergeDisplay={true} autoUseTouchEvents={true} useButtonTag={true} + width="100%" /> ) } diff --git a/app/src/atoms/SoftwareKeyboard/CustomKeyboard/index.css b/app/src/atoms/SoftwareKeyboard/CustomKeyboard/index.css deleted file mode 100644 index f3e0b6cdd54..00000000000 --- a/app/src/atoms/SoftwareKeyboard/CustomKeyboard/index.css +++ /dev/null @@ -1,33 +0,0 @@ -/* stylelint-disable */ - -.simple-keyboard.oddTheme1.hg-theme-default { - width: 100%; - height: 100%; - background-color: #cbcccc; /* grey35 */ - font-family: 'Public Sans', sans-serif; - padding: 8px; - font-size: 28px; -} - -.simple-keyboard.oddTheme1 - .hg-row:not(:last-child) - .hg-button:not(:last-child) { - margin-right: 8px; - margin-bottom: 3px; -} - -.simple-keyboard.simple-keyboard.oddTheme1 .hg-button { - height: 48px; -} - -.simple-keyboard .hg-button:active { - color: #16212d; - background-color: #e3e3e3; -} - -/* Numeric keyboard in custom keyboard */ -.hg-layout-numbers button.hg-button.hg-button-backspace, -.hg-layout-numbers button.hg-button.hg-button-abc, -.hg-layout-numbers button.hg-button.hg-standardBtn { - flex: 1; -} diff --git a/app/src/atoms/SoftwareKeyboard/NormalKeyboard/NormalKeyboard.stories.tsx b/app/src/atoms/SoftwareKeyboard/FullKeyboard/FullKeyboard.stories.tsx similarity index 74% rename from app/src/atoms/SoftwareKeyboard/NormalKeyboard/NormalKeyboard.stories.tsx rename to app/src/atoms/SoftwareKeyboard/FullKeyboard/FullKeyboard.stories.tsx index 7883d6fbdd0..3aaea8cb33d 100644 --- a/app/src/atoms/SoftwareKeyboard/NormalKeyboard/NormalKeyboard.stories.tsx +++ b/app/src/atoms/SoftwareKeyboard/FullKeyboard/FullKeyboard.stories.tsx @@ -7,7 +7,7 @@ import { VIEWPORT, } from '@opentrons/components' import { InputField } from '../../InputField' -import { NormalKeyboard } from '.' +import { FullKeyboard } from '.' import '../index.css' import './index.css' @@ -15,12 +15,12 @@ import './index.css' import type { Story, Meta } from '@storybook/react' export default { - title: 'ODD/Atoms/SoftwareKeyboard/NormalKeyboard', - component: NormalKeyboard, + title: 'ODD/Atoms/SoftwareKeyboard/FullKeyboard', + component: FullKeyboard, parameters: VIEWPORT.touchScreenViewport, } as Meta -const Template: Story> = args => { +const Template: Story> = args => { const [showKeyboard, setShowKeyboard] = React.useState(false) const [value, setValue] = React.useState('') const keyboardRef = React.useRef(null) @@ -34,9 +34,9 @@ const Template: Story> = args => { onFocus={() => setShowKeyboard(true)} /> - + {showKeyboard && ( - e != null && setValue(String(e))} keyboardRef={keyboardRef} /> @@ -46,4 +46,4 @@ const Template: Story> = args => { ) } -export const NormalSoftwareKeyboard = Template.bind({}) +export const FullSoftwareKeyboard = Template.bind({}) diff --git a/app/src/atoms/SoftwareKeyboard/NormalKeyboard/__tests__/NormalKeyboard.test.tsx b/app/src/atoms/SoftwareKeyboard/FullKeyboard/__tests__/FullKeyboard.test.tsx similarity index 87% rename from app/src/atoms/SoftwareKeyboard/NormalKeyboard/__tests__/NormalKeyboard.test.tsx rename to app/src/atoms/SoftwareKeyboard/FullKeyboard/__tests__/FullKeyboard.test.tsx index cc53e3ff827..c84a33a2796 100644 --- a/app/src/atoms/SoftwareKeyboard/NormalKeyboard/__tests__/NormalKeyboard.test.tsx +++ b/app/src/atoms/SoftwareKeyboard/FullKeyboard/__tests__/FullKeyboard.test.tsx @@ -3,14 +3,14 @@ import { describe, it, expect, vi } from 'vitest' import '@testing-library/jest-dom/vitest' import { fireEvent, renderHook, screen } from '@testing-library/react' import { renderWithProviders } from '../../../../__testing-utils__' -import { NormalKeyboard } from '..' +import { FullKeyboard } from '..' -const render = (props: React.ComponentProps) => { - return renderWithProviders()[0] +const render = (props: React.ComponentProps) => { + return renderWithProviders()[0] } -describe('SoftwareKeyboard', () => { - it('should render the software keyboards', () => { +describe('FullKeyboard', () => { + it('should render FullKeyboard keyboard', () => { const { result } = renderHook(() => React.useRef(null)) const props = { onChange: vi.fn(), @@ -40,7 +40,7 @@ describe('SoftwareKeyboard', () => { 'j', 'k', 'l', - 'SHIFT', + 'ABC', 'z', 'x', 'c', @@ -58,14 +58,14 @@ describe('SoftwareKeyboard', () => { }) }) - it('should render the software keyboards when hitting shift key', () => { + it('should render full keyboard when hitting ABC key', () => { const { result } = renderHook(() => React.useRef(null)) const props = { onChange: vi.fn(), keyboardRef: result.current, } render(props) - const shiftKey = screen.getByRole('button', { name: 'SHIFT' }) + const shiftKey = screen.getByRole('button', { name: 'ABC' }) fireEvent.click(shiftKey) const buttons = screen.getAllByRole('button') const expectedButtonNames = [ @@ -107,7 +107,7 @@ describe('SoftwareKeyboard', () => { }) }) - it('should render the software keyboards when hitting 123 key', () => { + it('should render full keyboard when hitting 123 key', () => { const { result } = renderHook(() => React.useRef(null)) const props = { onChange: vi.fn(), @@ -128,6 +128,7 @@ describe('SoftwareKeyboard', () => { '8', '9', '0', + 'abc', '-', '/', ':', @@ -138,13 +139,14 @@ describe('SoftwareKeyboard', () => { '&', '@', '"', - 'abc', '#+=', '.', ',', '?', '!', "'", + '*', + '~', 'del', 'space', ] @@ -172,29 +174,25 @@ describe('SoftwareKeyboard', () => { ']', '{', '}', - '#', '%', '^', - '*', '+', - '=', + 'abc', '_', '\\', '|', - '~', '<', '>', - '€', - '£', - '¥', - '·', - 'abc', + '#', + '=', '123', '.', ',', '?', '!', "'", + '*', + '~', 'del', 'space', ] diff --git a/app/src/atoms/SoftwareKeyboard/FullKeyboard/index.css b/app/src/atoms/SoftwareKeyboard/FullKeyboard/index.css new file mode 100644 index 00000000000..b54cde35e04 --- /dev/null +++ b/app/src/atoms/SoftwareKeyboard/FullKeyboard/index.css @@ -0,0 +1,110 @@ +/* stylelint-disable */ + +/* Full Keyboard has 4 layouts + 1. lower letter keys: hg-layout-default + 2. upper letter keys: hg-layout-shift + 3. number keys: hg-layout-numbers + 4. symbol keys: hg-layout-symbols + 1, 2 are using the same style but 3 & 4 have their own styles. + */ + +.simple-keyboard.oddTheme1.hg-theme-default { + width: 100%; + height: 100%; + background-color: #cbcccc; /* grey35 */ + font-family: 'Public Sans', sans-serif; + padding: 8px; +} + +.hg-layout-default .hg-row, +.hg-layout-shift .hg-row, +.hg-layout-symbols .hg-row, +.hg-layout-numbers .hg-row { + /* adding 3px because package's css add margin-right:5px */ + grid-gap: 3px; +} + +.simple-keyboard.simple-keyboard.oddTheme1 .hg-button { + height: 44.75px; +} + +.simple-keyboard.simple-keyboard.oddTheme1 .hg-button:not(:last-child) { + margin-bottom: 3px; +} + +.simple-keyboard .hg-button:active { + color: #16212d; + background-color: #dedede; +} + +.hg-layout-default .hg-row .hg-button, +.hg-layout-shift .hg-row .hg-button, +.hg-layout-symbols .hg-row .hg-button, +.hg-layout-numbers .hg-row .hg-button { + color: #16212d; + height: 44.75px; + font-size: 20px; + font-style: normal; + font-weight: 600; + line-height: 24px; + background-color: #ffffff; + padding: 10px 22px; +} + +.hg-layout-default .hg-row:nth-child(1) .hg-button, +.hg-layout-default .hg-row:nth-child(2) .hg-button, +.hg-layout-shift .hg-row:nth-child(1) .hg-button, +.hg-layout-shift .hg-row:nth-child(2) .hg-button, +.hg-layout-numbers .hg-row:nth-child(1) .hg-button { + width: 93.6px; +} + +.hg-layout-numbers .hg-row:nth-child(2) .hg-button { + width: 83.4px; +} + +.hg-layout-symbols .hg-row:nth-child(2) .hg-button { + width: 122.5px; +} + +.hg-layout-numbers .hg-row:nth-child(2) .hg-button:nth-child(10) { + /* This is needed to override the package style */ + max-width: 83.4px !important; +} + +.hg-layout-numbers .hg-row:nth-child(2) .hg-button:first-child, +.hg-layout-symbols .hg-row:nth-child(2) .hg-button:first-child { + width: 94px; +} + +.hg-layout-default .hg-row:nth-child(3) .hg-button, +.hg-layout-shift .hg-row:nth-child(3) .hg-button, +.hg-layout-numbers .hg-row:nth-child(3) .hg-button, +.hg-layout-symbols .hg-row:nth-child(3) .hg-button { + width: 97px; +} + +/* .hg-layout-default .hg-row:nth-child(3) .hg-button, +.hg-layout-shift .hg-row:nth-child(3) .hg-button { + width: 97px; +} */ + +.hg-layout-default .hg-row:nth-child(3) .hg-button:first-child, +.hg-layout-default .hg-row:nth-child(3) .hg-button:last-child, +.hg-layout-shift .hg-row:nth-child(3) .hg-button:first-child, +.hg-layout-shift .hg-row:nth-child(3) .hg-button:last-child, +.hg-layout-numbers .hg-row:nth-child(3) .hg-button:first-child, +.hg-layout-numbers .hg-row:nth-child(3) .hg-button:last-child, +.hg-layout-symbols .hg-row:nth-child(3) .hg-button:first-child, +.hg-layout-symbols .hg-row:nth-child(3) .hg-button:last-child { + width: 132px; +} + +.hg-layout-symbols .hg-row:nth-child(1) .hg-button { + width: 137.1px; +} + +.simple-keyboard .hg-button:active { + color: #16212d; + background-color: #e3e3e3; /* grey30 */ +} diff --git a/app/src/atoms/SoftwareKeyboard/NormalKeyboard/index.tsx b/app/src/atoms/SoftwareKeyboard/FullKeyboard/index.tsx similarity index 59% rename from app/src/atoms/SoftwareKeyboard/NormalKeyboard/index.tsx rename to app/src/atoms/SoftwareKeyboard/FullKeyboard/index.tsx index dcb02503f00..850ad689758 100644 --- a/app/src/atoms/SoftwareKeyboard/NormalKeyboard/index.tsx +++ b/app/src/atoms/SoftwareKeyboard/FullKeyboard/index.tsx @@ -1,46 +1,16 @@ import * as React from 'react' import Keyboard from 'react-simple-keyboard' -import { customDisplay } from '../constants' +import { customDisplay, fullKeyboardLayout } from '../constants' -interface NormalKeyboardProps { +interface FullKeyboardProps { onChange: (input: string) => void keyboardRef: React.MutableRefObject } -// Note the design team request is the following -// Input type: characters, numbers and special characters - -const customLayout = { - default: [ - 'q w e r t y u i o p', - '{numbers} a s d f g h j k l', - '{shift} z x c v b n m {backspace}', - '{space}', - ], - shift: [ - 'Q W E R T Y U I O P', - '{numbers} A S D F G H J K L', - '{abc} Z X C V B N M {backspace}', - '{space}', - ], - symbols: [ - '[ ] { } # % ^ * + =', - '_ \\ | ~ < > € £ ¥ ·', - "{abc} {numbers} . , ? ! ' {backspace}", - '{space}', - ], - numbers: [ - '1 2 3 4 5 6 7 8 9 0', - '- / : ; ( ) $ & @ "', - "{abc} {symbols} . , ? ! ' {backspace}", - '{space}', - ], -} - -export function NormalKeyboard({ +export function FullKeyboard({ onChange, keyboardRef, -}: NormalKeyboardProps): JSX.Element { +}: FullKeyboardProps): JSX.Element { const [layoutName, setLayoutName] = React.useState('default') const handleShift = (button: string): void => { switch (button) { @@ -78,7 +48,7 @@ export function NormalKeyboard({ onChange={onChange} onKeyPress={onKeyPress} layoutName={layoutName} - layout={customLayout} + layout={fullKeyboardLayout} display={customDisplay} mergeDisplay={true} autoUseTouchEvents={true} diff --git a/app/src/atoms/SoftwareKeyboard/Numpad/Numpad.stories.tsx b/app/src/atoms/SoftwareKeyboard/IndividualKey/IndividualKey.stories.tsx similarity index 67% rename from app/src/atoms/SoftwareKeyboard/Numpad/Numpad.stories.tsx rename to app/src/atoms/SoftwareKeyboard/IndividualKey/IndividualKey.stories.tsx index d5a569cd284..3600dafc89a 100644 --- a/app/src/atoms/SoftwareKeyboard/Numpad/Numpad.stories.tsx +++ b/app/src/atoms/SoftwareKeyboard/IndividualKey/IndividualKey.stories.tsx @@ -1,25 +1,25 @@ import * as React from 'react' import { - Flex, DIRECTION_COLUMN, + Flex, POSITION_ABSOLUTE, SPACING, VIEWPORT, } from '@opentrons/components' import { InputField } from '../../InputField' -import { Numpad } from './' +import { IndividualKey } from '.' import '../index.css' import './index.css' import type { Story, Meta } from '@storybook/react' export default { - title: 'ODD/Atoms/SoftwareKeyboard/Numpad', - component: Numpad, + title: 'ODD/Atoms/SoftwareKeyboard/IndividualKey', + component: IndividualKey, parameters: VIEWPORT.touchScreenViewport, } as Meta -const Template: Story> = args => { +const Template: Story> = args => { const [showKeyboard, setShowKeyboard] = React.useState(false) const [value, setValue] = React.useState('') const keyboardRef = React.useRef(null) @@ -30,14 +30,18 @@ const Template: Story> = args => { value={value} type="text" placeholder="When focusing, the numpad shows up" - onFocus={() => setShowKeyboard(true)} + onFocus={() => { + setShowKeyboard(true) + }} /> {showKeyboard && ( - e != null && setValue(String(e))} keyboardRef={keyboardRef} + keyText={args.keyText} /> )} @@ -45,4 +49,7 @@ const Template: Story> = args => { ) } -export const NormalSoftwareKeyboard = Template.bind({}) +export const Keyboard = Template.bind({}) +Keyboard.args = { + keyText: 'hello!', +} diff --git a/app/src/atoms/SoftwareKeyboard/IndividualKey/__tests__/IndividualKey.test.tsx b/app/src/atoms/SoftwareKeyboard/IndividualKey/__tests__/IndividualKey.test.tsx new file mode 100644 index 00000000000..f08c7e4566f --- /dev/null +++ b/app/src/atoms/SoftwareKeyboard/IndividualKey/__tests__/IndividualKey.test.tsx @@ -0,0 +1,36 @@ +import * as React from 'react' +import { describe, it, vi, expect } from 'vitest' +import { fireEvent, renderHook, screen } from '@testing-library/react' + +import { renderWithProviders } from '../../../../__testing-utils__' +import { IndividualKey } from '..' + +const render = (props: React.ComponentProps) => { + return renderWithProviders()[0] +} + +describe('IndividualKey', () => { + it('should render the text key', () => { + const { result } = renderHook(() => React.useRef(null)) + const props = { + onChange: vi.fn(), + keyboardRef: result.current, + keyText: 'mockKey', + } + render(props) + screen.getByRole('button', { name: 'mockKey' }) + }) + + it('should call mock function when clicking text key', () => { + const { result } = renderHook(() => React.useRef(null)) + const props = { + onChange: vi.fn(), + keyboardRef: result.current, + keyText: 'mockKey', + } + render(props) + const textKey = screen.getByRole('button', { name: 'mockKey' }) + fireEvent.click(textKey) + expect(props.onChange).toHaveBeenCalled() + }) +}) diff --git a/app/src/atoms/SoftwareKeyboard/IndividualKey/index.css b/app/src/atoms/SoftwareKeyboard/IndividualKey/index.css new file mode 100644 index 00000000000..cfd00f3a2af --- /dev/null +++ b/app/src/atoms/SoftwareKeyboard/IndividualKey/index.css @@ -0,0 +1,12 @@ +/* stylelint-disable */ + +.simple-keyboard .hg-button { + text-align: center; + font-size: 20px; + font-weight: 600; + line-height: 24px; +} +.simple-keyboard .hg-button:active { + color: #16212d; + background-color: #dedede; /* grey30 */ +} diff --git a/app/src/atoms/SoftwareKeyboard/Numpad/index.tsx b/app/src/atoms/SoftwareKeyboard/IndividualKey/index.tsx similarity index 65% rename from app/src/atoms/SoftwareKeyboard/Numpad/index.tsx rename to app/src/atoms/SoftwareKeyboard/IndividualKey/index.tsx index b16b950fada..c501b0eccc6 100644 --- a/app/src/atoms/SoftwareKeyboard/Numpad/index.tsx +++ b/app/src/atoms/SoftwareKeyboard/IndividualKey/index.tsx @@ -4,15 +4,20 @@ import Keyboard from 'react-simple-keyboard' const customDisplay = { '{backspace}': 'del', } -interface NumpadProps { +interface IndividualKeyProps { onChange: (input: string) => void keyboardRef: React.MutableRefObject + keyText: string } -export function Numpad({ onChange, keyboardRef }: NumpadProps): JSX.Element { - const keyboardNumpad = { +export function IndividualKey({ + onChange, + keyboardRef, + keyText, +}: IndividualKeyProps): JSX.Element { + const numericalKeyboard = { layout: { - default: ['7 8 9', '4 5 6', '1 2 3', '0 . {backspace}'], + default: [`${keyText}`], }, } return ( @@ -22,13 +27,14 @@ export function Numpad({ onChange, keyboardRef }: NumpadProps): JSX.Element { */ (keyboardRef.current = r)} - theme={'hg-theme-default oddTheme1 numpad'} + theme={'hg-theme-default oddTheme1 individual-key'} onChange={onChange} layoutName="default" display={customDisplay} autoUseTouchEvents={true} useButtonTag={true} - {...keyboardNumpad} + {...numericalKeyboard} + width="100%" /> ) } diff --git a/app/src/atoms/SoftwareKeyboard/NormalKeyboard/index.css b/app/src/atoms/SoftwareKeyboard/NormalKeyboard/index.css deleted file mode 100644 index 5e1b269ca82..00000000000 --- a/app/src/atoms/SoftwareKeyboard/NormalKeyboard/index.css +++ /dev/null @@ -1,26 +0,0 @@ -/* stylelint-disable */ - -.simple-keyboard.oddTheme1.hg-theme-default { - width: 100%; - height: 100%; - background-color: #cbcccc; /* grey35 */ - font-family: 'Public Sans', sans-serif; - padding: 8px; - font-size: 28px; -} - -.simple-keyboard.oddTheme1 - .hg-row:not(:last-child) - .hg-button:not(:last-child) { - margin-right: 8px; - margin-bottom: 3px; -} - -.simple-keyboard.simple-keyboard.oddTheme1 .hg-button { - height: 48px; -} - -.simple-keyboard .hg-button:active { - color: #16212d; - background-color: #e3e3e3; -} diff --git a/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/NumericalKeyboard.stories.tsx b/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/NumericalKeyboard.stories.tsx new file mode 100644 index 00000000000..710750697ff --- /dev/null +++ b/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/NumericalKeyboard.stories.tsx @@ -0,0 +1,80 @@ +import * as React from 'react' +import { + DIRECTION_COLUMN, + Flex, + POSITION_ABSOLUTE, + SPACING, +} from '@opentrons/components' +import { touchScreenViewport } from '../../../DesignTokens/constants' +import { InputField } from '../../InputField' +import { NumericalKeyboard } from '.' +import '../index.css' +import './index.css' + +import type { Story, Meta } from '@storybook/react' + +export default { + title: 'ODD/Atoms/SoftwareKeyboard/NumericalKeyboard', + component: NumericalKeyboard, + parameters: touchScreenViewport, + argTypes: { + isDecimal: { + control: { + type: 'boolean', + options: [true, false], + }, + defaultValue: false, + }, + hasHyphen: { + control: { + type: 'boolean', + options: [true, false], + }, + defaultValue: false, + }, + }, +} as Meta + +const Template: Story< + React.ComponentProps +> = args => { + const [showKeyboard, setShowKeyboard] = React.useState(false) + const [value, setValue] = React.useState('') + const keyboardRef = React.useRef(null) + return ( + +
+ { + setShowKeyboard(true) + }} + /> + + + {showKeyboard && ( + e != null && setValue(String(e))} + keyboardRef={keyboardRef} + isDecimal={args.isDecimal} + hasHyphen={args.hasHyphen} + /> + )} + +
+ ) +} + +export const Keyboard = Template.bind({}) +Keyboard.args = { + isDecimal: false, + hasHyphen: false, +} diff --git a/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/__tests__/NumericalKeyboard.test.tsx b/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/__tests__/NumericalKeyboard.test.tsx new file mode 100644 index 00000000000..0b3143554fa --- /dev/null +++ b/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/__tests__/NumericalKeyboard.test.tsx @@ -0,0 +1,178 @@ +import * as React from 'react' +import { describe, it, expect, vi } from 'vitest' +import '@testing-library/jest-dom/vitest' +import { fireEvent, renderHook, screen } from '@testing-library/react' +import { renderWithProviders } from '../../../../__testing-utils__' +import { NumericalKeyboard } from '..' + +const render = (props: React.ComponentProps) => { + return renderWithProviders()[0] +} + +describe('NumericalKeyboard', () => { + it('should render numerical keyboard isDecimal: false and hasHyphen: false', () => { + const { result } = renderHook(() => React.useRef(null)) + const props = { + onChange: vi.fn(), + keyboardRef: result.current, + isDecimal: false, + hasHyphen: false, + } + render(props) + const buttons = screen.getAllByRole('button') + const expectedButtonNames = [ + '1', + '2', + '3', + '4', + '5', + '6', + '7', + '8', + '9', + '0', + 'del', + ] + + buttons.forEach((button, index) => { + const expectedName = expectedButtonNames[index] + expect(button).toHaveTextContent(expectedName) + }) + }) + + it('should render numerical keyboard isDecimal: false and hasHyphen: true', () => { + const { result } = renderHook(() => React.useRef(null)) + const props = { + onChange: vi.fn(), + keyboardRef: result.current, + isDecimal: false, + hasHyphen: true, + } + render(props) + const buttons = screen.getAllByRole('button') + const expectedButtonNames = [ + '1', + '2', + '3', + '4', + '5', + '6', + '7', + '8', + '9', + '0', + '-', + 'del', + ] + + buttons.forEach((button, index) => { + const expectedName = expectedButtonNames[index] + expect(button).toHaveTextContent(expectedName) + }) + }) + + it('should render numerical keyboard isDecimal: true and hasHyphen: false', () => { + const { result } = renderHook(() => React.useRef(null)) + const props = { + onChange: vi.fn(), + keyboardRef: result.current, + isDecimal: true, + hasHyphen: false, + } + render(props) + const buttons = screen.getAllByRole('button') + const expectedButtonNames = [ + '1', + '2', + '3', + '4', + '5', + '6', + '7', + '8', + '9', + '0', + '.', + 'del', + ] + + buttons.forEach((button, index) => { + const expectedName = expectedButtonNames[index] + expect(button).toHaveTextContent(expectedName) + }) + }) + + it('should render numerical keyboard isDecimal: true and hasHyphen: true', () => { + const { result } = renderHook(() => React.useRef(null)) + const props = { + onChange: vi.fn(), + keyboardRef: result.current, + isDecimal: true, + hasHyphen: true, + } + render(props) + const buttons = screen.getAllByRole('button') + const expectedButtonNames = [ + '1', + '2', + '3', + '4', + '5', + '6', + '7', + '8', + '9', + '0', + '.', + '-', + 'del', + ] + + buttons.forEach((button, index) => { + const expectedName = expectedButtonNames[index] + expect(button).toHaveTextContent(expectedName) + }) + }) + + it('should call mock function when clicking num key', () => { + const { result } = renderHook(() => React.useRef(null)) + const props = { + onChange: vi.fn(), + keyboardRef: result.current, + isDecimal: false, + hasHyphen: false, + } + render(props) + const numKey = screen.getByRole('button', { name: '1' }) + fireEvent.click(numKey) + expect(props.onChange).toHaveBeenCalled() + }) + + it('should call mock function when clicking decimal point key', () => { + const { result } = renderHook(() => React.useRef(null)) + const props = { + onChange: vi.fn(), + keyboardRef: result.current, + isDecimal: true, + hasHyphen: false, + } + render(props) + const numKey = screen.getByRole('button', { name: '.' }) + fireEvent.click(numKey) + expect(props.onChange).toHaveBeenCalled() + }) + + it('should call mock function when clicking hyphen key', () => { + const { result } = renderHook(() => React.useRef(null)) + const props = { + onChange: vi.fn(), + keyboardRef: result.current, + isDecimal: true, + hasHyphen: true, + } + render(props) + const numKey = screen.getByRole('button', { name: '-' }) + fireEvent.click(numKey) + expect(props.onChange).toHaveBeenCalled() + }) +}) diff --git a/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/index.css b/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/index.css new file mode 100644 index 00000000000..239f86ba664 --- /dev/null +++ b/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/index.css @@ -0,0 +1,52 @@ +/* stylelint-disable */ + +/* Numerical Keyboard has 4 layouts + 1. int not allowed negative: intKeyboard + 2. int allowed negative: intNegKeyboard + 3. float not allowed negative: floatKeyboard + 4. float not allowed negative: floatNegKeyboard + */ + +.simple-keyboard.oddTheme1.hg-theme-default { + width: 100%; + height: 100%; + background-color: #cbcccc; /* grey35 */ + font-family: 'Public Sans', sans-serif; + padding: 8px; +} + +.hg-layout-intKeyboard .hg-row, +.hg-layout-intNegKeyboard .hg-row, +.hg-layout-floatKeyboard .hg-row, +.hg-layout-floatNegKeyboard .hg-row { + grid-gap: 3px; +} +.numerical-keyboard .hg-row .hg-button { + text-align: center; + font-size: 20px; + font-weight: 600; + line-height: 24px; + height: 75px; + padding: 10px 22px; +} + +.hg-layout-intKeyboard .hg-row:nth-child(-n + 3) .hg-button, +.hg-layout-intNegKeyboard .hg-row:nth-child(-n + 4) .hg-button, +.hg-layout-floatKeyboard .hg-row:nth-child(-n + 4) .hg-button, +.hg-layout-floatNegKeyboard .hg-row:nth-child(-n + 3) .hg-button { + width: 109.3px; + margin-bottom: 3px; +} + +.hg-layout-intKeyboard .hg-row:nth-child(4) .hg-button { + width: 168px; +} + +.hg-layout-floatNegKeyboard .hg-row:nth-child(4) .hg-button { + width: 80px; +} + +.simple-keyboard .hg-button:active { + color: #16212d; + background-color: #dedede; /* grey30 */ +} diff --git a/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/index.tsx b/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/index.tsx new file mode 100644 index 00000000000..85d1a0b8b43 --- /dev/null +++ b/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/index.tsx @@ -0,0 +1,39 @@ +import * as React from 'react' +import Keyboard from 'react-simple-keyboard' +import { numericalKeyboardLayout, numericalCustom } from '../constants' + +interface NumericalKeyboardProps { + onChange: (input: string) => void + keyboardRef: React.MutableRefObject + isDecimal?: boolean + hasHyphen?: boolean +} + +// the default keyboard layout intKeyboard that doesn't have decimal point and hyphen. +export function NumericalKeyboard({ + onChange, + keyboardRef, + isDecimal = false, + hasHyphen = false, +}: NumericalKeyboardProps): JSX.Element { + const layoutName = `${isDecimal ? 'float' : 'int'}${ + hasHyphen ? 'NegKeyboard' : 'Keyboard' + }` + + return ( + /* + * autoUseTouchEvents: for Flex on-device app + * useButtonTag: this is for testing purpose that each key renders as a button + */ + (keyboardRef.current = r)} + theme={'hg-theme-default oddTheme1 numerical-keyboard'} + onChange={onChange} + display={numericalCustom} + autoUseTouchEvents={true} + useButtonTag={true} + layoutName={layoutName} + layout={numericalKeyboardLayout} + /> + ) +} diff --git a/app/src/atoms/SoftwareKeyboard/Numpad/__tests__/Numpad.test.tsx b/app/src/atoms/SoftwareKeyboard/Numpad/__tests__/Numpad.test.tsx deleted file mode 100644 index f9c90938eba..00000000000 --- a/app/src/atoms/SoftwareKeyboard/Numpad/__tests__/Numpad.test.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import * as React from 'react' -import { describe, it, expect, vi } from 'vitest' -import '@testing-library/jest-dom/vitest' -import { fireEvent, renderHook, screen } from '@testing-library/react' -import { renderWithProviders } from '../../../../__testing-utils__' -import { Numpad } from '..' - -const render = (props: React.ComponentProps) => { - return renderWithProviders()[0] -} - -describe('Numpad', () => { - it('should render the numpad keys', () => { - const { result } = renderHook(() => React.useRef(null)) - const props = { - onChange: vi.fn(), - keyboardRef: result.current, - } - render(props) - const buttons = screen.getAllByRole('button') - const expectedButtonNames = [ - '7', - '8', - '9', - '4', - '5', - '6', - '1', - '2', - '3', - '0', - '.', - 'del', - ] - - buttons.forEach((button, index) => { - const expectedName = expectedButtonNames[index] - expect(button).toHaveTextContent(expectedName) - }) - }) - - it('should call mock function when clicking num key', () => { - const { result } = renderHook(() => React.useRef(null)) - const props = { - onChange: vi.fn(), - keyboardRef: result.current, - } - render(props) - const numKey = screen.getByRole('button', { name: '1' }) - fireEvent.click(numKey) - expect(props.onChange).toHaveBeenCalled() - }) -}) diff --git a/app/src/atoms/SoftwareKeyboard/Numpad/index.css b/app/src/atoms/SoftwareKeyboard/Numpad/index.css deleted file mode 100644 index 7d832afeb2f..00000000000 --- a/app/src/atoms/SoftwareKeyboard/Numpad/index.css +++ /dev/null @@ -1,7 +0,0 @@ -/* stylelint-disable */ - -.numpad button.hg-button.hg-button-backspace, -.numpad button.hg-button.hg-button-abc, -.numpad button.hg-button.hg-standardBtn { - flex: 1; -} diff --git a/app/src/atoms/SoftwareKeyboard/constants.ts b/app/src/atoms/SoftwareKeyboard/constants.ts index 11fe6f11272..1808f4bd2f3 100644 --- a/app/src/atoms/SoftwareKeyboard/constants.ts +++ b/app/src/atoms/SoftwareKeyboard/constants.ts @@ -1,8 +1,71 @@ export const customDisplay = { '{numbers}': '123', - '{shift}': 'SHIFT', + '{shift}': 'ABC', '{space}': 'space', '{backspace}': 'del', '{abc}': 'abc', + '{ABC}': 'ABC', '{symbols}': '#+=', } + +// keyboard layout for Alphanumeric Keyboard +export const alphanumericKeyboardLayout = { + default: [ + 'q w e r t y u i o p', + '{numbers} a s d f g h j k l', + '{ABC} z x c v b n m {backspace}', + ], + shift: [ + 'Q W E R T Y U I O P', + '{numbers} A S D F G H J K L', + '{abc} Z X C V B N M {backspace}', + ], + numbers: ['1 2 3', '4 5 6', '7 8 9', '{abc} 0 {backspace}'], +} + +// keyboard layout for Full Keyboard +export const fullKeyboardLayout = { + default: [ + 'q w e r t y u i o p', + '{numbers} a s d f g h j k l', + '{shift} z x c v b n m {backspace}', + '{space}', + ], + shift: [ + 'Q W E R T Y U I O P', + '{numbers} A S D F G H J K L', + '{abc} Z X C V B N M {backspace}', + '{space}', + ], + symbols: [ + '[ ] { } % ^ +', + '{abc} _ \\ | < > # =', + "{numbers} . , ? ! ' * ~ {backspace}", + '{space}', + ], + numbers: [ + '1 2 3 4 5 6 7 8 9 0', + '{abc} - / : ; ( ) $ & @ "', + "{symbols} . , ? ! ' * ~ {backspace}", + '{space}', + ], +} + +// Numerical keyboard layout +export const numericalKeyboardLayout = { + // int without negative value + intKeyboard: ['1 2 3', '4 5 6', '7 8 9', '0 {backspace}'], + + // int with negative value + intNegKeyboard: ['1 2 3', '4 5 6', '7 8 9', '0 - {backspace}'], + + // float without negative value, + floatKeyboard: ['1 2 3', '4 5 6', '7 8 9', '0 . {backspace}'], + + // float with negative value + floatNegKeyboard: ['1 2 3', '4 5 6', '7 8 9', '0 . - {backspace}'], +} + +export const numericalCustom = { + '{backspace}': 'del', +} diff --git a/app/src/atoms/SoftwareKeyboard/index.ts b/app/src/atoms/SoftwareKeyboard/index.ts index 93ae28749ac..81dc2e2b4fb 100644 --- a/app/src/atoms/SoftwareKeyboard/index.ts +++ b/app/src/atoms/SoftwareKeyboard/index.ts @@ -1,3 +1,4 @@ -export { CustomKeyboard } from './CustomKeyboard' -export { NormalKeyboard } from './NormalKeyboard' -export { Numpad } from './Numpad' +export { AlphanumericKeyboard } from './AlphanumericKeyboard' +export { IndividualKey } from './IndividualKey' +export { FullKeyboard } from './FullKeyboard' +export { NumericalKeyboard } from './NumericalKeyboard' diff --git a/app/src/index.tsx b/app/src/index.tsx index 123cfcc26fd..f6f4918d769 100644 --- a/app/src/index.tsx +++ b/app/src/index.tsx @@ -15,10 +15,10 @@ import { uiInitialized } from './redux/shell' import { history } from './redux/reducer' import { store } from './redux/store' -import '../src/atoms/SoftwareKeyboard/index.css' -import '../src/atoms/SoftwareKeyboard/CustomKeyboard/index.css' -import '../src/atoms/SoftwareKeyboard/NormalKeyboard/index.css' -import '../src/atoms/SoftwareKeyboard/Numpad/index.css' +import '../src/atoms/SoftwareKeyboard/AlphanumericKeyboard' +import '../src/atoms/SoftwareKeyboard/FullKeyboard/index.css' +import '../src/atoms/SoftwareKeyboard/IndividualKey/index.css' +import '../src/atoms/SoftwareKeyboard/NumericalKeyboard/index.css' // component tree import { App } from './App' diff --git a/app/src/organisms/NetworkSettings/SetWifiCred.tsx b/app/src/organisms/NetworkSettings/SetWifiCred.tsx index 876e10e0334..34cbef2330f 100644 --- a/app/src/organisms/NetworkSettings/SetWifiCred.tsx +++ b/app/src/organisms/NetworkSettings/SetWifiCred.tsx @@ -17,7 +17,7 @@ import { } from '@opentrons/components' import { InputField } from '../../atoms/InputField' -import { NormalKeyboard } from '../../atoms/SoftwareKeyboard' +import { FullKeyboard } from '../../atoms/SoftwareKeyboard' import { useIsUnboxingFlowOngoing } from '../RobotSettingsDashboard/NetworkSettings/hooks' interface SetWifiCredProps { @@ -78,7 +78,7 @@ export function SetWifiCred({
- e != null && setPassword(String(e))} keyboardRef={keyboardRef} /> diff --git a/app/src/organisms/NetworkSettings/SetWifiSsid.tsx b/app/src/organisms/NetworkSettings/SetWifiSsid.tsx index f9b2fdc8fff..9f920e9e519 100644 --- a/app/src/organisms/NetworkSettings/SetWifiSsid.tsx +++ b/app/src/organisms/NetworkSettings/SetWifiSsid.tsx @@ -12,7 +12,7 @@ import { } from '@opentrons/components' import { InputField } from '../../atoms/InputField' -import { NormalKeyboard } from '../../atoms/SoftwareKeyboard' +import { FullKeyboard } from '../../atoms/SoftwareKeyboard' import { useIsUnboxingFlowOngoing } from '../RobotSettingsDashboard/NetworkSettings/hooks' interface SetWifiSsidProps { @@ -57,7 +57,7 @@ export function SetWifiSsid({ /> - { e != null && setInputSsid(e) }} diff --git a/app/src/organisms/NetworkSettings/__tests__/SetWifiCred.test.tsx b/app/src/organisms/NetworkSettings/__tests__/SetWifiCred.test.tsx index 6532203b4cb..0af38eff22d 100644 --- a/app/src/organisms/NetworkSettings/__tests__/SetWifiCred.test.tsx +++ b/app/src/organisms/NetworkSettings/__tests__/SetWifiCred.test.tsx @@ -43,7 +43,7 @@ describe('SetWifiCred', () => { // software keyboard screen.getByRole('button', { name: 'del' }) screen.getByRole('button', { name: 'a' }) - screen.getByRole('button', { name: 'SHIFT' }) + screen.getByRole('button', { name: 'ABC' }) }) it('should display password', () => { diff --git a/app/src/pages/NameRobot/index.tsx b/app/src/pages/NameRobot/index.tsx index 16a868dddb8..1bbf4099234 100644 --- a/app/src/pages/NameRobot/index.tsx +++ b/app/src/pages/NameRobot/index.tsx @@ -32,7 +32,7 @@ import { } from '../../redux/discovery' import { useTrackEvent, ANALYTICS_RENAME_ROBOT } from '../../redux/analytics' import { InputField } from '../../atoms/InputField' -import { CustomKeyboard } from '../../atoms/SoftwareKeyboard' +import { AlphanumericKeyboard } from '../../atoms/SoftwareKeyboard' import { SmallButton } from '../../atoms/buttons' import { StepMeter } from '../../atoms/StepMeter' import { useIsUnboxingFlowOngoing } from '../../organisms/RobotSettingsDashboard/NetworkSettings/hooks' @@ -295,7 +295,7 @@ export function NameRobot(): JSX.Element { control={control} name="newRobotName" render={({ field }) => ( - { field.onChange(input) trigger('newRobotName') diff --git a/app/src/styles.global.module.css b/app/src/styles.global.module.css index 9cdcb703387..2247749b91b 100644 --- a/app/src/styles.global.module.css +++ b/app/src/styles.global.module.css @@ -3,6 +3,7 @@ */ @import '../../node_modules/react-simple-keyboard/build/css/index.css'; -@import './atoms/SoftwareKeyboard/CustomKeyboard/index.css'; -@import './atoms/SoftwareKeyboard/NormalKeyboard/index.css'; -@import './atoms/SoftwareKeyboard/Numpad/index.css'; +@import './atoms/SoftwareKeyboard/AlphanumericKeyboard/index.css'; +@import './atoms/SoftwareKeyboard/FullKeyboard/index.css'; +@import './atoms/SoftwareKeyboard/NumericalKeyboard/index.css'; +@import './atoms/SoftwareKeyboard/IndividualKey/index.css'; From 53ecdbbb989aaeaefbb8c05fd8fcc24533c1e0d0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 1 Apr 2024 10:12:48 -0400 Subject: [PATCH 006/194] fix(app-testing): snapshot failure capture (#14761) This PR is an automated snapshot update request. Please review the changes and merge if they are acceptable or find you bug and fix it. Co-authored-by: y3rsh --- ...sis_snapshot[0f71566d05][OT2_P20S_None_2_7_Walkthrough].json | 2 +- ...t[20cefcac62][OT2_P300M_P20S_TC_HS_TM_2_13_SmokeTestV3].json | 2 +- ...t[4835239037][OT2_P300M_P20S_MM_TM_TC1_5_2_6_PD40Error].json | 2 +- ...pshot[753ac8811f][OT2_None_None_2_13_PythonSyntaxError].json | 2 +- ...ysis_snapshot[8df082e960][OT2_P300MLeft_MM_TM_2_4_Zymo].json | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0f71566d05][OT2_P20S_None_2_7_Walkthrough].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0f71566d05][OT2_P20S_None_2_7_Walkthrough].json index a50062b2e14..a79130779de 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0f71566d05][OT2_P20S_None_2_7_Walkthrough].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0f71566d05][OT2_P20S_None_2_7_Walkthrough].json @@ -3293,7 +3293,7 @@ "errorInfo": { "args": "()", "class": "AssertionError", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/protocol_engine.py\", line 441, in finish\n await exit_stack.aclose()\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 656, in aclose\n await self.__aexit__(None, None, None)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 714, in __aexit__\n raise exc_details[1]\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 697, in __aexit__\n cb_suppress = await cb(*exc_details)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 608, in _exit_wrapper\n await callback(*args, **kwds)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 102, in stop\n await p.teardown()\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 112, in teardown\n await self._action_dispatching_task\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 160, in _dispatch_all_actions\n self.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 37, in dispatch\n return self._action_dispatcher.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/actions/action_dispatcher.py\", line 30, in dispatch\n self._sink.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/state.py\", line 204, in handle_action\n substore.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/commands.py\", line 283, in handle_action\n assert self._state.running_command_id is None\n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/protocol_engine.py\", line 442, in finish\n await exit_stack.aclose()\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 656, in aclose\n await self.__aexit__(None, None, None)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 714, in __aexit__\n raise exc_details[1]\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 697, in __aexit__\n cb_suppress = await cb(*exc_details)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 608, in _exit_wrapper\n await callback(*args, **kwds)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 102, in stop\n await p.teardown()\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 112, in teardown\n await self._action_dispatching_task\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 160, in _dispatch_all_actions\n self.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 37, in dispatch\n return self._action_dispatcher.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/actions/action_dispatcher.py\", line 30, in dispatch\n self._sink.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/state.py\", line 204, in handle_action\n substore.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/commands.py\", line 243, in handle_action\n self._state.command_history.set_command_running(running_command)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/command_history.py\", line 175, in set_command_running\n assert self.get_running_command() is None\n" }, "errorType": "PythonException", "wrappedErrors": [] diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[20cefcac62][OT2_P300M_P20S_TC_HS_TM_2_13_SmokeTestV3].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[20cefcac62][OT2_P300M_P20S_TC_HS_TM_2_13_SmokeTestV3].json index c93a79f99e2..d974b696058 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[20cefcac62][OT2_P300M_P20S_TC_HS_TM_2_13_SmokeTestV3].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[20cefcac62][OT2_P300M_P20S_TC_HS_TM_2_13_SmokeTestV3].json @@ -11889,7 +11889,7 @@ "errorInfo": { "args": "()", "class": "AssertionError", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/protocol_engine.py\", line 441, in finish\n await exit_stack.aclose()\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 656, in aclose\n await self.__aexit__(None, None, None)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 714, in __aexit__\n raise exc_details[1]\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 697, in __aexit__\n cb_suppress = await cb(*exc_details)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 608, in _exit_wrapper\n await callback(*args, **kwds)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 102, in stop\n await p.teardown()\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 112, in teardown\n await self._action_dispatching_task\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 160, in _dispatch_all_actions\n self.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 37, in dispatch\n return self._action_dispatcher.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/actions/action_dispatcher.py\", line 30, in dispatch\n self._sink.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/state.py\", line 204, in handle_action\n substore.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/commands.py\", line 283, in handle_action\n assert self._state.running_command_id is None\n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/protocol_engine.py\", line 442, in finish\n await exit_stack.aclose()\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 656, in aclose\n await self.__aexit__(None, None, None)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 714, in __aexit__\n raise exc_details[1]\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 697, in __aexit__\n cb_suppress = await cb(*exc_details)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 608, in _exit_wrapper\n await callback(*args, **kwds)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 102, in stop\n await p.teardown()\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 112, in teardown\n await self._action_dispatching_task\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 160, in _dispatch_all_actions\n self.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 37, in dispatch\n return self._action_dispatcher.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/actions/action_dispatcher.py\", line 30, in dispatch\n self._sink.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/state.py\", line 204, in handle_action\n substore.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/commands.py\", line 243, in handle_action\n self._state.command_history.set_command_running(running_command)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/command_history.py\", line 175, in set_command_running\n assert self.get_running_command() is None\n" }, "errorType": "PythonException", "wrappedErrors": [] 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 aadb742ef09..1c888cd46cc 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 @@ -6965,7 +6965,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/protocol_runner.py\", line 218, in run_func\n await self._legacy_executor.execute(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_wrappers.py\", line 180, in execute\n await to_thread.run_sync(\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 63, 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 270, 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 84, in _run\n await self._run_func()\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/task_queue.py\", line 61, in _do_run\n await func(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/protocol_runner.py\", line 218, in run_func\n await self._legacy_executor.execute(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_wrappers.py\", line 180, in execute\n await to_thread.run_sync(\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 63, 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 270, 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[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 04709c61b18..aab8caadd15 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 @@ -31,7 +31,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/protocol_runner.py\", line 218, in run_func\n await self._legacy_executor.execute(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_wrappers.py\", line 180, in execute\n await to_thread.run_sync(\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 40, in run_protocol\n run_python(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 95, 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 84, in _run\n await self._run_func()\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/task_queue.py\", line 61, in _do_run\n await func(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/protocol_runner.py\", line 218, in run_func\n await self._legacy_executor.execute(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_wrappers.py\", line 180, in execute\n await to_thread.run_sync(\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 40, in run_protocol\n run_python(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 95, 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[8df082e960][OT2_P300MLeft_MM_TM_2_4_Zymo].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8df082e960][OT2_P300MLeft_MM_TM_2_4_Zymo].json index 6ab08090f78..02165d003c7 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8df082e960][OT2_P300MLeft_MM_TM_2_4_Zymo].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8df082e960][OT2_P300MLeft_MM_TM_2_4_Zymo].json @@ -10913,7 +10913,7 @@ "errorInfo": { "args": "()", "class": "AssertionError", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/protocol_engine.py\", line 441, in finish\n await exit_stack.aclose()\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 656, in aclose\n await self.__aexit__(None, None, None)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 714, in __aexit__\n raise exc_details[1]\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 697, in __aexit__\n cb_suppress = await cb(*exc_details)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 608, in _exit_wrapper\n await callback(*args, **kwds)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 102, in stop\n await p.teardown()\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 112, in teardown\n await self._action_dispatching_task\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 160, in _dispatch_all_actions\n self.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 37, in dispatch\n return self._action_dispatcher.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/actions/action_dispatcher.py\", line 30, in dispatch\n self._sink.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/state.py\", line 204, in handle_action\n substore.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/commands.py\", line 283, in handle_action\n assert self._state.running_command_id is None\n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/protocol_engine.py\", line 442, in finish\n await exit_stack.aclose()\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 656, in aclose\n await self.__aexit__(None, None, None)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 714, in __aexit__\n raise exc_details[1]\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 697, in __aexit__\n cb_suppress = await cb(*exc_details)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 608, in _exit_wrapper\n await callback(*args, **kwds)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 102, in stop\n await p.teardown()\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 112, in teardown\n await self._action_dispatching_task\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 160, in _dispatch_all_actions\n self.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 37, in dispatch\n return self._action_dispatcher.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/actions/action_dispatcher.py\", line 30, in dispatch\n self._sink.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/state.py\", line 204, in handle_action\n substore.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/commands.py\", line 243, in handle_action\n self._state.command_history.set_command_running(running_command)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/command_history.py\", line 175, in set_command_running\n assert self.get_running_command() is None\n" }, "errorType": "PythonException", "wrappedErrors": [] From 45b6fa902a7d7075e2e782c381ae677df77f8b5c Mon Sep 17 00:00:00 2001 From: Nick Diehl <47604184+ncdiehl11@users.noreply.github.com> Date: Mon, 1 Apr 2024 10:45:28 -0400 Subject: [PATCH 007/194] feat(app, api-client): implement runTimeParametersValues in run creation (#14742) closes [AUTH-101](https://opentrons.atlassian.net/browse/AUTH-101) --- api-client/src/runs/createRun.ts | 7 ++++++- api-client/src/runs/types.ts | 4 ++++ .../ChooseRobotToRunProtocolSlideout.test.tsx | 19 ++++++++++++++----- .../index.tsx | 9 ++++++++- .../useCreateRunFromProtocol.ts | 10 ++++++++-- 5 files changed, 40 insertions(+), 9 deletions(-) diff --git a/api-client/src/runs/createRun.ts b/api-client/src/runs/createRun.ts index 285802d85b2..5b2883917c6 100644 --- a/api-client/src/runs/createRun.ts +++ b/api-client/src/runs/createRun.ts @@ -2,11 +2,16 @@ import { POST, request } from '../request' import type { ResponsePromise } from '../request' import type { HostConfig } from '../types' -import type { Run, LabwareOffsetCreateData } from './types' +import type { + Run, + LabwareOffsetCreateData, + RuntimeParameterCreateData, +} from './types' export interface CreateRunData { protocolId?: string labwareOffsets?: LabwareOffsetCreateData[] + runTimeParameterValues?: RuntimeParameterCreateData } export function createRun( diff --git a/api-client/src/runs/types.ts b/api-client/src/runs/types.ts index 7709e580a5e..0be2a9973ed 100644 --- a/api-client/src/runs/types.ts +++ b/api-client/src/runs/types.ts @@ -125,6 +125,10 @@ export interface LabwareOffsetCreateData { vector: VectorOffset } +export interface RuntimeParameterCreateData { + [key: string]: string | boolean | number +} + export interface CommandData { data: RunTimeCommand } diff --git a/app/src/organisms/ChooseRobotToRunProtocolSlideout/__tests__/ChooseRobotToRunProtocolSlideout.test.tsx b/app/src/organisms/ChooseRobotToRunProtocolSlideout/__tests__/ChooseRobotToRunProtocolSlideout.test.tsx index 3e9e437bbc4..8a7c9f64597 100644 --- a/app/src/organisms/ChooseRobotToRunProtocolSlideout/__tests__/ChooseRobotToRunProtocolSlideout.test.tsx +++ b/app/src/organisms/ChooseRobotToRunProtocolSlideout/__tests__/ChooseRobotToRunProtocolSlideout.test.tsx @@ -95,14 +95,20 @@ describe('ChooseRobotToRunProtocolSlideout', () => { .calledWith( expect.any(Object), { hostname: expect.any(String) }, - expect.any(Array) + expect.any(Array), + expect.any(Object) ) .thenReturn({ createRunFromProtocolSource: mockCreateRunFromProtocolSource, reset: mockResetCreateRun, } as any) when(vi.mocked(useCreateRunFromProtocol)) - .calledWith(expect.any(Object), null, expect.any(Array)) + .calledWith( + expect.any(Object), + null, + expect.any(Array), + expect.any(Object) + ) .thenReturn({ createRunFromProtocolSource: mockCreateRunFromProtocolSource, reset: mockResetCreateRun, @@ -315,7 +321,8 @@ describe('ChooseRobotToRunProtocolSlideout', () => { location: mockOffsetCandidate.location, definitionUri: mockOffsetCandidate.definitionUri, }, - ] + ], + {} ) expect(screen.getByRole('checkbox')).toBeChecked() const proceedButton = screen.getByRole('button', { @@ -373,13 +380,15 @@ describe('ChooseRobotToRunProtocolSlideout', () => { location: mockOffsetCandidate.location, definitionUri: mockOffsetCandidate.definitionUri, }, - ] + ], + {} ) expect(vi.mocked(useCreateRunFromProtocol)).nthCalledWith( 3, expect.any(Object), { hostname: 'otherIp' }, - [] + [], + {} ) }) }) diff --git a/app/src/organisms/ChooseRobotToRunProtocolSlideout/index.tsx b/app/src/organisms/ChooseRobotToRunProtocolSlideout/index.tsx index 56c1d9dd06e..cc94ee94457 100644 --- a/app/src/organisms/ChooseRobotToRunProtocolSlideout/index.tsx +++ b/app/src/organisms/ChooseRobotToRunProtocolSlideout/index.tsx @@ -165,7 +165,14 @@ export function ChooseRobotToRunProtocolSlideoutComponent( location, definitionUri, })) - : [] + : [], + runTimeParametersOverrides.reduce( + (acc, param) => + param.value !== param.default + ? { ...acc, [param.variableName]: param.value } + : acc, + {} + ) ) const handleProceed: React.MouseEventHandler = () => { trackCreateProtocolRunEvent({ name: 'createProtocolRecordRequest' }) diff --git a/app/src/organisms/ChooseRobotToRunProtocolSlideout/useCreateRunFromProtocol.ts b/app/src/organisms/ChooseRobotToRunProtocolSlideout/useCreateRunFromProtocol.ts index f44f92cb8c6..0e897881c5c 100644 --- a/app/src/organisms/ChooseRobotToRunProtocolSlideout/useCreateRunFromProtocol.ts +++ b/app/src/organisms/ChooseRobotToRunProtocolSlideout/useCreateRunFromProtocol.ts @@ -14,6 +14,7 @@ import type { HostConfig, LabwareOffsetCreateData, Protocol, + RuntimeParameterCreateData, } from '@opentrons/api-client' import type { UseCreateRunMutationOptions } from '@opentrons/react-api-client/src/runs/useCreateRunMutation' import type { CreateProtocolVariables } from '@opentrons/react-api-client/src/protocols/useCreateProtocolMutation' @@ -35,7 +36,8 @@ export interface UseCreateRun { export function useCreateRunFromProtocol( options: UseCreateRunMutationOptions, hostOverride?: HostConfig | null, - labwareOffsets?: LabwareOffsetCreateData[] + labwareOffsets?: LabwareOffsetCreateData[], + runTimeParameterValues?: RuntimeParameterCreateData ): UseCreateRun { const contextHost = useHost() const host = @@ -74,7 +76,11 @@ export function useCreateRunFromProtocol( } = useCreateProtocolMutation( { onSuccess: data => { - createRun({ protocolId: data.data.id, labwareOffsets }) + createRun({ + protocolId: data.data.id, + labwareOffsets, + runTimeParameterValues, + }) }, }, host From bb33f7c6d24aa65dedf429da9aa5b988949231cb Mon Sep 17 00:00:00 2001 From: Caila Marashaj <98041399+caila-marashaj@users.noreply.github.com> Date: Mon, 1 Apr 2024 12:02:30 -0400 Subject: [PATCH 008/194] fix(modules): use parse from packaging module (#14732) --- .../hardware_control/modules/mod_abc.py | 19 +++++++++++++------ .../hardware_control/test_modules.py | 19 +++++++++++++++++++ 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/api/src/opentrons/hardware_control/modules/mod_abc.py b/api/src/opentrons/hardware_control/modules/mod_abc.py index c6ea41437eb..9d5527991f6 100644 --- a/api/src/opentrons/hardware_control/modules/mod_abc.py +++ b/api/src/opentrons/hardware_control/modules/mod_abc.py @@ -2,9 +2,8 @@ import asyncio import logging import re -from pkg_resources import parse_version -from typing import ClassVar, Mapping, Optional, cast, TypeVar - +from typing import ClassVar, Mapping, Optional, TypeVar +from packaging.version import InvalidVersion, parse, Version from opentrons.config import IS_ROBOT, ROBOT_FIRMWARE_DIR from opentrons.drivers.rpi_drivers.types import USBPort @@ -16,6 +15,14 @@ TaskPayload = TypeVar("TaskPayload") +def parse_fw_version(version: str) -> Version: + try: + device_version = parse(version) + except InvalidVersion: + device_version = parse("v0.0.0") + return device_version + + class AbstractModule(abc.ABC): """Defines the common methods of a module.""" @@ -88,9 +95,9 @@ def get_bundled_fw(self) -> Optional[BundledFirmware]: def has_available_update(self) -> bool: """Return whether a newer firmware file is available""" if self.device_info and self._bundled_fw: - device_version = parse_version(self.device_info["version"]) - available_version = parse_version(self._bundled_fw.version) - return cast(bool, available_version > device_version) + device_version = parse_fw_version(self.device_info["version"]) + available_version = parse_fw_version(self._bundled_fw.version) + return available_version > device_version return False async def wait_for_is_running(self) -> None: diff --git a/api/tests/opentrons/hardware_control/test_modules.py b/api/tests/opentrons/hardware_control/test_modules.py index ce92ad2c1a8..eb3d0e48c6c 100644 --- a/api/tests/opentrons/hardware_control/test_modules.py +++ b/api/tests/opentrons/hardware_control/test_modules.py @@ -3,6 +3,7 @@ from pathlib import Path from unittest import mock +from packaging.version import Version from opentrons.hardware_control import ExecutionManager from opentrons.hardware_control.modules import ModuleAtPort @@ -22,6 +23,7 @@ HeaterShaker, AbstractModule, ) +from opentrons.hardware_control.modules.mod_abc import parse_fw_version from opentrons.drivers.rpi_drivers.types import USBPort @@ -422,3 +424,20 @@ def test_magnetic_module_revision_parsing(revision, model): ) def test_temperature_module_revision_parsing(revision, model): assert TempDeck._model_from_revision(revision) == model + + +@pytest.mark.parametrize( + argnames=["device_version", "expected_result"], + argvalues=[ + ["v1.0.4", Version("v1.0.4")], + ["v0.5.6", Version("v0.5.6")], + ["v1.0.4-dhfs", Version("v0.0.0")], + ["v3.0.dshjfd", Version("v0.0.0")], + ], +) +async def test_catch_invalid_fw_version( + device_version: str, + expected_result: bool, +) -> None: + """Assert that invalid firmware versions prompt a valid Version object of v0.0.0.""" + assert parse_fw_version(device_version) == expected_result From 4bf9073f9b1e491da05ff95fe669a08010abb0ad Mon Sep 17 00:00:00 2001 From: Ed Cormany Date: Mon, 1 Apr 2024 13:56:50 -0400 Subject: [PATCH 009/194] chore(release): add release note for speaker and camera (#14768) Release note for OT-2 speaker and camera fix yet to be merged. --- api/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/api/release-notes.md b/api/release-notes.md index a1db0c0e1f3..046b3e1e04b 100644 --- a/api/release-notes.md +++ b/api/release-notes.md @@ -14,6 +14,10 @@ Welcome to the v7.2.2 release of the Opentrons robot software! - Improved the low-volume performance of recently produced Flex 96-Channel Pipettes. +### Bug Fixes + +- Restores the ability to use the speaker and camera on OT-2. + --- ## Opentrons Robot Software Changes in 7.2.1 From ad5650d3e4fad904b9d1cfa146b3f292f13109fe Mon Sep 17 00:00:00 2001 From: Rhyann Clarke <146747548+rclarke0@users.noreply.github.com> Date: Mon, 1 Apr 2024 14:03:40 -0400 Subject: [PATCH 010/194] ABR JIRA TICKET CREATION. (#14767) # Overview Automate JIRA Ticket Creation Process for robots with errors. # Test Plan - used with ABR robots when errors have occurred. Tickets have been recorded accurately. # Changelog - Removed uncertain error levels in error_levels to allow for "Level # Failure" component to be filled in on JIRA - Created jira_tools function to create tickets, open tickets, collect error information from robot, read issues on board - jira_tools function add_attachments_to_ticket currently results in an error. The run log is saved as a file on your computer but cannot be added to the ticket . - added arguments for JIRA api key, storage directory, robot ip, email, board id # Review requests # Risk assessment - JIRA api token was acciedntly uploaded in previous merges but the token has been retired. - add_attachments_to_ticket does not currently work. - This script will only work if you run it before the errored out robot starts another run. - RABR is currently hard coded as the board to post to. --- .../__init__.py | 0 .../google_drive_tool.py | 0 .../google_sheets_tool.py | 0 .../abr_testing/automation/jira_tool.py | 275 ++++++++++++++++++ .../data_collection/abr_google_drive.py | 2 +- .../data_collection/error_levels.csv | 12 +- .../data_collection/get_run_logs.py | 8 +- .../data_collection/read_robot_logs.py | 10 + .../abr_testing/tools/abr_asair_sensor.py | 2 +- abr-testing/abr_testing/tools/abr_scale.py | 2 +- 10 files changed, 298 insertions(+), 13 deletions(-) rename abr-testing/abr_testing/{google_automation => automation}/__init__.py (100%) rename abr-testing/abr_testing/{google_automation => automation}/google_drive_tool.py (100%) rename abr-testing/abr_testing/{google_automation => automation}/google_sheets_tool.py (100%) create mode 100644 abr-testing/abr_testing/automation/jira_tool.py diff --git a/abr-testing/abr_testing/google_automation/__init__.py b/abr-testing/abr_testing/automation/__init__.py similarity index 100% rename from abr-testing/abr_testing/google_automation/__init__.py rename to abr-testing/abr_testing/automation/__init__.py diff --git a/abr-testing/abr_testing/google_automation/google_drive_tool.py b/abr-testing/abr_testing/automation/google_drive_tool.py similarity index 100% rename from abr-testing/abr_testing/google_automation/google_drive_tool.py rename to abr-testing/abr_testing/automation/google_drive_tool.py diff --git a/abr-testing/abr_testing/google_automation/google_sheets_tool.py b/abr-testing/abr_testing/automation/google_sheets_tool.py similarity index 100% rename from abr-testing/abr_testing/google_automation/google_sheets_tool.py rename to abr-testing/abr_testing/automation/google_sheets_tool.py diff --git a/abr-testing/abr_testing/automation/jira_tool.py b/abr-testing/abr_testing/automation/jira_tool.py new file mode 100644 index 00000000000..a98b023a44a --- /dev/null +++ b/abr-testing/abr_testing/automation/jira_tool.py @@ -0,0 +1,275 @@ +"""JIRA Ticket Creator.""" + +import requests +from requests.auth import HTTPBasicAuth +import json +import webbrowser +import argparse +from typing import List, Tuple +from abr_testing.data_collection import read_robot_logs, abr_google_drive, get_run_logs + + +def get_error_runs_from_robot(ip: str) -> List[str]: + """Get runs that have errors from robot.""" + error_run_ids = [] + response = requests.get( + f"http://{ip}:31950/runs", headers={"opentrons-version": "3"} + ) + run_data = response.json() + run_list = run_data["data"] + for run in run_list: + run_id = run["id"] + num_of_errors = len(run["errors"]) + if not run["current"] and num_of_errors > 0: + error_run_ids.append(run_id) + return error_run_ids + + +def get_error_info_from_robot( + ip: str, one_run: str, storage_directory: str +) -> Tuple[str, str, str, List[str], str, str]: + """Get error information from robot to fill out ticket.""" + description = dict() + # get run information + results = get_run_logs.get_run_data(one_run, ip) + # save run information to local directory as .json file + saved_file_path = read_robot_logs.save_run_log_to_json( + ip, results, storage_directory + ) + + # Error Printout + ( + num_of_errors, + error_type, + error_code, + error_instrument, + error_level, + ) = read_robot_logs.get_error_info(results) + # JIRA Ticket Fields + failure_level = "Level " + str(error_level) + " Failure" + components = [failure_level, "Flex-RABR"] + affects_version = results["API_Version"] + parent = results.get("robot_name", "") + print(parent) + summary = parent + "_" + str(one_run) + "_" + str(error_code) + "_" + error_type + # Description of error + description["protocol_name"] = results["protocol"]["metadata"].get( + "protocolName", "" + ) + description["error"] = " ".join([error_code, error_type, error_instrument]) + description["protocol_step"] = list(results["commands"])[-1] + description["right_mount"] = results.get("right", "No attachment") + description["left_mount"] = results.get("left", "No attachment") + description["gripper"] = results.get("extension", "No attachment") + all_modules = abr_google_drive.get_modules(results) + whole_description = {**description, **all_modules} + whole_description_str = ( + "{" + + "\n".join("{!r}: {!r},".format(k, v) for k, v in whole_description.items()) + + "}" + ) + + return ( + summary, + parent, + affects_version, + components, + whole_description_str, + saved_file_path, + ) + + +class JiraTicket: + """Connects to JIRA ticket site.""" + + def __init__(self, url: str, api_token: str, email: str) -> None: + """Connect to jira.""" + self.url = url + self.api_token = api_token + self.email = email + self.auth = HTTPBasicAuth(email, api_token) + self.headers = { + "Accept": "application/json", + "Content-Type": "application/json", + } + + def issues_on_board(self, board_id: str) -> List[str]: + """Print Issues on board.""" + response = requests.get( + f"{self.url}/rest/agile/1.0/board/{board_id}/issue", + headers=self.headers, + auth=self.auth, + ) + response.raise_for_status() + try: + board_data = response.json() + all_issues = board_data["issues"] + except json.JSONDecodeError as e: + print("Error decoding json: ", e) + issue_ids = [] + for i in all_issues: + issue_id = i.get("id") + issue_ids.append(issue_id) + return issue_ids + + def open_issue(self, issue_key: str) -> None: + """Open issue on web browser.""" + url = f"{self.url}/browse/{issue_key}" + webbrowser.open(url) + + def create_ticket( + self, + summary: str, + description: str, + project_key: str, + reporter_id: str, + issue_type: str, + priority: str, + components: list, + affects_versions: str, + robot: str, + ) -> Tuple[str, str]: + """Create ticket.""" + data = { + "fields": { + "project": {"id": "10273", "key": project_key}, + "issuetype": {"name": issue_type}, + "summary": summary, + "reporter": {"id": reporter_id}, + "parent": {"key": robot}, + "priority": {"name": priority}, + "components": [{"name": component} for component in components], + "versions": [{"name": affects_versions}], + "description": { + "content": [ + { + "content": [{"text": description, "type": "text"}], + "type": "paragraph", + } + ], + "type": "doc", + "version": 1, + } + # Include other required fields as needed + } + } + try: + response = requests.post( + f"{self.url}/rest/api/3/issue/", + headers=self.headers, + auth=self.auth, + json=data, + ) + response.raise_for_status() + response_str = str(response.content) + issue_url = response.json().get("self") + issue_key = response.json().get("key") + if issue_key is None: + print("Error: Could not create issue. No key returned.") + except requests.exceptions.HTTPError: + print(f"HTTP error occurred. Response content: {response_str}") + except json.JSONDecodeError: + print(f"JSON decoding error occurred. Response content: {response_str}") + return issue_url, issue_key + + def post_attachment_to_ticket(self, issue_id: str, attachment_path: str) -> None: + """Adds attachments to ticket.""" + # TODO: Ensure that file is actually uploaded. + file = {"file": open(attachment_path, "rb")} + JSON_headers = {"Accept": "application/json"} + try: + response = requests.post( + f"{self.url}/rest/api/3/issue/{issue_id}/attachments", + headers=JSON_headers, + auth=self.auth, + files=file, + ) + print(response) + except json.JSONDecodeError: + error_message = str(response.content) + print(f"JSON decoding error occurred. Response content: {error_message}.") + + +if __name__ == "__main__": + """Create ticket for specified robot.""" + parser = argparse.ArgumentParser(description="Pulls run logs from ABR robots.") + parser.add_argument( + "storage_directory", + metavar="STORAGE_DIRECTORY", + type=str, + nargs=1, + help="Path to long term storage directory for run logs.", + ) + parser.add_argument( + "robot_ip", + metavar="ROBOT_IP", + type=str, + nargs=1, + help="IP address of robot as string.", + ) + parser.add_argument( + "jira_api_token", + metavar="JIRA_API_TOKEN", + type=str, + nargs=1, + help="JIRA API Token. Get from https://id.atlassian.com/manage-profile/security.", + ) + parser.add_argument( + "email", + metavar="EMAIL", + type=str, + nargs=1, + help="Email connected to JIRA account.", + ) + # TODO: write function to get reporter_id from email. + parser.add_argument( + "reporter_id", + metavar="REPORTER_ID", + type=str, + nargs=1, + help="JIRA Reporter ID.", + ) + # TODO: improve help comment on jira board id. + parser.add_argument( + "board_id", + metavar="BOARD_ID", + type=str, + nargs=1, + help="JIRA Board ID. RABR is 217", + ) + args = parser.parse_args() + storage_directory = args.storage_directory[0] + ip = args.robot_ip[0] + url = "https://opentrons.atlassian.net" + api_token = args.jira_api_token[0] + email = args.email[0] + board_id = args.board_id[0] + reporter_id = args.reporter_id[0] + ticket = JiraTicket(url, api_token, email) + error_runs = get_error_runs_from_robot(ip) + one_run = error_runs[-1] # Most recent run with error. + ( + summary, + robot, + affects_version, + components, + whole_description_str, + saved_file_path, + ) = get_error_info_from_robot(ip, one_run, storage_directory) + print(f"Making ticket for run: {one_run} on robot {robot}.") + # TODO: make argument or see if I can get rid of with using board_id. + project_key = "RABR" + parent_key = project_key + "-" + robot[-1] + issue_url, issue_key = ticket.create_ticket( + summary, + whole_description_str, + project_key, + reporter_id, + "Bug", + "Medium", + components, + affects_version, + parent_key, + ) + ticket.open_issue(issue_key) + ticket.post_attachment_to_ticket(issue_key, saved_file_path) diff --git a/abr-testing/abr_testing/data_collection/abr_google_drive.py b/abr-testing/abr_testing/data_collection/abr_google_drive.py index be3fe162867..6dfc5e8f284 100644 --- a/abr-testing/abr_testing/data_collection/abr_google_drive.py +++ b/abr-testing/abr_testing/data_collection/abr_google_drive.py @@ -7,7 +7,7 @@ from datetime import datetime, timedelta from abr_testing.data_collection import read_robot_logs from typing import Set, Dict, Any -from abr_testing.google_automation import google_drive_tool, google_sheets_tool +from abr_testing.automation import google_drive_tool, google_sheets_tool def get_modules(file_results: Dict[str, str]) -> Dict[str, Any]: diff --git a/abr-testing/abr_testing/data_collection/error_levels.csv b/abr-testing/abr_testing/data_collection/error_levels.csv index c03cab56367..e9d93591967 100644 --- a/abr-testing/abr_testing/data_collection/error_levels.csv +++ b/abr-testing/abr_testing/data_collection/error_levels.csv @@ -11,7 +11,7 @@ Prefix,Error Code,Description,Categories,Level of Failure, 2,2000,Robotics Control Error,A Robot Action Failed,3, 2,2001,Motion Failed,A Robot Action Failed,4, 2,2002,Homing Failed,A Robot Action Failed,4, -2,2003,Stall or Collision Detected,A Robot Action Failed,3-4, +2,2003,Stall or Collision Detected,A Robot Action Failed,3, 2,2004,Motion Planning Failed,A Robot Action Failed,3, 2,2005,Position Estimation Invalid,A Robot Action Failed,3, 2,2006,Move Condition Not Met,A Robot Action Failed,3, @@ -22,15 +22,15 @@ Prefix,Error Code,Description,Categories,Level of Failure, 2,2011,Misaligned Gantry,A Robot Action Failed,3, 2,2012,Unmatched Tip Presence States,A Robot Action Failed,3-4, 2,2013,Position Unknown,A Robot Action Failed,4, -2,2014,Execution Cancelled,A Robot Action Failed,3-4, -2,2015,Failed Gripper Pickup Error,A Robot Action Failed,3-4, +2,2014,Execution Cancelled,A Robot Action Failed, 4, +2,2015,Failed Gripper Pickup Error,A Robot Action Failed,3, 3,3000,Robotics Interaction Error,A Robot Interaction Failed,3, -3,3001,Labware Dropped,A Robot Interaction Failed,3-4, -3,3002,Labware Not Picked Up,A Robot Interaction Failed,3-4, +3,3001,Labware Dropped,A Robot Interaction Failed, 4, +3,3002,Labware Not Picked Up,A Robot Interaction Failed,4, 3,3003,Tip Pickup Failed,A Robot Interaction Failed,4, 3,3004,Tip Drop Failed,A Robot Interaction Failed,4, 3,3005,Unexpeted Tip Removal,A Robot Interaction Failed,4, -3,3006,Pipette Overpressure,A Robot Interaction Failed,3-4, +3,3006,Pipette Overpressure,A Robot Interaction Failed,3, 3,3008,E-Stop Activated,A Robot Interaction Failed,Not an error, 3,3009,E-Stop Not Present,A Robot Interaction Failed,5, 3,3010,Pipette Not Present,A Robot Interaction Failed,5, diff --git a/abr-testing/abr_testing/data_collection/get_run_logs.py b/abr-testing/abr_testing/data_collection/get_run_logs.py index f80a4fb9f77..1511e3405e7 100644 --- a/abr-testing/abr_testing/data_collection/get_run_logs.py +++ b/abr-testing/abr_testing/data_collection/get_run_logs.py @@ -6,7 +6,7 @@ import requests import sys from abr_testing.data_collection import read_robot_logs -from abr_testing.google_automation import google_drive_tool +from abr_testing.automation import google_drive_tool def get_run_ids_from_robot(ip: str) -> Set[str]: @@ -80,9 +80,9 @@ def save_runs(runs_to_save: Set[str], ip: str, storage_directory: str) -> Set[st saved_file_paths = set() for a_run in runs_to_save: data = get_run_data(a_run, ip) - data_file_name = ip + "_" + data["run_id"] + ".json" - saved_file_path = os.path.join(storage_directory, data_file_name) - json.dump(data, open(saved_file_path, mode="w")) + saved_file_path = read_robot_logs.save_run_log_to_json( + ip, data, storage_directory + ) saved_file_paths.add(saved_file_path) print(f"Saved {len(runs_to_save)} run(s) from robot with IP address {ip}.") return saved_file_paths diff --git a/abr-testing/abr_testing/data_collection/read_robot_logs.py b/abr-testing/abr_testing/data_collection/read_robot_logs.py index d30842b33fd..abc8efb095e 100644 --- a/abr-testing/abr_testing/data_collection/read_robot_logs.py +++ b/abr-testing/abr_testing/data_collection/read_robot_logs.py @@ -138,6 +138,16 @@ def get_unseen_run_ids(runs: Set[str], runs_from_storage: Set[str]) -> Set[str]: return runs_to_save +def save_run_log_to_json( + ip: str, results: Dict[str, Any], storage_directory: str +) -> str: + """Save run log to local json file.""" + data_file_name = ip + "_" + results["run_id"] + ".json" + saved_file_path = os.path.join(storage_directory, data_file_name) + json.dump(results, open(saved_file_path, mode="w")) + return saved_file_path + + def get_run_ids_from_google_drive(google_drive: Any) -> Set[str]: """Get run ids in google drive folder.""" # Run ids in google_drive_folder diff --git a/abr-testing/abr_testing/tools/abr_asair_sensor.py b/abr-testing/abr_testing/tools/abr_asair_sensor.py index 4183b812930..eef69329436 100644 --- a/abr-testing/abr_testing/tools/abr_asair_sensor.py +++ b/abr-testing/abr_testing/tools/abr_asair_sensor.py @@ -6,7 +6,7 @@ import time as t from typing import List import argparse -from abr_testing.google_automation import google_sheets_tool +from abr_testing.automation import google_sheets_tool class _ABRAsairSensor: diff --git a/abr-testing/abr_testing/tools/abr_scale.py b/abr-testing/abr_testing/tools/abr_scale.py index 5d253d25c70..04ed34c3f8e 100644 --- a/abr-testing/abr_testing/tools/abr_scale.py +++ b/abr-testing/abr_testing/tools/abr_scale.py @@ -7,7 +7,7 @@ import argparse import csv from abr_testing.data_collection import read_robot_logs -from abr_testing.google_automation import google_sheets_tool +from abr_testing.automation import google_sheets_tool def write_to_sheets(file_name_csv: str, google_sheet: Any, row_list: List) -> None: From c864a993ca23242c944de1c15ccbc8415ef8bf22 Mon Sep 17 00:00:00 2001 From: Nick Diehl <47604184+ncdiehl11@users.noreply.github.com> Date: Mon, 1 Apr 2024 14:12:01 -0400 Subject: [PATCH 011/194] feat(app): mark protocol anlayses as stale if they lack RTP (#14763) closes [AUTH-247](https://opentrons.atlassian.net/browse/AUTH-247) --- .../__fixtures__/simpleAnalysisFile.json | 3 +- .../__tests__/writeFailedAnalysis.test.ts | 1 + .../protocol-analysis/writeFailedAnalysis.ts | 1 + .../__tests__/protocol-storage.test.ts | 1 + .../useStoredProtocolAnalysis.test.tsx | 2 ++ .../ProtocolsLanding/__tests__/hooks.test.tsx | 3 ++ .../ProtocolsLanding/__tests__/utils.test.ts | 36 ++++++++++++++++++- app/src/organisms/ProtocolsLanding/utils.ts | 5 ++- shared-data/protocol/types/schemaV8/index.ts | 2 ++ 9 files changed, 51 insertions(+), 3 deletions(-) diff --git a/api-client/src/protocols/__fixtures__/simpleAnalysisFile.json b/api-client/src/protocols/__fixtures__/simpleAnalysisFile.json index df8fcad1d98..e6f0a5bba3b 100644 --- a/api-client/src/protocols/__fixtures__/simpleAnalysisFile.json +++ b/api-client/src/protocols/__fixtures__/simpleAnalysisFile.json @@ -3936,5 +3936,6 @@ "description": "", "displayColor": "#b925ff" } - ] + ], + "runTimeParameters": [] } diff --git a/app-shell/src/protocol-analysis/__tests__/writeFailedAnalysis.test.ts b/app-shell/src/protocol-analysis/__tests__/writeFailedAnalysis.test.ts index 2c4d5a911ae..4514887cb6d 100644 --- a/app-shell/src/protocol-analysis/__tests__/writeFailedAnalysis.test.ts +++ b/app-shell/src/protocol-analysis/__tests__/writeFailedAnalysis.test.ts @@ -41,6 +41,7 @@ describe('write failed analysis', () => { modules: [], pipettes: [], liquids: [], + runTimeParameters: [], }) }) }) diff --git a/app-shell/src/protocol-analysis/writeFailedAnalysis.ts b/app-shell/src/protocol-analysis/writeFailedAnalysis.ts index 519184a3d41..8723cd52d04 100644 --- a/app-shell/src/protocol-analysis/writeFailedAnalysis.ts +++ b/app-shell/src/protocol-analysis/writeFailedAnalysis.ts @@ -27,6 +27,7 @@ export function createFailedAnalysis( pipettes: [], modules: [], liquids: [], + runTimeParameters: [], // TODO(mc, 2022-05-04): this field does not make sense for an // analysis that was unable to complete, but is required by // ProtocolAnalysisOutput diff --git a/app-shell/src/protocol-storage/__tests__/protocol-storage.test.ts b/app-shell/src/protocol-storage/__tests__/protocol-storage.test.ts index c873f47242c..3ac1a106dbe 100644 --- a/app-shell/src/protocol-storage/__tests__/protocol-storage.test.ts +++ b/app-shell/src/protocol-storage/__tests__/protocol-storage.test.ts @@ -119,6 +119,7 @@ describe('protocol storage directory utilities', () => { pipettes: [], modules: [], labware: [], + runTimeParameters: [], }) }) }) diff --git a/app/src/organisms/Devices/hooks/__tests__/useStoredProtocolAnalysis.test.tsx b/app/src/organisms/Devices/hooks/__tests__/useStoredProtocolAnalysis.test.tsx index 34365a075e7..fa63db104c6 100644 --- a/app/src/organisms/Devices/hooks/__tests__/useStoredProtocolAnalysis.test.tsx +++ b/app/src/organisms/Devices/hooks/__tests__/useStoredProtocolAnalysis.test.tsx @@ -42,6 +42,8 @@ const modifiedStoredProtocolData = { commands: storedProtocolData?.mostRecentAnalysis?.commands, liquids: storedProtocolData?.mostRecentAnalysis?.liquids, errors: storedProtocolData?.mostRecentAnalysis?.errors, + runTimeParameters: + storedProtocolData?.mostRecentAnalysis?.runTimeParameters, }, } diff --git a/app/src/organisms/ProtocolsLanding/__tests__/hooks.test.tsx b/app/src/organisms/ProtocolsLanding/__tests__/hooks.test.tsx index 49243c2b790..cfba2402172 100644 --- a/app/src/organisms/ProtocolsLanding/__tests__/hooks.test.tsx +++ b/app/src/organisms/ProtocolsLanding/__tests__/hooks.test.tsx @@ -97,6 +97,7 @@ const mockStoredProtocolData = [ displayColor: '#ff4f4f', }, ], + runTimeParameters: [], errors: [], } as ProtocolAnalysisOutput, }, @@ -183,6 +184,7 @@ const mockStoredProtocolData = [ displayColor: '#ff4f4f', }, ], + runTimeParameters: [], errors: [], } as ProtocolAnalysisOutput, }, @@ -273,6 +275,7 @@ const mockStoredProtocolData = [ displayColor: '#ff4f4f', }, ], + runTimeParameters: [], errors: [], } as ProtocolAnalysisOutput, }, diff --git a/app/src/organisms/ProtocolsLanding/__tests__/utils.test.ts b/app/src/organisms/ProtocolsLanding/__tests__/utils.test.ts index e4383c842b9..1ff0d74f72a 100644 --- a/app/src/organisms/ProtocolsLanding/__tests__/utils.test.ts +++ b/app/src/organisms/ProtocolsLanding/__tests__/utils.test.ts @@ -1,5 +1,9 @@ import { describe, it, expect } from 'vitest' -import { getisFlexProtocol, getRobotTypeDisplayName } from '../utils' +import { + getAnalysisStatus, + getisFlexProtocol, + getRobotTypeDisplayName, +} from '../utils' import type { ProtocolAnalysisOutput } from '@opentrons/shared-data' const mockOT3ProtocolAnalysisOutput = { @@ -10,6 +14,36 @@ const mockOT2ProtocolAnalysisOutput = { robotType: 'OT-2 Standard', } as ProtocolAnalysisOutput +describe('getAnalysisStatus', () => { + it('should return stale if no liquids in analysis', () => { + const result = getAnalysisStatus(false, { + ...mockOT3ProtocolAnalysisOutput, + liquids: [], + errors: [], + }) + expect(result).toBe('stale') + }) + + it('should return stale if no runTimeParameters in analysis', () => { + const result = getAnalysisStatus(false, { + ...mockOT3ProtocolAnalysisOutput, + runTimeParameters: [], + errors: [], + }) + expect(result).toBe('stale') + }) + + it('should return complete if liquids and runTimeParameters in analysis', () => { + const result = getAnalysisStatus(false, { + ...mockOT3ProtocolAnalysisOutput, + liquids: [], + runTimeParameters: [], + errors: [], + }) + expect(result).toBe('complete') + }) +}) + describe('getisFlexProtocol', () => { it('should return true for protocols intended for a Flex', () => { const result = getisFlexProtocol(mockOT3ProtocolAnalysisOutput) diff --git a/app/src/organisms/ProtocolsLanding/utils.ts b/app/src/organisms/ProtocolsLanding/utils.ts index 59ccfc2e852..dfc9b4fafc3 100644 --- a/app/src/organisms/ProtocolsLanding/utils.ts +++ b/app/src/organisms/ProtocolsLanding/utils.ts @@ -10,7 +10,10 @@ export function getAnalysisStatus( ): AnalysisStatus { if (isAnalyzing) { return 'loading' - } else if (analysis != null && analysis?.liquids == null) { + } else if ( + analysis != null && + (analysis.liquids == null || analysis.runTimeParameters == null) + ) { return 'stale' } else if (analysis != null) { return analysis.errors.length > 0 ? 'error' : 'complete' diff --git a/shared-data/protocol/types/schemaV8/index.ts b/shared-data/protocol/types/schemaV8/index.ts index 0a6972fe271..d501abbe38e 100644 --- a/shared-data/protocol/types/schemaV8/index.ts +++ b/shared-data/protocol/types/schemaV8/index.ts @@ -4,6 +4,7 @@ import type { LoadedLabware, LoadedModule, Liquid, + RunTimeParameter, } from '../../../js' import type { CommandAnnotation } from '../../../commandAnnotation/types' import type { LabwareDefinition2, RobotType } from '../../../js/types' @@ -136,6 +137,7 @@ export interface ProtocolAnalysisOutput { modules: LoadedModule[] liquids: Liquid[] errors: AnalysisError[] + runTimeParameters: RunTimeParameter[] robotType?: RobotType } From 34cdcb6ca2878ed582def26f2a3283be33905255 Mon Sep 17 00:00:00 2001 From: Nick Diehl <47604184+ncdiehl11@users.noreply.github.com> Date: Mon, 1 Apr 2024 15:24:54 -0400 Subject: [PATCH 012/194] feat(app, components): ProtocolRun RTPs (#14745) closes [AUTH-226](https://opentrons.atlassian.net/browse/AUTH-226) --- .../localization/en/protocol_setup.json | 1 + .../organisms/ChooseRobotSlideout/index.tsx | 4 +- .../index.tsx | 14 +- .../ProtocolRunRunTimeParameters.tsx | 284 ++++++------------ .../ProtocolRunRuntimeParameters.test.tsx | 25 +- .../__tests__/ProtocolParameters.test.tsx | 11 + .../ProtocolParameters/index.tsx | 2 +- app/src/organisms/ProtocolDetails/index.tsx | 1 - .../ParametersTable/NoParameters.tsx | 9 +- .../__tests__/NoParameters.test.tsx | 17 +- 10 files changed, 143 insertions(+), 225 deletions(-) diff --git a/app/src/assets/localization/en/protocol_setup.json b/app/src/assets/localization/en/protocol_setup.json index 084debdb5f0..371ce03a791 100644 --- a/app/src/assets/localization/en/protocol_setup.json +++ b/app/src/assets/localization/en/protocol_setup.json @@ -167,6 +167,7 @@ "no_modules_specified": "no modules are specified for this protocol.", "no_modules_used_in_this_protocol": "No hardware used in this protocol", "no_parameters_specified": "No parameters specified", + "no_parameters_specified_in_protocol": "No parameters specified in this protocol", "no_tiprack_loaded": "Protocol must load a tip rack", "no_tiprack_used": "Protocol must pick up a tip", "no_usb_connection_required": "No USB connection required", diff --git a/app/src/organisms/ChooseRobotSlideout/index.tsx b/app/src/organisms/ChooseRobotSlideout/index.tsx index ef5bb8c9368..1732adee134 100644 --- a/app/src/organisms/ChooseRobotSlideout/index.tsx +++ b/app/src/organisms/ChooseRobotSlideout/index.tsx @@ -112,7 +112,7 @@ interface ChooseRobotSlideoutProps isAnalysisError?: boolean isAnalysisStale?: boolean showIdleOnly?: boolean - multiSlideout?: { currentPage: number } + multiSlideout?: { currentPage: number } | null } export function ChooseRobotSlideout( @@ -135,7 +135,7 @@ export function ChooseRobotSlideout( setSelectedRobot, robotType, showIdleOnly = false, - multiSlideout, + multiSlideout = null, runTimeParametersOverrides, setRunTimeParametersOverrides, } = props diff --git a/app/src/organisms/ChooseRobotToRunProtocolSlideout/index.tsx b/app/src/organisms/ChooseRobotToRunProtocolSlideout/index.tsx index cc94ee94457..ff94a3ecec2 100644 --- a/app/src/organisms/ChooseRobotToRunProtocolSlideout/index.tsx +++ b/app/src/organisms/ChooseRobotToRunProtocolSlideout/index.tsx @@ -36,7 +36,6 @@ interface ChooseRobotToRunProtocolSlideoutProps extends StyleProps { storedProtocolData: StoredProtocolData onCloseClick: () => void showSlideout: boolean - runTimeParameters?: RunTimeParameter[] } export function ChooseRobotToRunProtocolSlideoutComponent( @@ -63,6 +62,8 @@ export function ChooseRobotToRunProtocolSlideoutComponent( ) // TODO: (nd: 3/20/24) remove stubs and pull parameters from analysis + // const runTimeParameters = + // storedProtocolData.mostRecentAnalysis?.runTimeParameters ?? [] const mockRunTimeParameters: RunTimeParameter[] = [ { displayName: 'Dry Run', @@ -230,18 +231,19 @@ export function ChooseRobotToRunProtocolSlideoutComponent( ) + const hasRunTimeParameters = + enableRunTimeParametersFF && runTimeParameters.length > 0 + return ( 0 && - currentPage === 2 + hasRunTimeParameters && currentPage === 2 ? t('select_parameters_for_robot', { robot_name: selectedRobot?.name, }) @@ -253,7 +255,7 @@ export function ChooseRobotToRunProtocolSlideoutComponent( setRunTimeParametersOverrides={setRunTimeParametersOverrides} footer={ - {enableRunTimeParametersFF && runTimeParameters.length > 0 ? ( + {hasRunTimeParameters ? ( currentPage === 1 ? ( <> 0 - // ToDo (kk:03/19/2024) this will be replaced with the boolean from values check result - const dummyBoolean = true + const hasCustomValues = runTimeParameters.some( + parameter => parameter.value !== parameter.default + ) - // ToDO (kk:03/18/2024) Need to add Chip to updated runTime parameter value - // This part will be implemented in a following PR since need to runTime parameter slideout return ( <> {hasParameter ? ( - {dummyBoolean ? t('custom_values') : t('default_values')} + {hasCustomValues ? t('custom_values') : t('default_values')} ) : null} @@ -221,55 +79,28 @@ export function ProtocolRunRuntimeParameters({ {!hasParameter ? ( - + ) : ( <> - + {t('name')} {t('value')} - + {runTimeParameters.map( - (parameter: RunTimeParameter, index: number) => { - return ( - - - - {parameter.displayName} - - - - - - {formatRunTimeParameterValue(parameter, t)} - - {/* ToDo (kk:03/19/2024) need to implement a logic when be is ready */} - {index % 2 === 0 ? ( - - ) : null} - - - - ) - } + (parameter: RunTimeParameter, index: number) => ( + + ) )} @@ -280,16 +111,77 @@ export function ProtocolRunRuntimeParameters({ ) } +interface StyledTableRowComponentProps { + parameter: RunTimeParameter + index: number + runTimeParametersLength: number + t: any +} + +const StyledTableRowComponent = ( + props: StyledTableRowComponentProps +): JSX.Element => { + const { parameter, index, runTimeParametersLength, t } = props + const [targetProps, tooltipProps] = useHoverTooltip() + return ( + + + + {parameter.displayName} + {parameter.description != null ? ( + <> + + + + + {parameter.description} + + + ) : null} + + + + + + {formatRunTimeParameterValue(parameter, t)} + + {parameter.value !== parameter.default ? ( + + ) : null} + + + + ) +} + const StyledTable = styled.table` width: 100%; border-collapse: collapse; text-align: left; ` +const StyledTableHeaderContainer = styled.thead` + display: grid; + grid-template-columns: 1fr 1fr; + grid-gap: 48px; + border-bottom: ${BORDERS.lineBorder}; +` const StyledTableHeader = styled.th` ${TYPOGRAPHY.labelSemiBold} padding: ${SPACING.spacing8}; - border-bottom: ${BORDERS.lineBorder}; ` interface StyledTableRowProps { @@ -297,8 +189,13 @@ interface StyledTableRowProps { } const StyledTableRow = styled.tr` - padding: ${SPACING.spacing8}; + display: grid; + grid-template-columns: 1fr 1fr; + grid-gap: 48px; + padding-top: ${SPACING.spacing8}; + padding-bottom: ${SPACING.spacing8}; border-bottom: ${props => (props.isLast ? 'none' : BORDERS.lineBorder)}; + align-items: ${ALIGN_CENTER}; ` interface StyledTableCellProps { @@ -307,6 +204,5 @@ interface StyledTableCellProps { const StyledTableCell = styled.td` padding-left: ${SPACING.spacing8}; - padding-top: ${SPACING.spacing12}; - padding-bottom: ${props => (props.isLast ? 0 : SPACING.spacing12)}; + height: 1.25rem; ` diff --git a/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunRuntimeParameters.test.tsx b/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunRuntimeParameters.test.tsx index 8844f551d08..ba8b39e64a2 100644 --- a/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunRuntimeParameters.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunRuntimeParameters.test.tsx @@ -106,7 +106,30 @@ describe('ProtocolRunRuntimeParameters', () => { vi.resetAllMocks() }) - it('should render title, and banner when RunTimeParameters are note empty', () => { + it('should render title, and banner when RunTimeParameters are note empty and all values are default', () => { + render(props) + screen.getByText('Parameters') + screen.getByText('Default values') + screen.getByText('Values are view-only') + screen.getByText('Cancel the run and restart setup to edit') + screen.getByText('Name') + screen.getByText('Value') + }) + + it('should render title, and banner when RunTimeParameters are note empty and some value is changed', () => { + vi.mocked(useMostRecentCompletedAnalysis).mockReturnValue({ + runTimeParameters: [ + ...mockRunTimeParameterData, + { + displayName: 'Dry Run', + variableName: 'DRYRUN', + description: 'Is this a dry or wet run? Wet is true, dry is false', + type: 'boolean', + default: false, + value: true, + }, + ], + } as CompletedProtocolAnalysis) render(props) screen.getByText('Parameters') screen.getByText('Custom values') diff --git a/app/src/organisms/ProtocolDetails/ProtocolParameters/__tests__/ProtocolParameters.test.tsx b/app/src/organisms/ProtocolDetails/ProtocolParameters/__tests__/ProtocolParameters.test.tsx index 707aa5256cf..727ca022890 100644 --- a/app/src/organisms/ProtocolDetails/ProtocolParameters/__tests__/ProtocolParameters.test.tsx +++ b/app/src/organisms/ProtocolDetails/ProtocolParameters/__tests__/ProtocolParameters.test.tsx @@ -7,6 +7,17 @@ import { i18n } from '../../../../i18n' import { ProtocolParameters } from '..' import type { RunTimeParameter } from '@opentrons/shared-data' +import type * as Components from '@opentrons/components' + +vi.mock('@opentrons/components', async importOriginal => { + const actual = await importOriginal() + return { + ...actual, + NoParameters: vi.fn(() => ( +
No parameters specified in this protocol
+ )), + } +}) const mockRunTimeParameter: RunTimeParameter[] = [ { diff --git a/app/src/organisms/ProtocolDetails/ProtocolParameters/index.tsx b/app/src/organisms/ProtocolDetails/ProtocolParameters/index.tsx index d7a64fd2396..69be8a3a468 100644 --- a/app/src/organisms/ProtocolDetails/ProtocolParameters/index.tsx +++ b/app/src/organisms/ProtocolDetails/ProtocolParameters/index.tsx @@ -48,7 +48,7 @@ export function ProtocolParameters({
) : ( - + )}
) diff --git a/app/src/organisms/ProtocolDetails/index.tsx b/app/src/organisms/ProtocolDetails/index.tsx index 9329b6329b3..02d897c3b4e 100644 --- a/app/src/organisms/ProtocolDetails/index.tsx +++ b/app/src/organisms/ProtocolDetails/index.tsx @@ -394,7 +394,6 @@ export function ProtocolDetails( onCloseClick={() => setShowChooseRobotToRunProtocolSlideout(false)} showSlideout={showChooseRobotToRunProtocolSlideout} storedProtocolData={props} - runTimeParameters={runTimeParameters} /> - {t != null - ? t('no_parameters') - : 'No parameters specified in this protocol'} + No parameters specified in this protocol
) diff --git a/components/src/molecules/ParametersTable/__tests__/NoParameters.test.tsx b/components/src/molecules/ParametersTable/__tests__/NoParameters.test.tsx index 5b2e7f2927d..660a6936d51 100644 --- a/components/src/molecules/ParametersTable/__tests__/NoParameters.test.tsx +++ b/components/src/molecules/ParametersTable/__tests__/NoParameters.test.tsx @@ -6,21 +6,19 @@ import { renderWithProviders } from '../../../testing/utils' import { BORDERS, COLORS } from '../../../helix-design-system' import { NoParameters } from '../NoParameters' -const render = (props: React.ComponentProps) => { - return renderWithProviders() +const render = () => { + return renderWithProviders() } -const tMock = (key: string) => key - describe('NoParameters', () => { it('should render text and icon with proper color', () => { - render({}) + render() screen.getByLabelText('alert') screen.getByText('No parameters specified in this protocol') }) it('should have proper styles', () => { - render({}) + render() expect(screen.getByTestId('NoRunTimeParameter')).toHaveStyle( `background-color: ${COLORS.grey30}` ) @@ -31,11 +29,4 @@ describe('NoParameters', () => { `color: ${COLORS.grey60}` ) }) - - it('should render the raw i18n value if a t is provided', () => { - render({ - t: tMock, - }) - screen.getByText('no_parameters') - }) }) From 62f6db9e6e333024e473bc20e349e9d5f07d95a6 Mon Sep 17 00:00:00 2001 From: Nick Diehl <47604184+ncdiehl11@users.noreply.github.com> Date: Mon, 1 Apr 2024 15:29:40 -0400 Subject: [PATCH 013/194] feat(app): range error handling for number RTPs (#14765) closes [AUTH-104](https://opentrons.atlassian.net/browse/AUTH-104) --- .../localization/en/protocol_details.json | 1 + .../__tests__/ChooseRobotSlideout.test.tsx | 53 +++++++++++++++---- .../organisms/ChooseRobotSlideout/index.tsx | 24 ++++++++- 3 files changed, 66 insertions(+), 12 deletions(-) diff --git a/app/src/assets/localization/en/protocol_details.json b/app/src/assets/localization/en/protocol_details.json index c5cc80f2804..d00d7e5f9f9 100644 --- a/app/src/assets/localization/en/protocol_details.json +++ b/app/src/assets/localization/en/protocol_details.json @@ -80,6 +80,7 @@ "unavailable_robot_not_listed_plural": "{{count}} unavailable robots are not listed.", "unavailable_robot_not_listed": "{{count}} unavailable robot is not listed.", "unsuccessfully_sent": "Unsuccessfully sent", + "value_out_of_range": "Value must be between {{min}}-{{max}}", "view_run_details": "View run details", "view_unavailable_robots": "View unavailable robots on the Devices page" } diff --git a/app/src/organisms/ChooseRobotSlideout/__tests__/ChooseRobotSlideout.test.tsx b/app/src/organisms/ChooseRobotSlideout/__tests__/ChooseRobotSlideout.test.tsx index 586bc6fe3b9..ffaaf0f11eb 100644 --- a/app/src/organisms/ChooseRobotSlideout/__tests__/ChooseRobotSlideout.test.tsx +++ b/app/src/organisms/ChooseRobotSlideout/__tests__/ChooseRobotSlideout.test.tsx @@ -22,7 +22,7 @@ import { useFeatureFlag } from '../../../redux/config' import { getNetworkInterfaces } from '../../../redux/networking' import { ChooseRobotSlideout } from '..' import { useNotifyService } from '../../../resources/useNotifyService' -import { RunTimeParameter } from '@opentrons/shared-data' +import { OT2_ROBOT_TYPE, RunTimeParameter } from '@opentrons/shared-data' vi.mock('../../../redux/discovery') vi.mock('../../../redux/robot-update') @@ -121,7 +121,7 @@ describe('ChooseRobotSlideout', () => { selectedRobot: null, setSelectedRobot: vi.fn(), title: 'choose robot slideout title', - robotType: 'OT-2 Standard', + robotType: OT2_ROBOT_TYPE, }) screen.getByText('choose robot slideout title') }) @@ -134,7 +134,7 @@ describe('ChooseRobotSlideout', () => { setSelectedRobot: vi.fn(), title: 'choose robot slideout title', isAnalysisError: true, - robotType: 'OT-2 Standard', + robotType: OT2_ROBOT_TYPE, }) screen.getByText( 'This protocol failed in-app analysis. It may be unusable on robots without custom software configurations.' @@ -148,7 +148,7 @@ describe('ChooseRobotSlideout', () => { selectedRobot: null, setSelectedRobot: vi.fn(), title: 'choose robot slideout title', - robotType: 'OT-2 Standard', + robotType: OT2_ROBOT_TYPE, }) screen.getByText('opentrons-robot-name') screen.getByText('2 unavailable robots are not listed.') @@ -162,7 +162,7 @@ describe('ChooseRobotSlideout', () => { selectedRobot: null, setSelectedRobot: vi.fn(), title: 'choose robot slideout title', - robotType: 'OT-2 Standard', + robotType: OT2_ROBOT_TYPE, }) screen.getByText('opentrons-robot-name') expect( @@ -177,7 +177,7 @@ describe('ChooseRobotSlideout', () => { selectedRobot: null, setSelectedRobot: mockSetSelectedRobot, title: 'choose robot slideout title', - robotType: 'OT-2 Standard', + robotType: OT2_ROBOT_TYPE, })[1] const refreshButton = screen.getByRole('button', { name: 'refresh' }) fireEvent.click(refreshButton) @@ -192,7 +192,7 @@ describe('ChooseRobotSlideout', () => { selectedRobot: null, setSelectedRobot: mockSetSelectedRobot, title: 'choose robot slideout title', - robotType: 'OT-2 Standard', + robotType: OT2_ROBOT_TYPE, multiSlideout: { currentPage: 1 }, }) screen.getByText('Step 1 / 2') @@ -205,7 +205,7 @@ describe('ChooseRobotSlideout', () => { selectedRobot: null, setSelectedRobot: mockSetSelectedRobot, title: 'choose robot slideout title', - robotType: 'OT-2 Standard', + robotType: OT2_ROBOT_TYPE, multiSlideout: { currentPage: 2 }, }) screen.getByText('Step 2 / 2') @@ -220,7 +220,7 @@ describe('ChooseRobotSlideout', () => { selectedRobot: null, setSelectedRobot: mockSetSelectedRobot, title: 'choose robot slideout title', - robotType: 'OT-2 Standard', + robotType: OT2_ROBOT_TYPE, multiSlideout: { currentPage: 2 }, runTimeParametersOverrides: [param], }) @@ -229,11 +229,42 @@ describe('ChooseRobotSlideout', () => { if (param.type === 'boolean' || 'choices' in param) { screen.getByText(param.description) } else { - screen.getByText(`${param.min}-${param.max}`) + if (param.type === 'int') { + screen.getByText(`${param.min}-${param.max}`) + } else { + screen.getByText(`${param.min.toFixed(1)}-${param.max.toFixed(1)}`) + } } }) }) + it('renders error message for runtime parameter out of range', () => { + render({ + onCloseClick: vi.fn(), + isExpanded: true, + isSelectedRobotOnDifferentSoftwareVersion: false, + selectedRobot: null, + setSelectedRobot: mockSetSelectedRobot, + title: 'choose robot slideout title', + robotType: OT2_ROBOT_TYPE, + multiSlideout: { currentPage: 2 }, + runTimeParametersOverrides: [ + { + value: 1000, + displayName: 'EtoH Volume', + variableName: 'ETOH_VOLUME', + description: '70% ethanol volume', + type: 'float', + suffix: 'mL', + min: 1.5, + max: 10.0, + default: 6.5, + }, + ], + }) + screen.getByText('Value must be between 1.5-10.0') + }) + it('defaults to first available robot and allows an available robot to be selected', () => { vi.mocked(getConnectableRobots).mockReturnValue([ { ...mockConnectableRobot, name: 'otherRobot', ip: 'otherIp' }, @@ -246,7 +277,7 @@ describe('ChooseRobotSlideout', () => { selectedRobot: null, setSelectedRobot: mockSetSelectedRobot, title: 'choose robot slideout title', - robotType: 'OT-2 Standard', + robotType: OT2_ROBOT_TYPE, }) expect(mockSetSelectedRobot).toBeCalledWith({ ...mockConnectableRobot, diff --git a/app/src/organisms/ChooseRobotSlideout/index.tsx b/app/src/organisms/ChooseRobotSlideout/index.tsx index 1732adee134..c6061d437e7 100644 --- a/app/src/organisms/ChooseRobotSlideout/index.tsx +++ b/app/src/organisms/ChooseRobotSlideout/index.tsx @@ -379,8 +379,30 @@ export function ChooseRobotSlideout( value={value} title={runtimeParam.displayName} tooltipText={runtimeParam.description} - caption={`${runtimeParam.min}-${runtimeParam.max}`} + caption={ + runtimeParam.type === 'int' + ? `${runtimeParam.min}-${runtimeParam.max}` + : `${runtimeParam.min.toFixed(1)}-${runtimeParam.max.toFixed( + 1 + )}` + } id={id} + error={ + Number.isNaN(value) || + value < runtimeParam.min || + value > runtimeParam.max + ? t(`value_out_of_range`, { + min: + runtimeParam.type === 'int' + ? runtimeParam.min + : runtimeParam.min.toFixed(1), + max: + runtimeParam.type === 'int' + ? runtimeParam.max + : runtimeParam.max.toFixed(1), + }) + : null + } onChange={e => { const clone = runTimeParametersOverrides.map((parameter, i) => { if (i === index) { From 18e4dfd15b1547174327c584ca6e9219b2856afc Mon Sep 17 00:00:00 2001 From: Jethary Rader <66035149+jerader@users.noreply.github.com> Date: Mon, 1 Apr 2024 16:40:47 -0400 Subject: [PATCH 014/194] feat(protocol-designer): tuberack form warnings & warnings now dismissible (#14764) closes AUTH-10 --- .../src/components/alerts/Alerts.tsx | 33 ++++-- protocol-designer/src/dismiss/actions.ts | 4 +- .../src/steplist/formLevel/index.ts | 12 +- .../steplist/formLevel/test/warnings.test.ts | 106 +++++++++++++++-- .../src/steplist/formLevel/warnings.tsx | 108 +++++++++++++++--- 5 files changed, 227 insertions(+), 36 deletions(-) diff --git a/protocol-designer/src/components/alerts/Alerts.tsx b/protocol-designer/src/components/alerts/Alerts.tsx index 6d5f191486a..1fc95e8162f 100644 --- a/protocol-designer/src/components/alerts/Alerts.tsx +++ b/protocol-designer/src/components/alerts/Alerts.tsx @@ -11,6 +11,7 @@ import { import { selectors as stepFormSelectors } from '../../step-forms' import { StepFieldName } from '../../steplist/fieldLevel' import { selectors as fileDataSelectors } from '../../file-data' +import { PRESAVED_STEP_ID } from '../../steplist' import { getVisibleFormWarnings, getVisibleFormErrors, @@ -105,16 +106,6 @@ const AlertsComponent = (props: Props): JSX.Element => { }) } - const dismissWarning = (dismissId: string): void => { - if (stepId) { - dispatch( - dismissActions.dismissTimelineWarning({ - type: dismissId, - stepId, - }) - ) - } - } const makeHandleCloseWarning = (dismissId?: string | null) => () => { console.assert( dismissId, @@ -153,6 +144,28 @@ const AlertsComponent = (props: Props): JSX.Element => { dismissId: warning.type, })) + const dismissWarning = (dismissId: string): void => { + const isTimelineWarning = Object.values(timelineWarnings).some( + warning => warning.dismissId === dismissId + ) + if (isTimelineWarning && stepId) { + dispatch( + dismissActions.dismissTimelineWarning({ + type: dismissId, + stepId, + }) + ) + } else { + dispatch( + dismissActions.dismissFormWarning({ + type: dismissId, + // if stepId does not exist, assume it is a presaved step + stepId: stepId ?? PRESAVED_STEP_ID, + }) + ) + } + } + return ( <> {componentType === 'Form' diff --git a/protocol-designer/src/dismiss/actions.ts b/protocol-designer/src/dismiss/actions.ts index 772d69f02a4..09f2c5a33c7 100644 --- a/protocol-designer/src/dismiss/actions.ts +++ b/protocol-designer/src/dismiss/actions.ts @@ -1,4 +1,5 @@ -import { StepIdType } from '../form-types' +import type { StepIdType } from '../form-types' + export interface DismissAction { type: ActionType payload: { @@ -6,6 +7,7 @@ export interface DismissAction { stepId: StepIdType } } + export type DismissFormWarning = DismissAction<'DISMISS_FORM_WARNING'> export type DismissTimelineWarning = DismissAction<'DISMISS_TIMELINE_WARNING'> export const dismissFormWarning = ( diff --git a/protocol-designer/src/steplist/formLevel/index.ts b/protocol-designer/src/steplist/formLevel/index.ts index 669e048ab4e..64c4fbff39b 100644 --- a/protocol-designer/src/steplist/formLevel/index.ts +++ b/protocol-designer/src/steplist/formLevel/index.ts @@ -29,6 +29,9 @@ import { minDisposalVolume, minAspirateAirGapVolume, minDispenseAirGapVolume, + aspirateTipPositionInTube, + dispenseTipPositionInTube, + mixTipPositionInTube, } from './warnings' import { HydratedFormdata, StepType } from '../../form-types' @@ -52,7 +55,10 @@ interface FormHelpers { const stepFormHelperMap: Partial> = { mix: { getErrors: composeErrors(incompatibleLabware, volumeTooHigh), - getWarnings: composeWarnings(belowPipetteMinimumVolume), + getWarnings: composeWarnings( + belowPipetteMinimumVolume, + mixTipPositionInTube + ), }, pause: { getErrors: composeErrors(pauseForTimeOrUntilTold), @@ -68,7 +74,9 @@ const stepFormHelperMap: Partial> = { maxDispenseWellVolume, minDisposalVolume, minAspirateAirGapVolume, - minDispenseAirGapVolume + minDispenseAirGapVolume, + aspirateTipPositionInTube, + dispenseTipPositionInTube ), }, magnet: { diff --git a/protocol-designer/src/steplist/formLevel/test/warnings.test.ts b/protocol-designer/src/steplist/formLevel/test/warnings.test.ts index d441007b206..16b1c5030f3 100644 --- a/protocol-designer/src/steplist/formLevel/test/warnings.test.ts +++ b/protocol-designer/src/steplist/formLevel/test/warnings.test.ts @@ -1,11 +1,16 @@ import { describe, it, beforeEach, expect } from 'vitest' -import { fixture_24_tuberack } from '@opentrons/shared-data/labware/fixtures/2' +import { fixture24Tuberack, fixture96Plate } from '@opentrons/shared-data' import { _minAirGapVolume, belowPipetteMinimumVolume, minDisposalVolume, maxDispenseWellVolume, + aspirateTipPositionInTube, + dispenseTipPositionInTube, + mixTipPositionInTube, } from '../warnings' +import type { LabwareEntity } from '@opentrons/step-generation' +import type { LabwareDefinition2 } from '@opentrons/shared-data' type CheckboxFields = 'aspirate_airGap_checkbox' | 'dispense_airGap_checkbox' type VolumeFields = 'aspirate_airGap_volume' | 'dispense_airGap_volume' @@ -16,11 +21,15 @@ describe('Min air gap volume', () => { const volumeField = `${aspDisp}_airGap_volume` as VolumeFields describe(`${aspOrDisp} -> air gap`, () => { - let pipette: { spec: { minVolume: number } } + let pipette: { spec: { liquids: { default: { minVolume: number } } } } beforeEach(() => { pipette = { spec: { - minVolume: 100, + liquids: { + default: { + minVolume: 100, + }, + }, }, } }) @@ -82,12 +91,18 @@ describe('Min air gap volume', () => { }) }) describe('Below pipette minimum volume', () => { - let fieldsWithPipette: { pipette: { spec: { minVolume: number } } } + let fieldsWithPipette: { + pipette: { spec: { liquids: { default: { minVolume: number } } } } + } beforeEach(() => { fieldsWithPipette = { pipette: { spec: { - minVolume: 100, + liquids: { + default: { + minVolume: 100, + }, + }, }, }, } @@ -119,7 +134,7 @@ describe('Below pipette minimum volume', () => { }) describe('Below min disposal volume', () => { let fieldsWithPipette: { - pipette: { spec: { minVolume: number } } + pipette: { spec: { liquids: { default: { minVolume: number } } } } disposalVolume_checkbox: boolean disposalVolume_volume: number path: string @@ -128,7 +143,11 @@ describe('Below min disposal volume', () => { fieldsWithPipette = { pipette: { spec: { - minVolume: 100, + liquids: { + default: { + minVolume: 100, + }, + }, }, }, disposalVolume_checkbox: true, @@ -201,7 +220,7 @@ describe('Max dispense well volume', () => { let fieldsWithDispenseLabware: any beforeEach(() => { fieldsWithDispenseLabware = { - dispense_labware: { def: { ...fixture_24_tuberack } }, + dispense_labware: { def: fixture24Tuberack }, dispense_wells: ['A1', 'A2'], } }) @@ -244,4 +263,75 @@ describe('Max dispense well volume', () => { // @ts-expect-error(sa, 2021-6-15): maxDispenseWellVolume might return null, need to null check before property access expect(maxDispenseWellVolume(fields).type).toBe('OVER_MAX_WELL_VOLUME') }) + describe('tip position in tube warnings', () => { + let fields: { + aspirate_labware: LabwareEntity + aspirate_mmFromBottom: number | null + labware: LabwareEntity + mix_mmFromBottom: number + dispense_labware: LabwareEntity + dispense_mmFromBottom: number | null + } + beforeEach(() => { + fields = { + aspirate_labware: { + def: fixture24Tuberack as LabwareDefinition2, + id: 'mockId', + labwareDefURI: 'mockURI', + }, + aspirate_mmFromBottom: null, + labware: { + def: fixture24Tuberack as LabwareDefinition2, + id: 'mockId', + labwareDefURI: 'mockURI', + }, + mix_mmFromBottom: 0.5, + dispense_labware: { + def: fixture24Tuberack as LabwareDefinition2, + id: 'mockId', + labwareDefURI: 'mockURI', + }, + dispense_mmFromBottom: null, + } + }) + it('renders the errors for all 3', () => { + expect(aspirateTipPositionInTube(fields)?.type).toBe( + 'ASPIRATE_TIP_POSITIONED_LOW_IN_TUBE' + ) + expect(dispenseTipPositionInTube(fields)?.type).toBe( + 'DISPENSE_TIP_POSITIONED_LOW_IN_TUBE' + ) + expect(mixTipPositionInTube(fields)?.type).toBe( + 'MIX_TIP_POSITIONED_LOW_IN_TUBE' + ) + }) + it('renders null for all 3 when the number has been adjusted', () => { + fields.aspirate_mmFromBottom = 3 + fields.dispense_mmFromBottom = 3 + fields.mix_mmFromBottom = 3 + expect(aspirateTipPositionInTube(fields)).toBe(null) + expect(dispenseTipPositionInTube(fields)).toBe(null) + expect(mixTipPositionInTube(fields)).toBe(null) + }) + it('renders null for all 3 when the labware is not a tube rack', () => { + fields.aspirate_labware = { + def: fixture96Plate as LabwareDefinition2, + id: 'mockId', + labwareDefURI: 'mockURI', + } + fields.labware = { + def: fixture96Plate as LabwareDefinition2, + id: 'mockId', + labwareDefURI: 'mockURI', + } + fields.dispense_labware = { + def: fixture96Plate as LabwareDefinition2, + id: 'mockId', + labwareDefURI: 'mockURI', + } + expect(aspirateTipPositionInTube(fields)).toBe(null) + expect(dispenseTipPositionInTube(fields)).toBe(null) + expect(mixTipPositionInTube(fields)).toBe(null) + }) + }) }) diff --git a/protocol-designer/src/steplist/formLevel/warnings.tsx b/protocol-designer/src/steplist/formLevel/warnings.tsx index 1b6fa0ab071..6a9c31a1a72 100644 --- a/protocol-designer/src/steplist/formLevel/warnings.tsx +++ b/protocol-designer/src/steplist/formLevel/warnings.tsx @@ -1,16 +1,19 @@ import * as React from 'react' import { getWellTotalVolume } from '@opentrons/shared-data' import { KnowledgeBaseLink } from '../../components/KnowledgeBaseLink' -import { FormError } from './errors' +import type { FormError } from './errors' /******************* ** Warning Messages ** ********************/ export type FormWarningType = + | 'ASPIRATE_TIP_POSITIONED_LOW_IN_TUBE' + | 'BELOW_MIN_AIR_GAP_VOLUME' + | 'BELOW_MIN_DISPOSAL_VOLUME' | 'BELOW_PIPETTE_MINIMUM_VOLUME' + | 'DISPENSE_TIP_POSITIONED_LOW_IN_TUBE' | 'OVER_MAX_WELL_VOLUME' - | 'BELOW_MIN_DISPOSAL_VOLUME' - | 'BELOW_MIN_AIR_GAP_VOLUME' + | 'MIX_TIP_POSITIONED_LOW_IN_TUBE' export type FormWarning = FormError & { type: FormWarningType @@ -56,6 +59,27 @@ const belowMinDisposalVolumeWarning = (min: number): FormWarning => ({ dependentFields: ['disposalVolume_volume', 'pipette'], }) +const aspirateTipPositionedLowInTube = (): FormWarning => ({ + type: 'ASPIRATE_TIP_POSITIONED_LOW_IN_TUBE', + title: + 'The default aspirate height is 1mm from the bottom of the well, which could cause liquid overflow or pipette damage. Edit tip position in advanced settings.', + dependentFields: ['aspirate_labware'], +}) + +const dispenseTipPositionedLowInTube = (): FormWarning => ({ + type: 'DISPENSE_TIP_POSITIONED_LOW_IN_TUBE', + title: + 'The default dispense height is 0.5mm from the bottom of the well, which could cause liquid overflow or pipette damage. Edit tip position in advanced settings.', + dependentFields: ['dispense_labware'], +}) + +const mixTipPositionedLowInTube = (): FormWarning => ({ + type: 'MIX_TIP_POSITIONED_LOW_IN_TUBE', + title: + 'The default mix height is 0.5mm from the bottom of the well, which could cause liquid overflow or pipette damage. Edit tip position in advanced settings.', + dependentFields: ['labware'], +}) + export type WarningChecker = (val: unknown) => FormWarning | null /******************* @@ -64,14 +88,57 @@ export type WarningChecker = (val: unknown) => FormWarning | null // TODO: real HydratedFormData type export type HydratedFormData = any +export const aspirateTipPositionInTube = ( + fields: HydratedFormData +): FormWarning | null => { + const { aspirate_labware, aspirate_mmFromBottom } = fields + let isTubeRack: boolean = false + if (aspirate_labware != null) { + isTubeRack = aspirate_labware.def.metadata.displayCategory === 'tubeRack' + } + return isTubeRack && aspirate_mmFromBottom === null + ? aspirateTipPositionedLowInTube() + : null +} +export const dispenseTipPositionInTube = ( + fields: HydratedFormData +): FormWarning | null => { + const { dispense_labware, dispense_mmFromBottom } = fields + let isTubeRack: boolean = false + if (dispense_labware != null) { + isTubeRack = + // checking that the dispense labware is a labware and not a trash/waste chute + 'def' in dispense_labware + ? dispense_labware.def.metadata.displayCategory === 'tubeRack' + : false + } + return isTubeRack && dispense_mmFromBottom === null + ? dispenseTipPositionedLowInTube() + : null +} +export const mixTipPositionInTube = ( + fields: HydratedFormData +): FormWarning | null => { + const { labware, mix_mmFromBottom } = fields + let isTubeRack: boolean = false + if (labware != null) { + isTubeRack = labware.def.metadata.displayCategory === 'tubeRack' + } + return isTubeRack && mix_mmFromBottom === 0.5 + ? mixTipPositionedLowInTube() + : null +} export const belowPipetteMinimumVolume = ( fields: HydratedFormData ): FormWarning | null => { const { pipette, volume } = fields if (!(pipette && pipette.spec)) return null - return volume < pipette.spec.minVolume - ? belowPipetteMinVolumeWarning(pipette.spec.minVolume) - : null + const liquidSpecs = pipette.spec.liquids + const minVolume = + 'lowVolumeDefault' in liquidSpecs + ? liquidSpecs.lowVolumeDefault.minVolume + : liquidSpecs.default.minVolume + return volume < minVolume ? belowPipetteMinVolumeWarning(minVolume) : null } export const maxDispenseWellVolume = ( @@ -102,11 +169,16 @@ export const minDisposalVolume = ( } = fields if (!(pipette && pipette.spec) || path !== 'multiDispense') return null const isUnselected = !disposalVolume_checkbox || !disposalVolume_volume - if (isUnselected) return belowMinDisposalVolumeWarning(pipette.spec.minVolume) - const isBelowMin = disposalVolume_volume < pipette.spec.minVolume - return isBelowMin - ? belowMinDisposalVolumeWarning(pipette.spec.minVolume) - : null + const liquidSpecs = pipette.spec.liquids + const minVolume = + 'lowVolumeDefault' in liquidSpecs + ? liquidSpecs.lowVolumeDefault.minVolume + : liquidSpecs.default.minVolume + if (isUnselected) { + return belowMinDisposalVolumeWarning(minVolume) + } + const isBelowMin = disposalVolume_volume < minVolume + return isBelowMin ? belowMinDisposalVolumeWarning(minVolume) : null } // both aspirate and dispense air gap volumes have the same minimums @@ -117,10 +189,16 @@ export const _minAirGapVolume = ( const checkboxValue = fields[checkboxField] const volumeValue = fields[volumeField] const { pipette } = fields - if (!checkboxValue || !volumeValue || !pipette || !pipette.spec) return null - - const isBelowMin = Number(volumeValue) < pipette.spec.minVolume - return isBelowMin ? belowMinAirGapVolumeWarning(pipette.spec.minVolume) : null + if (!checkboxValue || !volumeValue || !pipette || !pipette.spec) { + return null + } + const liquidSpecs = pipette.spec.liquids + const minVolume = + 'lowVolumeDefault' in liquidSpecs + ? liquidSpecs.lowVolumeDefault.minVolume + : liquidSpecs.default.minVolume + const isBelowMin = Number(volumeValue) < minVolume + return isBelowMin ? belowMinAirGapVolumeWarning(minVolume) : null } export const minAspirateAirGapVolume: ( From 2ec93cd4c72eb006dc01f855b461f702bedb920b Mon Sep 17 00:00:00 2001 From: Max Marrone Date: Tue, 2 Apr 2024 09:15:04 -0400 Subject: [PATCH 015/194] fix(robot-server): Update status bar to account for `awaiting-recovery` run state (#14773) --- .../robot_server/runs/light_control_task.py | 31 +++++++++---------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/robot-server/robot_server/runs/light_control_task.py b/robot-server/robot_server/runs/light_control_task.py index ee84981359a..1cb2ad71616 100644 --- a/robot-server/robot_server/runs/light_control_task.py +++ b/robot-server/robot_server/runs/light_control_task.py @@ -32,22 +32,21 @@ def _engine_status_to_status_bar( initialization_done: bool, ) -> StatusBarState: """Convert an engine status into a status bar status.""" - if status is None: - return StatusBarState.IDLE if initialization_done else StatusBarState.OFF - - return { - EngineStatus.IDLE: StatusBarState.IDLE - if initialization_done - else StatusBarState.OFF, - EngineStatus.RUNNING: StatusBarState.RUNNING, - EngineStatus.PAUSED: StatusBarState.PAUSED, - EngineStatus.BLOCKED_BY_OPEN_DOOR: StatusBarState.PAUSED, - EngineStatus.STOP_REQUESTED: StatusBarState.UPDATING, - EngineStatus.STOPPED: StatusBarState.IDLE, - EngineStatus.FINISHING: StatusBarState.UPDATING, - EngineStatus.FAILED: StatusBarState.HARDWARE_ERROR, - EngineStatus.SUCCEEDED: StatusBarState.RUN_COMPLETED, - }[status] + match status: + case None | EngineStatus.IDLE: + return StatusBarState.IDLE if initialization_done else StatusBarState.OFF + case EngineStatus.RUNNING: + return StatusBarState.RUNNING + case EngineStatus.PAUSED | EngineStatus.AWAITING_RECOVERY | EngineStatus.BLOCKED_BY_OPEN_DOOR: + return StatusBarState.PAUSED + case EngineStatus.STOP_REQUESTED | EngineStatus.FINISHING: + return StatusBarState.UPDATING + case EngineStatus.STOPPED: + return StatusBarState.IDLE + case EngineStatus.FAILED: + return StatusBarState.HARDWARE_ERROR + case EngineStatus.SUCCEEDED: + return StatusBarState.RUN_COMPLETED def _active_updates_to_status_bar( From cf93d9c9adbfc18cfb602999921bdf8b3ab45a24 Mon Sep 17 00:00:00 2001 From: Jamey Huffnagle Date: Tue, 2 Apr 2024 09:53:39 -0400 Subject: [PATCH 016/194] refactor(robot-server, api): Wire up protocol engine event bubbling to robot server (#14766) Closes EXEC-358 Wire up PE event bubbling to the robot server for notifications as an alternative to the current polling that occurs. There are no functional changes. PublisherNotifier is the new interface that handles event management for publishers, using a generic ChangeNotifier that is given to PE as a callback. When PE reports a change in state, the callback fires. PublisherNotifier then iterates through each callback, invoking them. In the future, each publisher that requires access to PE state updates (eg, RunsPublisher) will add relevant callbacks during their initialization via register_publish_callbacks. Each callback will contain the conditional logic required for an MQTT publish to occur. --- .../protocol_engine/create_protocol_engine.py | 3 + .../opentrons/protocol_engine/state/state.py | 5 ++ robot-server/robot_server/app_setup.py | 4 +- .../maintenance_engine_store.py | 5 +- .../maintenance_run_data_manager.py | 5 +- .../maintenance_runs/router/base_router.py | 6 +- .../robot_server/runs/engine_store.py | 5 +- .../robot_server/runs/router/base_router.py | 7 +- .../robot_server/runs/run_data_manager.py | 5 +- .../service/notifications/__init__.py | 12 ++- .../service/notifications/change_notifier.py | 23 ++++++ .../notifications/initialize_notifications.py | 11 +++ .../notifications/notification_client.py | 5 +- .../notifications/publisher_notifier.py | 81 +++++++++++++++++++ .../notifications/publishers/__init__.py | 5 ++ .../service/notifications/topics.py | 1 + .../router/test_base_router.py | 7 ++ .../maintenance_runs/test_engine_store.py | 31 +++++-- .../maintenance_runs/test_run_data_manager.py | 11 +++ .../tests/runs/router/test_base_router.py | 11 +++ robot-server/tests/runs/test_engine_store.py | 71 +++++++++++++--- .../tests/runs/test_run_data_manager.py | 13 +++ .../tests/service/notifications/__init__.py | 0 .../notifications/test_change_notifier.py | 56 +++++++++++++ .../notifications/test_publisher_notifier.py | 74 +++++++++++++++++ 25 files changed, 427 insertions(+), 30 deletions(-) create mode 100644 robot-server/robot_server/service/notifications/change_notifier.py create mode 100644 robot-server/robot_server/service/notifications/initialize_notifications.py create mode 100644 robot-server/robot_server/service/notifications/publisher_notifier.py create mode 100644 robot-server/tests/service/notifications/__init__.py create mode 100644 robot-server/tests/service/notifications/test_change_notifier.py create mode 100644 robot-server/tests/service/notifications/test_publisher_notifier.py diff --git a/api/src/opentrons/protocol_engine/create_protocol_engine.py b/api/src/opentrons/protocol_engine/create_protocol_engine.py index 39268f28bc7..ab91b5fabaa 100644 --- a/api/src/opentrons/protocol_engine/create_protocol_engine.py +++ b/api/src/opentrons/protocol_engine/create_protocol_engine.py @@ -20,6 +20,7 @@ async def create_protocol_engine( config: Config, load_fixed_trash: bool = False, deck_configuration: typing.Optional[DeckConfigurationType] = None, + notify_publishers: typing.Optional[typing.Callable[[], None]] = None, ) -> ProtocolEngine: """Create a ProtocolEngine instance. @@ -28,6 +29,7 @@ async def create_protocol_engine( config: ProtocolEngine configuration. load_fixed_trash: Automatically load fixed trash labware in engine. deck_configuration: The initial deck configuration the engine will be instantiated with. + notify_publishers: Notifies robot server publishers of internal state change. """ deck_data = DeckDataProvider(config.deck_type) deck_definition = await deck_data.get_deck_definition() @@ -45,6 +47,7 @@ async def create_protocol_engine( is_door_open=hardware_api.door_state is DoorState.OPEN, module_calibration_offsets=module_calibration_offsets, deck_configuration=deck_configuration, + notify_publishers=notify_publishers, ) return ProtocolEngine(state_store=state_store, hardware_api=hardware_api) diff --git a/api/src/opentrons/protocol_engine/state/state.py b/api/src/opentrons/protocol_engine/state/state.py index a34f016deab..a472b574e6f 100644 --- a/api/src/opentrons/protocol_engine/state/state.py +++ b/api/src/opentrons/protocol_engine/state/state.py @@ -146,6 +146,7 @@ def __init__( change_notifier: Optional[ChangeNotifier] = None, module_calibration_offsets: Optional[Dict[str, ModuleOffsetData]] = None, deck_configuration: Optional[DeckConfigurationType] = None, + notify_publishers: Optional[Callable[[], None]] = None, ) -> None: """Initialize a StateStore and its substores. @@ -159,6 +160,7 @@ def __init__( change_notifier: Internal state change notifier. module_calibration_offsets: Module offsets to preload. deck_configuration: The initial deck configuration the addressable area store will be instantiated with. + notify_publishers: Notifies robot server publishers of internal state change. """ self._command_store = CommandStore(config=config, is_door_open=is_door_open) self._pipette_store = PipetteStore() @@ -191,6 +193,7 @@ def __init__( ] self._config = config self._change_notifier = change_notifier or ChangeNotifier() + self._notify_robot_server = notify_publishers self._initialize_state() def handle_action(self, action: Action) -> None: @@ -319,3 +322,5 @@ def _update_state_views(self) -> None: self._liquid._state = next_state.liquids self._tips._state = next_state.tips self._change_notifier.notify() + if self._notify_robot_server is not None: + self._notify_robot_server() diff --git a/robot-server/robot_server/app_setup.py b/robot-server/robot_server/app_setup.py index 80fda961119..04147753906 100644 --- a/robot-server/robot_server/app_setup.py +++ b/robot-server/robot_server/app_setup.py @@ -36,7 +36,7 @@ ) from .service.notifications import ( - initialize_notification_client, + initialize_notifications, clean_up_notification_client, ) @@ -106,7 +106,7 @@ async def on_startup() -> None: fbl_mark_persistence_init_complete ], ) - initialize_notification_client( + await initialize_notifications( app_state=app.state, ) diff --git a/robot-server/robot_server/maintenance_runs/maintenance_engine_store.py b/robot-server/robot_server/maintenance_runs/maintenance_engine_store.py index 8e42cbf2cae..3b60f38f533 100644 --- a/robot-server/robot_server/maintenance_runs/maintenance_engine_store.py +++ b/robot-server/robot_server/maintenance_runs/maintenance_engine_store.py @@ -1,6 +1,6 @@ """In-memory storage of ProtocolEngine instances.""" from datetime import datetime -from typing import List, NamedTuple, Optional +from typing import List, NamedTuple, Optional, Callable from opentrons.protocol_engine.types import PostRunHardwareState from opentrons_shared_data.robot.dev_types import RobotType @@ -127,6 +127,7 @@ async def create( run_id: str, created_at: datetime, labware_offsets: List[LabwareOffsetCreate], + notify_publishers: Callable[[], None], deck_configuration: Optional[DeckConfigurationType] = [], ) -> StateSummary: """Create and store a ProtocolRunner and ProtocolEngine for a given Run. @@ -135,6 +136,7 @@ async def create( run_id: The run resource the engine is assigned to. created_at: Run creation datetime labware_offsets: Labware offsets to create the engine with. + notify_publishers: Utilized by the engine to notify publishers of state changes. Returns: The initial equipment and status summary of the engine. @@ -154,6 +156,7 @@ async def create( ), ), deck_configuration=deck_configuration, + notify_publishers=notify_publishers, ) # Using LiveRunner as the runner to allow for future refactor of maintenance runs diff --git a/robot-server/robot_server/maintenance_runs/maintenance_run_data_manager.py b/robot-server/robot_server/maintenance_runs/maintenance_run_data_manager.py index 9857c50a200..084a7552a3a 100644 --- a/robot-server/robot_server/maintenance_runs/maintenance_run_data_manager.py +++ b/robot-server/robot_server/maintenance_runs/maintenance_run_data_manager.py @@ -1,6 +1,6 @@ """Manage current maintenance run data.""" from datetime import datetime -from typing import List, Optional +from typing import List, Optional, Callable from opentrons.protocol_engine import ( EngineStatus, @@ -83,6 +83,7 @@ async def create( created_at: datetime, labware_offsets: List[LabwareOffsetCreate], deck_configuration: DeckConfigurationType, + notify_publishers: Callable[[], None], ) -> MaintenanceRun: """Create a new, current maintenance run. @@ -90,6 +91,7 @@ async def create( run_id: Identifier to assign the new run. created_at: Creation datetime. labware_offsets: Labware offsets to initialize the engine with. + notify_publishers: Utilized by the engine to notify publishers of state changes. Returns: The run resource. @@ -102,6 +104,7 @@ async def create( created_at=created_at, labware_offsets=labware_offsets, deck_configuration=deck_configuration, + notify_publishers=notify_publishers, ) maintenance_run_data = _build_run( diff --git a/robot-server/robot_server/maintenance_runs/router/base_router.py b/robot-server/robot_server/maintenance_runs/router/base_router.py index d2eb71a5798..c115d46509f 100644 --- a/robot-server/robot_server/maintenance_runs/router/base_router.py +++ b/robot-server/robot_server/maintenance_runs/router/base_router.py @@ -5,7 +5,7 @@ import logging from datetime import datetime from textwrap import dedent -from typing import Optional +from typing import Optional, Callable from typing_extensions import Literal from fastapi import APIRouter, Depends, status @@ -39,6 +39,7 @@ get_deck_configuration_store, ) from robot_server.deck_configuration.store import DeckConfigurationStore +from robot_server.service.notifications import get_notify_publishers log = logging.getLogger(__name__) base_router = APIRouter() @@ -155,6 +156,7 @@ async def create_run( deck_configuration_store: DeckConfigurationStore = Depends( get_deck_configuration_store ), + notify_publishers: Callable[[], None] = Depends(get_notify_publishers), ) -> PydanticResponse[SimpleBody[MaintenanceRun]]: """Create a new maintenance run. @@ -166,6 +168,7 @@ async def create_run( is_ok_to_create_maintenance_run: Verify if a maintenance run may be created if a protocol run exists. check_estop: Dependency to verify the estop is in a valid state. deck_configuration_store: Dependency to fetch the deck configuration. + notify_publishers: Utilized by the engine to notify publishers of state changes. """ if not is_ok_to_create_maintenance_run: raise ProtocolRunIsActive( @@ -180,6 +183,7 @@ async def create_run( created_at=created_at, labware_offsets=offsets, deck_configuration=deck_configuration, + notify_publishers=notify_publishers, ) log.info(f'Created an empty run "{run_id}"".') diff --git a/robot-server/robot_server/runs/engine_store.py b/robot-server/robot_server/runs/engine_store.py index aa5b26d4a77..673ff5549f3 100644 --- a/robot-server/robot_server/runs/engine_store.py +++ b/robot-server/robot_server/runs/engine_store.py @@ -1,5 +1,5 @@ """In-memory storage of ProtocolEngine instances.""" -from typing import List, NamedTuple, Optional +from typing import List, NamedTuple, Optional, Callable from opentrons.protocol_engine.types import PostRunHardwareState from opentrons_shared_data.robot.dev_types import RobotType @@ -152,6 +152,7 @@ async def create( run_id: str, labware_offsets: List[LabwareOffsetCreate], deck_configuration: DeckConfigurationType, + notify_publishers: Callable[[], None], protocol: Optional[ProtocolResource], ) -> StateSummary: """Create and store a ProtocolRunner and ProtocolEngine for a given Run. @@ -160,6 +161,7 @@ async def create( run_id: The run resource the engine is assigned to. labware_offsets: Labware offsets to create the engine with. protocol: The protocol to load the runner with, if any. + notify_publishers: Utilized by the engine to notify publishers of state changes. Returns: The initial equipment and status summary of the engine. @@ -184,6 +186,7 @@ async def create( ), load_fixed_trash=load_fixed_trash, deck_configuration=deck_configuration, + notify_publishers=notify_publishers, ) post_run_hardware_state = PostRunHardwareState.HOME_AND_STAY_ENGAGED diff --git a/robot-server/robot_server/runs/router/base_router.py b/robot-server/robot_server/runs/router/base_router.py index fc7b3f223e3..e1e62fdf0d4 100644 --- a/robot-server/robot_server/runs/router/base_router.py +++ b/robot-server/robot_server/runs/router/base_router.py @@ -5,7 +5,7 @@ import logging from datetime import datetime from textwrap import dedent -from typing import Optional, Union +from typing import Optional, Union, Callable from typing_extensions import Literal from fastapi import APIRouter, Depends, status, Query @@ -45,7 +45,7 @@ get_deck_configuration_store, ) from robot_server.deck_configuration.store import DeckConfigurationStore - +from robot_server.service.notifications import get_notify_publishers log = logging.getLogger(__name__) base_router = APIRouter() @@ -144,6 +144,7 @@ async def create_run( deck_configuration_store: DeckConfigurationStore = Depends( get_deck_configuration_store ), + notify_publishers: Callable[[], None] = Depends(get_notify_publishers), ) -> PydanticResponse[SimpleBody[Union[Run, BadRun]]]: """Create a new run. @@ -157,6 +158,7 @@ async def create_run( the new run. check_estop: Dependency to verify the estop is in a valid state. deck_configuration_store: Dependency to fetch the deck configuration. + notify_publishers: Utilized by the engine to notify publishers of state changes. """ protocol_id = request_body.data.protocolId if request_body is not None else None offsets = request_body.data.labwareOffsets if request_body is not None else [] @@ -184,6 +186,7 @@ async def create_run( labware_offsets=offsets, deck_configuration=deck_configuration, protocol=protocol_resource, + notify_publishers=notify_publishers, ) except EngineConflictError as e: raise RunAlreadyActive(detail=str(e)).as_error(status.HTTP_409_CONFLICT) from e diff --git a/robot-server/robot_server/runs/run_data_manager.py b/robot-server/robot_server/runs/run_data_manager.py index 92c7d5e12b5..f0fc28dca37 100644 --- a/robot-server/robot_server/runs/run_data_manager.py +++ b/robot-server/robot_server/runs/run_data_manager.py @@ -1,6 +1,6 @@ """Manage current and historical run data.""" from datetime import datetime -from typing import List, Optional, Union +from typing import List, Optional, Callable, Union from opentrons_shared_data.labware.labware_definition import LabwareDefinition from opentrons_shared_data.errors.exceptions import InvalidStoredData, EnumeratedError @@ -142,6 +142,7 @@ async def create( created_at: datetime, labware_offsets: List[LabwareOffsetCreate], deck_configuration: DeckConfigurationType, + notify_publishers: Callable[[], None], protocol: Optional[ProtocolResource], ) -> Union[Run, BadRun]: """Create a new, current run. @@ -150,6 +151,7 @@ async def create( run_id: Identifier to assign the new run. created_at: Creation datetime. labware_offsets: Labware offsets to initialize the engine with. + notify_publishers: Utilized by the engine to notify publishers of state changes. Returns: The run resource. @@ -171,6 +173,7 @@ async def create( labware_offsets=labware_offsets, deck_configuration=deck_configuration, protocol=protocol, + notify_publishers=notify_publishers, ) run_resource = self._run_store.insert( run_id=run_id, diff --git a/robot-server/robot_server/service/notifications/__init__.py b/robot-server/robot_server/service/notifications/__init__.py index 202c7fc71f1..7a71a61298d 100644 --- a/robot-server/robot_server/service/notifications/__init__.py +++ b/robot-server/robot_server/service/notifications/__init__.py @@ -1,15 +1,19 @@ +"""Notification service creation and management.""" +from .initialize_notifications import initialize_notifications + from .notification_client import ( NotificationClient, get_notification_client, - initialize_notification_client, clean_up_notification_client, ) +from .publisher_notifier import PublisherNotifier, get_notify_publishers from .publishers import ( MaintenanceRunsPublisher, RunsPublisher, get_maintenance_runs_publisher, get_runs_publisher, ) +from .change_notifier import ChangeNotifier __all__ = [ # main export @@ -18,10 +22,14 @@ "MaintenanceRunsPublisher", "RunsPublisher", # initialization and teardown - "initialize_notification_client", + "initialize_notifications", "clean_up_notification_client", # for use by FastAPI "get_notification_client", + "get_notify_publishers", "get_maintenance_runs_publisher", "get_runs_publisher", + # for testing + "PublisherNotifier", + "ChangeNotifier", ] diff --git a/robot-server/robot_server/service/notifications/change_notifier.py b/robot-server/robot_server/service/notifications/change_notifier.py new file mode 100644 index 00000000000..60c36c420af --- /dev/null +++ b/robot-server/robot_server/service/notifications/change_notifier.py @@ -0,0 +1,23 @@ +"""Simple state change notification interface.""" +import asyncio + + +class ChangeNotifier: + """An interface to emit or subscribe to state change notifications.""" + + def __init__(self) -> None: + """Initialize the ChangeNotifier with an internal Event.""" + self._event = asyncio.Event() + + def notify(self) -> None: + """Notify all `waiters` of a change.""" + self._event.set() + + async def wait(self) -> None: + """Wait until the next change notification.""" + self._event.clear() + await self._event.wait() + + def clear(self) -> None: + """Reset the internal event flag.""" + self._event.clear() diff --git a/robot-server/robot_server/service/notifications/initialize_notifications.py b/robot-server/robot_server/service/notifications/initialize_notifications.py new file mode 100644 index 00000000000..d5569d09eff --- /dev/null +++ b/robot-server/robot_server/service/notifications/initialize_notifications.py @@ -0,0 +1,11 @@ +"""Utilities for initializing the notification service.""" +from server_utils.fastapi_utils.app_state import AppState + +from .notification_client import initialize_notification_client +from .publisher_notifier import initialize_publisher_notifier + + +async def initialize_notifications(app_state: AppState) -> None: + """Initialize the notification system for the given app state.""" + initialize_notification_client(app_state) + await initialize_publisher_notifier(app_state) diff --git a/robot-server/robot_server/service/notifications/notification_client.py b/robot-server/robot_server/service/notifications/notification_client.py index 568d161cf53..6b51eba9cc9 100644 --- a/robot-server/robot_server/service/notifications/notification_client.py +++ b/robot-server/robot_server/service/notifications/notification_client.py @@ -1,3 +1,4 @@ +"""An interface for managing interactions with the notification broker and relevant lifecycle utilities.""" import random import logging import paho.mqtt.client as mqtt @@ -208,7 +209,5 @@ def get_notification_client( app_state: AppState = Depends(get_app_state), ) -> Optional[NotificationClient]: """Intended to be used by endpoint functions as a FastAPI dependency.""" - notification_client: Optional[ - NotificationClient - ] = _notification_client_accessor.get_from(app_state) + notification_client = _notification_client_accessor.get_from(app_state) return notification_client diff --git a/robot-server/robot_server/service/notifications/publisher_notifier.py b/robot-server/robot_server/service/notifications/publisher_notifier.py new file mode 100644 index 00000000000..d1769ac4379 --- /dev/null +++ b/robot-server/robot_server/service/notifications/publisher_notifier.py @@ -0,0 +1,81 @@ +"""Provides an interface for alerting notification publishers to events and related lifecycle utilities.""" +import asyncio +from fastapi import Depends +from typing import Optional, Callable, List, Awaitable + +from server_utils.fastapi_utils.app_state import ( + AppState, + AppStateAccessor, + get_app_state, +) + +from .change_notifier import ChangeNotifier + + +class PublisherNotifier: + """An interface that invokes notification callbacks whenever a generic notify event occurs.""" + + def __init__( + self, + change_notifier: Optional[ChangeNotifier] = None, + ): + self._change_notifier = change_notifier or ChangeNotifier() + self._pe_notifier: Optional[asyncio.Task[None]] = None + self._callbacks: List[Callable[[], Awaitable[None]]] = [] + + def register_publish_callbacks( + self, callbacks: List[Callable[[], Awaitable[None]]] + ): + """Extend the list of callbacks with a given list of callbacks.""" + self._callbacks.extend(callbacks) + + async def _initialize(self) -> None: + """Initializes an instance of PublisherNotifier. This method should only be called once.""" + self._pe_notifier = asyncio.create_task(self._wait_for_event()) + + def _notify_publishers(self) -> None: + """A generic notifier, alerting all `waiters` of a change.""" + self._change_notifier.notify() + + async def _wait_for_event(self) -> None: + """Indefinitely wait for an event to occur, then invoke each callback.""" + while True: + await self._change_notifier.wait() + for callback in self._callbacks: + await callback() + + +_publisher_notifier_accessor: AppStateAccessor[PublisherNotifier] = AppStateAccessor[ + PublisherNotifier +]("publisher_notifier") + + +def get_publisher_notifier( + app_state: AppState = Depends(get_app_state), +) -> PublisherNotifier: + """Intended for use by various publishers only.""" + publisher_notifier = _publisher_notifier_accessor.get_from(app_state) + assert publisher_notifier is not None + + return publisher_notifier + + +def get_notify_publishers( + app_state: AppState = Depends(get_app_state), +) -> Callable[[], None]: + """Provides access to the callback used to notify publishers of changes.""" + publisher_notifier = _publisher_notifier_accessor.get_from(app_state) + assert isinstance(publisher_notifier, PublisherNotifier) + + return publisher_notifier._notify_publishers + + +async def initialize_publisher_notifier(app_state: AppState) -> None: + """Create a new `NotificationClient` and store it on `app_state`. + + Intended to be called just once, when the server starts up. + """ + publisher_notifier: PublisherNotifier = PublisherNotifier() + _publisher_notifier_accessor.set_on(app_state, publisher_notifier) + + await publisher_notifier._initialize() diff --git a/robot-server/robot_server/service/notifications/publishers/__init__.py b/robot-server/robot_server/service/notifications/publishers/__init__.py index 1dcdc43d4a9..59a30e7a135 100644 --- a/robot-server/robot_server/service/notifications/publishers/__init__.py +++ b/robot-server/robot_server/service/notifications/publishers/__init__.py @@ -1,3 +1,8 @@ +"""Publisher creation and management. + +A unique publisher is responsible for each router's related set of endpoints. The publisher conditionally determines +whether a relevant event has occurred, and if true, it publishes an appropriate message to the robot's message broker. +""" from .maintenance_runs_publisher import ( MaintenanceRunsPublisher, get_maintenance_runs_publisher, diff --git a/robot-server/robot_server/service/notifications/topics.py b/robot-server/robot_server/service/notifications/topics.py index 9e3d5fe0ea4..34f2fd0eea1 100644 --- a/robot-server/robot_server/service/notifications/topics.py +++ b/robot-server/robot_server/service/notifications/topics.py @@ -1,3 +1,4 @@ +"""Notification topics.""" from enum import Enum diff --git a/robot-server/tests/maintenance_runs/router/test_base_router.py b/robot-server/tests/maintenance_runs/router/test_base_router.py index 4e2b8b399e5..2f61afcac48 100644 --- a/robot-server/tests/maintenance_runs/router/test_base_router.py +++ b/robot-server/tests/maintenance_runs/router/test_base_router.py @@ -36,6 +36,11 @@ from robot_server.deck_configuration.store import DeckConfigurationStore +def mock_notify_publishers() -> None: + """A mock notify_publishers.""" + return None + + @pytest.fixture def labware_offset_create() -> LabwareOffsetCreate: """Get a labware offset create request value object.""" @@ -79,6 +84,7 @@ async def test_create_run( created_at=run_created_at, labware_offsets=[labware_offset_create], deck_configuration=[], + notify_publishers=mock_notify_publishers, ) ).then_return(expected_response) @@ -91,6 +97,7 @@ async def test_create_run( created_at=run_created_at, is_ok_to_create_maintenance_run=True, deck_configuration_store=mock_deck_configuration_store, + notify_publishers=mock_notify_publishers, ) assert result.content.data == expected_response diff --git a/robot-server/tests/maintenance_runs/test_engine_store.py b/robot-server/tests/maintenance_runs/test_engine_store.py index d0a3ccfc1c8..15855ab48d1 100644 --- a/robot-server/tests/maintenance_runs/test_engine_store.py +++ b/robot-server/tests/maintenance_runs/test_engine_store.py @@ -24,6 +24,11 @@ ) +def mock_notify_publishers() -> None: + """A mock notify_publishers.""" + return None + + @pytest.fixture def subject(decoy: Decoy) -> MaintenanceEngineStore: """Get a MaintenanceEngineStore test subject.""" @@ -42,7 +47,10 @@ def subject(decoy: Decoy) -> MaintenanceEngineStore: async def test_create_engine(subject: MaintenanceEngineStore) -> None: """It should create an engine for a run.""" result = await subject.create( - run_id="run-id", labware_offsets=[], created_at=datetime(2023, 1, 1) + run_id="run-id", + labware_offsets=[], + created_at=datetime(2023, 1, 1), + notify_publishers=mock_notify_publishers, ) assert subject.current_run_id == "run-id" @@ -67,7 +75,10 @@ async def test_create_engine_uses_robot_and_deck_type( ) await subject.create( - run_id="run-id", labware_offsets=[], created_at=datetime(2023, 4, 1) + run_id="run-id", + labware_offsets=[], + created_at=datetime(2023, 4, 1), + notify_publishers=mock_notify_publishers, ) assert subject.engine.state_view.config.robot_type == robot_type @@ -88,6 +99,7 @@ async def test_create_engine_with_labware_offsets( run_id="run-id", labware_offsets=[labware_offset], created_at=datetime(2023, 1, 1), + notify_publishers=mock_notify_publishers, ) assert result.labwareOffsets == [ @@ -104,7 +116,10 @@ async def test_create_engine_with_labware_offsets( async def test_clear_engine(subject: MaintenanceEngineStore) -> None: """It should clear a stored engine entry.""" await subject.create( - run_id="run-id", labware_offsets=[], created_at=datetime(2023, 5, 1) + run_id="run-id", + labware_offsets=[], + created_at=datetime(2023, 5, 1), + notify_publishers=mock_notify_publishers, ) await subject.runner.run(deck_configuration=[]) result = await subject.clear() @@ -124,7 +139,10 @@ async def test_clear_engine_not_stopped_or_idle( ) -> None: """It should raise a conflict if the engine is not stopped.""" await subject.create( - run_id="run-id", labware_offsets=[], created_at=datetime(2023, 6, 1) + run_id="run-id", + labware_offsets=[], + created_at=datetime(2023, 6, 1), + notify_publishers=mock_notify_publishers, ) subject.runner.play() @@ -135,7 +153,10 @@ async def test_clear_engine_not_stopped_or_idle( async def test_clear_idle_engine(subject: MaintenanceEngineStore) -> None: """It should successfully clear engine if idle (not started).""" await subject.create( - run_id="run-id", labware_offsets=[], created_at=datetime(2023, 7, 1) + run_id="run-id", + labware_offsets=[], + created_at=datetime(2023, 7, 1), + notify_publishers=mock_notify_publishers, ) assert subject.engine is not None assert subject.runner is not None diff --git a/robot-server/tests/maintenance_runs/test_run_data_manager.py b/robot-server/tests/maintenance_runs/test_run_data_manager.py index f0e63809d68..0046b3098db 100644 --- a/robot-server/tests/maintenance_runs/test_run_data_manager.py +++ b/robot-server/tests/maintenance_runs/test_run_data_manager.py @@ -35,6 +35,11 @@ from opentrons.protocol_engine import Liquid +def mock_notify_publishers() -> None: + """A mock notify_publishers.""" + return None + + @pytest.fixture def mock_maintenance_engine_store(decoy: Decoy) -> MaintenanceEngineStore: """Get a mock MaintenanceEngineStore.""" @@ -104,6 +109,7 @@ async def test_create( labware_offsets=[], created_at=created_at, deck_configuration=[], + notify_publishers=mock_notify_publishers, ) ).then_return(engine_state_summary) decoy.when(mock_maintenance_engine_store.current_run_created_at).then_return( @@ -114,6 +120,7 @@ async def test_create( created_at=created_at, labware_offsets=[], deck_configuration=[], + notify_publishers=mock_notify_publishers, ) assert result == MaintenanceRun( @@ -153,6 +160,7 @@ async def test_create_with_options( labware_offsets=[labware_offset], created_at=created_at, deck_configuration=[], + notify_publishers=mock_notify_publishers, ) ).then_return(engine_state_summary) decoy.when(mock_maintenance_engine_store.current_run_created_at).then_return( @@ -164,6 +172,7 @@ async def test_create_with_options( created_at=created_at, labware_offsets=[labware_offset], deck_configuration=[], + notify_publishers=mock_notify_publishers, ) assert result == MaintenanceRun( @@ -196,6 +205,7 @@ async def test_create_engine_error( labware_offsets=[], created_at=created_at, deck_configuration=[], + notify_publishers=mock_notify_publishers, ) ).then_raise(EngineConflictError("oh no")) decoy.when(mock_maintenance_engine_store.current_run_created_at).then_return( @@ -208,6 +218,7 @@ async def test_create_engine_error( created_at=created_at, labware_offsets=[], deck_configuration=[], + notify_publishers=mock_notify_publishers, ) diff --git a/robot-server/tests/runs/router/test_base_router.py b/robot-server/tests/runs/router/test_base_router.py index 1fd754f224a..5c772e14be7 100644 --- a/robot-server/tests/runs/router/test_base_router.py +++ b/robot-server/tests/runs/router/test_base_router.py @@ -42,6 +42,11 @@ from robot_server.deck_configuration.store import DeckConfigurationStore +def mock_notify_publishers() -> None: + """A mock notify_publishers.""" + return None + + @pytest.fixture def labware_offset_create() -> LabwareOffsetCreate: """Get a labware offset create request value object.""" @@ -87,6 +92,7 @@ async def test_create_run( labware_offsets=[labware_offset_create], deck_configuration=[], protocol=None, + notify_publishers=mock_notify_publishers, ) ).then_return(expected_response) @@ -99,6 +105,7 @@ async def test_create_run( created_at=run_created_at, run_auto_deleter=mock_run_auto_deleter, deck_configuration_store=mock_deck_configuration_store, + notify_publishers=mock_notify_publishers, ) assert result.content.data == expected_response @@ -162,6 +169,7 @@ async def test_create_protocol_run( labware_offsets=[], deck_configuration=[], protocol=protocol_resource, + notify_publishers=mock_notify_publishers, ) ).then_return(expected_response) @@ -173,6 +181,7 @@ async def test_create_protocol_run( created_at=run_created_at, run_auto_deleter=mock_run_auto_deleter, deck_configuration_store=mock_deck_configuration_store, + notify_publishers=mock_notify_publishers, ) assert result.content.data == expected_response @@ -223,6 +232,7 @@ async def test_create_run_conflict( labware_offsets=[], deck_configuration=[], protocol=None, + notify_publishers=mock_notify_publishers, ) ).then_raise(EngineConflictError("oh no")) @@ -234,6 +244,7 @@ async def test_create_run_conflict( run_data_manager=mock_run_data_manager, run_auto_deleter=mock_run_auto_deleter, deck_configuration_store=mock_deck_configuration_store, + notify_publishers=mock_notify_publishers, ) assert exc_info.value.status_code == 409 diff --git a/robot-server/tests/runs/test_engine_store.py b/robot-server/tests/runs/test_engine_store.py index 1bf74632139..7a1f79b903a 100644 --- a/robot-server/tests/runs/test_engine_store.py +++ b/robot-server/tests/runs/test_engine_store.py @@ -27,6 +27,11 @@ ) +def mock_notify_publishers() -> None: + """A mock notify_publishers.""" + return None + + @pytest.fixture def subject(decoy: Decoy, hardware_api: HardwareControlAPI) -> EngineStore: """Get a EngineStore test subject.""" @@ -51,7 +56,11 @@ async def json_protocol_source(tmp_path: Path) -> ProtocolSource: async def test_create_engine(subject: EngineStore) -> None: """It should create an engine for a run.""" result = await subject.create( - run_id="run-id", labware_offsets=[], protocol=None, deck_configuration=[] + run_id="run-id", + labware_offsets=[], + protocol=None, + deck_configuration=[], + notify_publishers=mock_notify_publishers, ) assert subject.current_run_id == "run-id" @@ -82,6 +91,7 @@ async def test_create_engine_with_protocol( labware_offsets=[], deck_configuration=[], protocol=protocol, + notify_publishers=mock_notify_publishers, ) assert subject.current_run_id == "run-id" assert isinstance(result, StateSummary) @@ -103,7 +113,11 @@ async def test_create_engine_uses_robot_type( ) await subject.create( - run_id="run-id", labware_offsets=[], deck_configuration=[], protocol=None + run_id="run-id", + labware_offsets=[], + deck_configuration=[], + protocol=None, + notify_publishers=mock_notify_publishers, ) assert subject.engine.state_view.config.robot_type == robot_type @@ -122,6 +136,7 @@ async def test_create_engine_with_labware_offsets(subject: EngineStore) -> None: labware_offsets=[labware_offset], deck_configuration=[], protocol=None, + notify_publishers=mock_notify_publishers, ) assert result.labwareOffsets == [ @@ -138,12 +153,20 @@ async def test_create_engine_with_labware_offsets(subject: EngineStore) -> None: async def test_archives_state_if_engine_already_exists(subject: EngineStore) -> None: """It should not create more than one engine / runner pair.""" await subject.create( - run_id="run-id-1", labware_offsets=[], deck_configuration=[], protocol=None + run_id="run-id-1", + labware_offsets=[], + deck_configuration=[], + protocol=None, + notify_publishers=mock_notify_publishers, ) with pytest.raises(EngineConflictError): await subject.create( - run_id="run-id-2", labware_offsets=[], deck_configuration=[], protocol=None + run_id="run-id-2", + labware_offsets=[], + deck_configuration=[], + protocol=None, + notify_publishers=mock_notify_publishers, ) assert subject.current_run_id == "run-id-1" @@ -152,7 +175,11 @@ async def test_archives_state_if_engine_already_exists(subject: EngineStore) -> async def test_clear_engine(subject: EngineStore) -> None: """It should clear a stored engine entry.""" await subject.create( - run_id="run-id", labware_offsets=[], deck_configuration=[], protocol=None + run_id="run-id", + labware_offsets=[], + deck_configuration=[], + protocol=None, + notify_publishers=mock_notify_publishers, ) await subject.runner.run(deck_configuration=[]) result = await subject.clear() @@ -172,7 +199,11 @@ async def test_clear_engine_not_stopped_or_idle( ) -> None: """It should raise a conflict if the engine is not stopped.""" await subject.create( - run_id="run-id", labware_offsets=[], deck_configuration=[], protocol=None + run_id="run-id", + labware_offsets=[], + deck_configuration=[], + protocol=None, + notify_publishers=mock_notify_publishers, ) subject.runner.play(deck_configuration=[]) @@ -183,7 +214,11 @@ async def test_clear_engine_not_stopped_or_idle( async def test_clear_idle_engine(subject: EngineStore) -> None: """It should successfully clear engine if idle (not started).""" await subject.create( - run_id="run-id", labware_offsets=[], deck_configuration=[], protocol=None + run_id="run-id", + labware_offsets=[], + deck_configuration=[], + protocol=None, + notify_publishers=mock_notify_publishers, ) assert subject.engine is not None assert subject.runner is not None @@ -216,7 +251,9 @@ async def test_get_default_engine_robot_type( # should pass in some sort of actual, valid HardwareAPI instead of a mock hardware_api = decoy.mock(cls=API) subject = EngineStore( - hardware_api=hardware_api, robot_type=robot_type, deck_type=deck_type + hardware_api=hardware_api, + robot_type=robot_type, + deck_type=deck_type, ) result = await subject.get_default_engine() @@ -227,7 +264,11 @@ async def test_get_default_engine_robot_type( async def test_get_default_engine_current_unstarted(subject: EngineStore) -> None: """It should allow a default engine if another engine current but unstarted.""" await subject.create( - run_id="run-id", labware_offsets=[], deck_configuration=[], protocol=None + run_id="run-id", + labware_offsets=[], + deck_configuration=[], + protocol=None, + notify_publishers=mock_notify_publishers, ) result = await subject.get_default_engine() @@ -237,7 +278,11 @@ async def test_get_default_engine_current_unstarted(subject: EngineStore) -> Non async def test_get_default_engine_conflict(subject: EngineStore) -> None: """It should not allow a default engine if another engine is executing commands.""" await subject.create( - run_id="run-id", labware_offsets=[], deck_configuration=[], protocol=None + run_id="run-id", + labware_offsets=[], + deck_configuration=[], + protocol=None, + notify_publishers=mock_notify_publishers, ) subject.engine.play() @@ -248,7 +293,11 @@ async def test_get_default_engine_conflict(subject: EngineStore) -> None: async def test_get_default_engine_run_stopped(subject: EngineStore) -> None: """It allow a default engine if another engine is terminal.""" await subject.create( - run_id="run-id", labware_offsets=[], deck_configuration=[], protocol=None + run_id="run-id", + labware_offsets=[], + deck_configuration=[], + protocol=None, + notify_publishers=mock_notify_publishers, ) await subject.engine.finish() diff --git a/robot-server/tests/runs/test_run_data_manager.py b/robot-server/tests/runs/test_run_data_manager.py index 92152eb3940..bac302e3065 100644 --- a/robot-server/tests/runs/test_run_data_manager.py +++ b/robot-server/tests/runs/test_run_data_manager.py @@ -40,6 +40,11 @@ from opentrons_shared_data.errors.exceptions import InvalidStoredData +def mock_notify_publishers() -> None: + """A mock notify_publishers.""" + return None + + @pytest.fixture def mock_engine_store(decoy: Decoy) -> EngineStore: """Get a mock EngineStore.""" @@ -138,6 +143,7 @@ async def test_create( labware_offsets=[], protocol=None, deck_configuration=[], + notify_publishers=mock_notify_publishers, ) ).then_return(engine_state_summary) decoy.when( @@ -154,6 +160,7 @@ async def test_create( labware_offsets=[], protocol=None, deck_configuration=[], + notify_publishers=mock_notify_publishers, ) assert result == Run( @@ -203,6 +210,7 @@ async def test_create_with_options( labware_offsets=[labware_offset], protocol=protocol, deck_configuration=[], + notify_publishers=mock_notify_publishers, ) ).then_return(engine_state_summary) @@ -220,6 +228,7 @@ async def test_create_with_options( labware_offsets=[labware_offset], protocol=protocol, deck_configuration=[], + notify_publishers=mock_notify_publishers, ) assert result == Run( @@ -254,6 +263,7 @@ async def test_create_engine_error( labware_offsets=[], protocol=None, deck_configuration=[], + notify_publishers=mock_notify_publishers, ) ).then_raise(EngineConflictError("oh no")) @@ -264,6 +274,7 @@ async def test_create_engine_error( labware_offsets=[], protocol=None, deck_configuration=[], + notify_publishers=mock_notify_publishers, ) decoy.verify( @@ -640,6 +651,7 @@ async def test_create_archives_existing( labware_offsets=[], protocol=None, deck_configuration=[], + notify_publishers=mock_notify_publishers, ) ).then_return(engine_state_summary) @@ -657,6 +669,7 @@ async def test_create_archives_existing( labware_offsets=[], protocol=None, deck_configuration=[], + notify_publishers=mock_notify_publishers, ) decoy.verify( diff --git a/robot-server/tests/service/notifications/__init__.py b/robot-server/tests/service/notifications/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/robot-server/tests/service/notifications/test_change_notifier.py b/robot-server/tests/service/notifications/test_change_notifier.py new file mode 100644 index 00000000000..4967e6d254e --- /dev/null +++ b/robot-server/tests/service/notifications/test_change_notifier.py @@ -0,0 +1,56 @@ +"""Tests for the ChangeNotifier interface.""" +import asyncio +import pytest +from opentrons.protocol_engine.state.change_notifier import ChangeNotifier + + +async def test_single_subscriber() -> None: + """Test that a single subscriber can wait for a notification.""" + subject = ChangeNotifier() + result = asyncio.create_task(subject.wait()) + + # ensure that the wait actually waits by delaying and + # checking that the task has not resolved + await asyncio.sleep(0.1) + assert result.done() is False + + asyncio.get_running_loop().call_soon(subject.notify) + + await result + + +@pytest.mark.parametrize("_test_repetition", range(10)) +async def test_multiple_subscribers(_test_repetition: int) -> None: + """Test that multiple subscribers can wait for a notification. + + This test checks that the subscribers are awoken in the order they + subscribed. This may or may not be guarenteed according to the + implementations of both ChangeNotifier and the event loop. + This test functions as a canary, given that our code may relies + on this ordering for determinism. + + This test runs multiple times to check for flakyness. + """ + subject = ChangeNotifier() + results = [] + + async def _do_task_1() -> None: + await subject.wait() + results.append(1) + + async def _do_task_2() -> None: + await subject.wait() + results.append(2) + + async def _do_task_3() -> None: + await subject.wait() + results.append(3) + + task_1 = asyncio.create_task(_do_task_1()) + task_2 = asyncio.create_task(_do_task_2()) + task_3 = asyncio.create_task(_do_task_3()) + + asyncio.get_running_loop().call_soon(subject.notify) + await asyncio.gather(task_1, task_2, task_3) + + assert results == [1, 2, 3] diff --git a/robot-server/tests/service/notifications/test_publisher_notifier.py b/robot-server/tests/service/notifications/test_publisher_notifier.py new file mode 100644 index 00000000000..125cfdd1806 --- /dev/null +++ b/robot-server/tests/service/notifications/test_publisher_notifier.py @@ -0,0 +1,74 @@ +import asyncio +from unittest.mock import Mock, MagicMock + +from robot_server.service.notifications import ( + PublisherNotifier, + ChangeNotifier, +) + + +async def test_initialize() -> None: + """It should create a new task.""" + publisher_notifier = PublisherNotifier() + + await publisher_notifier._initialize() + + assert asyncio.get_running_loop() + + +def test_notify_publishers() -> None: + """Invoke the change notifier's notify method.""" + change_notifier = MagicMock() + publisher_notifier = PublisherNotifier(change_notifier) + + publisher_notifier._notify_publishers() + + change_notifier.notify.assert_called_once() + + +def test_register_publish_callbacks() -> None: + """It should extend the list of callbacks within a given list of callbacks.""" + publisher_notifier = PublisherNotifier() + callback1 = Mock() + callback2 = Mock() + + publisher_notifier.register_publish_callbacks([callback1, callback2]) + + assert len(publisher_notifier._callbacks) == 2 + assert publisher_notifier._callbacks[0] == callback1 + assert publisher_notifier._callbacks[1] == callback2 + + +async def test_wait_for_event() -> None: + """It should wait for an event to occur, then invoke each callback.""" + change_notifier = ChangeNotifier() + publisher_notifier = PublisherNotifier(change_notifier) + + callback_called = False + callback_2_called = False + + async def callback() -> None: + """Mock callback.""" + nonlocal callback_called + callback_called = True + + async def callback_2() -> None: + """Mock callback.""" + nonlocal callback_2_called + callback_2_called = True + + publisher_notifier.register_publish_callbacks([callback, callback_2]) + + async def trigger_callbacks() -> None: + """Mock trigger for callbacks.""" + await asyncio.sleep(0.1) + change_notifier.notify() + + task = asyncio.create_task(publisher_notifier._initialize()) + + await asyncio.gather(trigger_callbacks(), task) + + assert callback_called + assert callback_2_called + + task.cancel() From bb680e2972fc92d2600fc1b2055f2537e840aadd Mon Sep 17 00:00:00 2001 From: Sarah Breen Date: Tue, 2 Apr 2024 12:03:29 -0400 Subject: [PATCH 017/194] feat(app): button to launch quick transfer flow when FF is on (#14772) fix PLAT-170 --- app/src/assets/localization/en/protocol_info.json | 1 + app/src/pages/ProtocolDashboard/index.tsx | 14 +++++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/app/src/assets/localization/en/protocol_info.json b/app/src/assets/localization/en/protocol_info.json index bbaac1ce9c2..d1e73288dcd 100644 --- a/app/src/assets/localization/en/protocol_info.json +++ b/app/src/assets/localization/en/protocol_info.json @@ -65,6 +65,7 @@ "protocol_title": "Protocol - {{protocol_name}}", "protocol_upload_failed": "Protocol upload failed. Fix the error and try again", "protocols": "Protocols", + "quick_transfer": "Quick transfer", "required_cal_data_title": "Calibration Data", "required_quantity_title": "Quantity", "required_type_title": "Type", diff --git a/app/src/pages/ProtocolDashboard/index.tsx b/app/src/pages/ProtocolDashboard/index.tsx index 85d3fe154c0..e326ab7176c 100644 --- a/app/src/pages/ProtocolDashboard/index.tsx +++ b/app/src/pages/ProtocolDashboard/index.tsx @@ -16,12 +16,13 @@ import { } from '@opentrons/components' import { useAllProtocolsQuery } from '@opentrons/react-api-client' -import { SmallButton } from '../../atoms/buttons' +import { SmallButton, FloatingActionButton } from '../../atoms/buttons' import { Navigation } from '../../organisms/Navigation' import { getPinnedProtocolIds, getProtocolsOnDeviceSortKey, updateConfigValue, + useFeatureFlag, } from '../../redux/config' import { PinnedProtocolCarousel } from './PinnedProtocolCarousel' import { sortProtocols } from './utils' @@ -57,6 +58,8 @@ export function ProtocolDashboard(): JSX.Element { const pinnedProtocolIds = useSelector(getPinnedProtocolIds) ?? [] const pinnedProtocols: ProtocolResource[] = [] + const enableQuickTransferFF = useFeatureFlag('enableQuickTransfer') + // We only need to grab out the pinned protocol data once all the protocols load // and if we have pinned ids stored in config. if (protocolsData.length > 0 && pinnedProtocolIds.length > 0) { @@ -272,6 +275,15 @@ export function ProtocolDashboard(): JSX.Element { ) : null}
+ {enableQuickTransferFF && ( + { + console.log('launch quick transfer flow') + }} + /> + )} ) } From 2dbb1d98e30cbe8b75631d2eefa3b280ed451037 Mon Sep 17 00:00:00 2001 From: Nick Diehl <47604184+ncdiehl11@users.noreply.github.com> Date: Tue, 2 Apr 2024 13:01:28 -0400 Subject: [PATCH 018/194] feat(app): add support for toggling boolean RTPs and restoring defaults (#14770) closes [AUTH-117](https://opentrons.atlassian.net/browse/AUTH-117) --- .../ResetValuesModal.tsx | 12 ++++++++++- .../__tests__/ResetValuesModal.test.tsx | 12 ++++++++++- .../ProtocolSetupParameters/index.tsx | 20 ++++++++++++++----- 3 files changed, 37 insertions(+), 7 deletions(-) diff --git a/app/src/organisms/ProtocolSetupParameters/ResetValuesModal.tsx b/app/src/organisms/ProtocolSetupParameters/ResetValuesModal.tsx index 458b1172f3a..b49151f883b 100644 --- a/app/src/organisms/ProtocolSetupParameters/ResetValuesModal.tsx +++ b/app/src/organisms/ProtocolSetupParameters/ResetValuesModal.tsx @@ -14,13 +14,18 @@ import { import { SmallButton } from '../../atoms/buttons' import { Modal } from '../../molecules/Modal' +import type { RunTimeParameter } from '@opentrons/shared-data' import type { ModalHeaderBaseProps } from '../../molecules/Modal/types' interface ResetValuesModalProps { + runTimeParametersOverrides: RunTimeParameter[] + setRunTimeParametersOverrides: (parameters: RunTimeParameter[]) => void handleGoBack: () => void } export function ResetValuesModal({ + runTimeParametersOverrides, + setRunTimeParametersOverrides, handleGoBack, }: ResetValuesModalProps): JSX.Element { const { t } = useTranslation(['protocol_setup', 'shared']) @@ -33,7 +38,12 @@ export function ResetValuesModal({ // ToDo (kk:03/18/2024) reset values function will be implemented const handleResetValues = (): void => { - console.log('todo add reset values function') + setRunTimeParametersOverrides( + runTimeParametersOverrides.map(param => { + return { ...param, value: param.default } + }) + ) + handleGoBack() } const modalProps = { diff --git a/app/src/organisms/ProtocolSetupParameters/__tests__/ResetValuesModal.test.tsx b/app/src/organisms/ProtocolSetupParameters/__tests__/ResetValuesModal.test.tsx index a8f876b94f3..ec2eb28a81c 100644 --- a/app/src/organisms/ProtocolSetupParameters/__tests__/ResetValuesModal.test.tsx +++ b/app/src/organisms/ProtocolSetupParameters/__tests__/ResetValuesModal.test.tsx @@ -5,8 +5,10 @@ import { fireEvent, screen } from '@testing-library/react' import { renderWithProviders } from '../../../__testing-utils__' import { i18n } from '../../../i18n' import { ResetValuesModal } from '../ResetValuesModal' +import { RunTimeParameter } from '@opentrons/shared-data' const mockGoBack = vi.fn() +const mockSetRunTimeParametersOverrides = vi.fn() const render = (props: React.ComponentProps) => { return renderWithProviders(, { @@ -19,6 +21,8 @@ describe('ResetValuesModal', () => { beforeEach(() => { props = { + runTimeParametersOverrides: [] as RunTimeParameter[], + setRunTimeParametersOverrides: mockSetRunTimeParametersOverrides, handleGoBack: mockGoBack, } }) @@ -42,5 +46,11 @@ describe('ResetValuesModal', () => { }) // ToDo (kk: 03/18/2024) reset value button test will be added - it.todo('should call a mock function when tapping reset values button') + it('should call a mock function when tapping reset values button', () => { + render(props) + const resetValuesButton = screen.getByText('Reset values') + fireEvent.click(resetValuesButton) + expect(mockSetRunTimeParametersOverrides) + expect(mockGoBack).toHaveBeenCalled() + }) }) diff --git a/app/src/organisms/ProtocolSetupParameters/index.tsx b/app/src/organisms/ProtocolSetupParameters/index.tsx index c99c4ebeff6..a95f1b59b23 100644 --- a/app/src/organisms/ProtocolSetupParameters/index.tsx +++ b/app/src/organisms/ProtocolSetupParameters/index.tsx @@ -3,13 +3,13 @@ import { useTranslation } from 'react-i18next' import { useHistory } from 'react-router-dom' import { useCreateRunMutation, useHost } from '@opentrons/react-api-client' import { useQueryClient } from 'react-query' -import { formatRunTimeParameterValue } from '@opentrons/shared-data' import { ALIGN_CENTER, DIRECTION_COLUMN, Flex, SPACING, } from '@opentrons/components' +import { formatRunTimeParameterValue } from '@opentrons/shared-data' import { ProtocolSetupStep } from '../../pages/ProtocolSetup' import { ChildNavigation } from '../ChildNavigation' @@ -179,8 +179,14 @@ export function ProtocolSetupParameters({ const [resetValuesModal, showResetValuesModal] = React.useState( false ) - const parameters = runTimeParameters ?? [] - // TODO(jr, 3/20/24): modify useCreateRunMutation to take in optional run time parameters + + // todo (nd:04/01/2024): remove mock and look at runTimeParameters prop + // const parameters = runTimeParameters ?? [] + const parameters = runTimeParameters ?? mockData + const [ + runTimeParametersOverrides, + setRunTimeParametersOverrides, + ] = React.useState(parameters) const { createRun, isLoading } = useCreateRunMutation({ onSuccess: data => { queryClient @@ -197,7 +203,11 @@ export function ProtocolSetupParameters({ return ( <> {resetValuesModal ? ( - showResetValuesModal(false)} /> + showResetValuesModal(false)} + /> ) : null} {parameters.map((parameter, index) => { return ( From 6933c61388f82505f0a4fd61c1c3e1b5f390cc05 Mon Sep 17 00:00:00 2001 From: Sarah Breen Date: Tue, 2 Apr 2024 13:12:31 -0400 Subject: [PATCH 019/194] fix(app): fix capitalization of ML to uL in instrument cards (#14779) --- app/src/molecules/InstrumentCard/index.tsx | 4 +--- app/src/organisms/Devices/PipetteCard/FlexPipetteCard.tsx | 4 +++- app/src/organisms/GripperCard/index.tsx | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/src/molecules/InstrumentCard/index.tsx b/app/src/molecules/InstrumentCard/index.tsx index b0f722b8c5a..365c0a3eea5 100644 --- a/app/src/molecules/InstrumentCard/index.tsx +++ b/app/src/molecules/InstrumentCard/index.tsx @@ -111,9 +111,7 @@ export function InstrumentCard(props: InstrumentCardProps): JSX.Element { > {label} - - {description} - + {description} {menuOverlayItems != null && ( Date: Tue, 2 Apr 2024 13:13:43 -0400 Subject: [PATCH 020/194] fix(app,shared-data): change type name from boolean to bool (#14778) * fix(app,shared-data): change type name from boolean to bool --- .../__tests__/ChooseRobotSlideout.test.tsx | 4 ++-- app/src/organisms/ChooseRobotSlideout/index.tsx | 2 +- .../ChooseRobotToRunProtocolSlideout/index.tsx | 2 +- .../__tests__/ProtocolRunRuntimeParameters.test.tsx | 4 ++-- .../__tests__/ProtocolParameters.test.tsx | 2 +- app/src/organisms/ProtocolSetupParameters/index.tsx | 10 +++++----- app/src/pages/ProtocolDetails/Parameters.tsx | 2 +- app/src/pages/ProtocolDetails/fixtures.ts | 8 ++++---- app/src/pages/Protocols/hooks/__tests__/hooks.test.tsx | 8 ++++---- app/src/pages/Protocols/hooks/index.ts | 8 ++++---- .../ParametersTable/ParametersTable.stories.tsx | 8 ++++---- .../ParametersTable/__tests__/ParametersTable.test.tsx | 2 +- components/src/molecules/ParametersTable/index.tsx | 2 +- .../__tests__/formatRunTimeParameterValue.test.ts | 4 ++-- shared-data/js/helpers/formatRunTimeParameterValue.ts | 2 +- shared-data/js/types.ts | 2 +- 16 files changed, 35 insertions(+), 35 deletions(-) diff --git a/app/src/organisms/ChooseRobotSlideout/__tests__/ChooseRobotSlideout.test.tsx b/app/src/organisms/ChooseRobotSlideout/__tests__/ChooseRobotSlideout.test.tsx index ffaaf0f11eb..18bdf233f75 100644 --- a/app/src/organisms/ChooseRobotSlideout/__tests__/ChooseRobotSlideout.test.tsx +++ b/app/src/organisms/ChooseRobotSlideout/__tests__/ChooseRobotSlideout.test.tsx @@ -48,7 +48,7 @@ const mockRunTimeParameters: RunTimeParameter[] = [ value: false, variableName: 'DRYRUN', description: 'Is this a dry or wet run? Wet is true, dry is false', - type: 'boolean', + type: 'bool', default: false, }, { @@ -226,7 +226,7 @@ describe('ChooseRobotSlideout', () => { }) screen.getByText(param.displayName) - if (param.type === 'boolean' || 'choices' in param) { + if (param.type === 'bool' || 'choices' in param) { screen.getByText(param.description) } else { if (param.type === 'int') { diff --git a/app/src/organisms/ChooseRobotSlideout/index.tsx b/app/src/organisms/ChooseRobotSlideout/index.tsx index c6061d437e7..b21e417774b 100644 --- a/app/src/organisms/ChooseRobotSlideout/index.tsx +++ b/app/src/organisms/ChooseRobotSlideout/index.tsx @@ -422,7 +422,7 @@ export function ChooseRobotSlideout( }} /> ) - } else if (runtimeParam.type === 'boolean') { + } else if (runtimeParam.type === 'bool') { return ( { displayName: 'Dry Run', variableName: 'DRYRUN', description: 'Is this a dry or wet run? Wet is true, dry is false', - type: 'boolean', + type: 'bool', default: false, value: true, }, diff --git a/app/src/organisms/ProtocolDetails/ProtocolParameters/__tests__/ProtocolParameters.test.tsx b/app/src/organisms/ProtocolDetails/ProtocolParameters/__tests__/ProtocolParameters.test.tsx index 727ca022890..a752d19c8a4 100644 --- a/app/src/organisms/ProtocolDetails/ProtocolParameters/__tests__/ProtocolParameters.test.tsx +++ b/app/src/organisms/ProtocolDetails/ProtocolParameters/__tests__/ProtocolParameters.test.tsx @@ -25,7 +25,7 @@ const mockRunTimeParameter: RunTimeParameter[] = [ variableName: 'TIP_TRASH', description: 'to throw tip into the trash or to not throw tip into the trash', - type: 'boolean', + type: 'bool', default: true, value: true, }, diff --git a/app/src/organisms/ProtocolSetupParameters/index.tsx b/app/src/organisms/ProtocolSetupParameters/index.tsx index a95f1b59b23..a3cc0687b17 100644 --- a/app/src/organisms/ProtocolSetupParameters/index.tsx +++ b/app/src/organisms/ProtocolSetupParameters/index.tsx @@ -24,7 +24,7 @@ export const mockData: RunTimeParameter[] = [ displayName: 'Dry Run', variableName: 'DRYRUN', description: 'Is this a dry or wet run? Wet is true, dry is false', - type: 'boolean', + type: 'bool', default: false, }, { @@ -32,7 +32,7 @@ export const mockData: RunTimeParameter[] = [ displayName: 'Use Gripper', variableName: 'USE_GRIPPER', description: 'For using the gripper.', - type: 'boolean', + type: 'bool', default: true, }, { @@ -41,7 +41,7 @@ export const mockData: RunTimeParameter[] = [ variableName: 'TIP_TRASH', description: 'to throw tip into the trash or to not throw tip into the trash', - type: 'boolean', + type: 'bool', default: true, }, { @@ -49,7 +49,7 @@ export const mockData: RunTimeParameter[] = [ displayName: 'Deactivate Temperatures', variableName: 'DEACTIVATE_TEMP', description: 'deactivate temperature on the module', - type: 'boolean', + type: 'bool', default: true, }, { @@ -234,7 +234,7 @@ export function ProtocolSetupParameters({ return ( console.log('TODO: wire this up')} diff --git a/app/src/pages/ProtocolDetails/Parameters.tsx b/app/src/pages/ProtocolDetails/Parameters.tsx index 0e12e8d7997..c43b56d7242 100644 --- a/app/src/pages/ProtocolDetails/Parameters.tsx +++ b/app/src/pages/ProtocolDetails/Parameters.tsx @@ -70,7 +70,7 @@ export const Parameters = (props: { protocolId: string }): JSX.Element => { } switch (type) { - case 'boolean': { + case 'bool': { return t('on_off') } case 'float': diff --git a/app/src/pages/ProtocolDetails/fixtures.ts b/app/src/pages/ProtocolDetails/fixtures.ts index 4f5cfa6cdad..d1752853bda 100644 --- a/app/src/pages/ProtocolDetails/fixtures.ts +++ b/app/src/pages/ProtocolDetails/fixtures.ts @@ -5,7 +5,7 @@ export const mockRunTimeParameterData: RunTimeParameter[] = [ displayName: 'Dry Run', variableName: 'DRYRUN', description: 'a dry run description', - type: 'boolean', + type: 'bool', default: false, value: false, }, @@ -13,7 +13,7 @@ export const mockRunTimeParameterData: RunTimeParameter[] = [ displayName: 'Use Gripper', variableName: 'USE_GRIPPER', description: '', - type: 'boolean', + type: 'bool', default: true, value: true, }, @@ -21,7 +21,7 @@ export const mockRunTimeParameterData: RunTimeParameter[] = [ displayName: 'Trash Tips', variableName: 'TIP_TRASH', description: 'throw tip in trash', - type: 'boolean', + type: 'bool', default: true, value: true, }, @@ -29,7 +29,7 @@ export const mockRunTimeParameterData: RunTimeParameter[] = [ displayName: 'Deactivate Temperatures', variableName: 'DEACTIVATE_TEMP', description: 'deactivate temperature?', - type: 'boolean', + type: 'bool', default: true, value: true, }, diff --git a/app/src/pages/Protocols/hooks/__tests__/hooks.test.tsx b/app/src/pages/Protocols/hooks/__tests__/hooks.test.tsx index ce09a610ff7..aa8d9f07e8a 100644 --- a/app/src/pages/Protocols/hooks/__tests__/hooks.test.tsx +++ b/app/src/pages/Protocols/hooks/__tests__/hooks.test.tsx @@ -38,28 +38,28 @@ const mockRTPData = [ displayName: 'Dry Run', variableName: 'DRYRUN', description: 'a dry run description', - type: 'boolean', + type: 'bool', default: false, }, { displayName: 'Use Gripper', variableName: 'USE_GRIPPER', description: '', - type: 'boolean', + type: 'bool', default: true, }, { displayName: 'Trash Tips', variableName: 'TIP_TRASH', description: 'throw tip in trash', - type: 'boolean', + type: 'bool', default: true, }, { displayName: 'Deactivate Temperatures', variableName: 'DEACTIVATE_TEMP', description: 'deactivate temperature?', - type: 'boolean', + type: 'bool', default: true, }, { diff --git a/app/src/pages/Protocols/hooks/index.ts b/app/src/pages/Protocols/hooks/index.ts index 9931a49444f..c873ff35a9f 100644 --- a/app/src/pages/Protocols/hooks/index.ts +++ b/app/src/pages/Protocols/hooks/index.ts @@ -206,7 +206,7 @@ export const useRunTimeParameters = ( displayName: 'Dry Run', variableName: 'DRYRUN', description: 'Is this a dry or wet run? Wet is true, dry is false', - type: 'boolean', + type: 'bool', default: false, }, { @@ -214,7 +214,7 @@ export const useRunTimeParameters = ( displayName: 'Use Gripper', variableName: 'USE_GRIPPER', description: 'For using the gripper.', - type: 'boolean', + type: 'bool', default: true, }, { @@ -223,7 +223,7 @@ export const useRunTimeParameters = ( variableName: 'TIP_TRASH', description: 'to throw tip into the trash or to not throw tip into the trash', - type: 'boolean', + type: 'bool', default: true, }, { @@ -231,7 +231,7 @@ export const useRunTimeParameters = ( displayName: 'Deactivate Temperatures', variableName: 'DEACTIVATE_TEMP', description: 'deactivate temperature on the module', - type: 'boolean', + type: 'bool', default: true, }, { diff --git a/components/src/molecules/ParametersTable/ParametersTable.stories.tsx b/components/src/molecules/ParametersTable/ParametersTable.stories.tsx index ce55f700dc3..93ba92cfdd4 100644 --- a/components/src/molecules/ParametersTable/ParametersTable.stories.tsx +++ b/components/src/molecules/ParametersTable/ParametersTable.stories.tsx @@ -17,7 +17,7 @@ const runTimeParameters: RunTimeParameter[] = [ displayName: 'Dry Run', variableName: 'DRYRUN', description: 'Is this a dry or wet run? Wet is true, dry is false', - type: 'boolean', + type: 'bool', default: false, }, { @@ -25,7 +25,7 @@ const runTimeParameters: RunTimeParameter[] = [ displayName: 'Use Gripper', variableName: 'USE_GRIPPER', description: 'For using the gripper.', - type: 'boolean', + type: 'bool', default: true, }, { @@ -34,7 +34,7 @@ const runTimeParameters: RunTimeParameter[] = [ variableName: 'TIP_TRASH', description: 'to throw tip into the trash or to not throw tip into the trash', - type: 'boolean', + type: 'bool', default: true, }, { @@ -42,7 +42,7 @@ const runTimeParameters: RunTimeParameter[] = [ displayName: 'Deactivate Temperatures', variableName: 'DEACTIVATE_TEMP', description: 'deactivate temperature on the module', - type: 'boolean', + type: 'bool', default: true, }, { diff --git a/components/src/molecules/ParametersTable/__tests__/ParametersTable.test.tsx b/components/src/molecules/ParametersTable/__tests__/ParametersTable.test.tsx index 1c9cd2d571c..6a4fe44bff0 100644 --- a/components/src/molecules/ParametersTable/__tests__/ParametersTable.test.tsx +++ b/components/src/molecules/ParametersTable/__tests__/ParametersTable.test.tsx @@ -13,7 +13,7 @@ const mockRunTimeParameter: RunTimeParameter[] = [ variableName: 'TIP_TRASH', description: 'to throw tip into the trash or to not throw tip into the trash', - type: 'boolean', + type: 'bool', default: true, value: true, }, diff --git a/components/src/molecules/ParametersTable/index.tsx b/components/src/molecules/ParametersTable/index.tsx index 4ff5cdeeb18..358b09c65c0 100644 --- a/components/src/molecules/ParametersTable/index.tsx +++ b/components/src/molecules/ParametersTable/index.tsx @@ -32,7 +32,7 @@ export function ParametersTable({ case 'int': case 'float': return minMax - case 'boolean': + case 'bool': return t != null ? t('on_off') : 'On, off' case 'str': if (count > 2) { diff --git a/shared-data/js/helpers/__tests__/formatRunTimeParameterValue.test.ts b/shared-data/js/helpers/__tests__/formatRunTimeParameterValue.test.ts index bfdad493913..fec7bd7f4b2 100644 --- a/shared-data/js/helpers/__tests__/formatRunTimeParameterValue.test.ts +++ b/shared-data/js/helpers/__tests__/formatRunTimeParameterValue.test.ts @@ -70,7 +70,7 @@ describe('utils-formatRunTimeParameterValue', () => { displayName: 'Deactivate Temperatures', variableName: 'DEACTIVATE_TEMP', description: 'deactivate temperature on the module', - type: 'boolean', + type: 'bool', default: true, } as RunTimeParameter const result = formatRunTimeParameterValue(mockData, mockTFunction) @@ -83,7 +83,7 @@ describe('utils-formatRunTimeParameterValue', () => { displayName: 'Dry Run', variableName: 'DRYRUN', description: 'Is this a dry or wet run? Wet is true, dry is false', - type: 'boolean', + type: 'bool', default: false, } as RunTimeParameter const result = formatRunTimeParameterValue(mockData, mockTFunction) diff --git a/shared-data/js/helpers/formatRunTimeParameterValue.ts b/shared-data/js/helpers/formatRunTimeParameterValue.ts index ffbab087849..ed154bbcf8a 100644 --- a/shared-data/js/helpers/formatRunTimeParameterValue.ts +++ b/shared-data/js/helpers/formatRunTimeParameterValue.ts @@ -15,7 +15,7 @@ export const formatRunTimeParameterValue = ( return suffix !== null ? `${defaultValue.toString()} ${suffix}` : defaultValue.toString() - case 'boolean': + case 'bool': if (t != null) { return Boolean(defaultValue) ? t('on') : t('off') } else { diff --git a/shared-data/js/types.ts b/shared-data/js/types.ts index 12e991ce7f3..13fa4491a43 100644 --- a/shared-data/js/types.ts +++ b/shared-data/js/types.ts @@ -614,7 +614,7 @@ interface BooleanParameter { } type NumberParameterType = 'int' | 'float' -type BooleanParameterType = 'boolean' +type BooleanParameterType = 'bool' type StringParameterType = 'str' type RunTimeParameterType = | NumberParameter From 8ee0268ba7d722469810ad3382c822bef2411175 Mon Sep 17 00:00:00 2001 From: koji Date: Tue, 2 Apr 2024 13:15:05 -0400 Subject: [PATCH 021/194] fix(app): fix storybook build error (#14780) * fix(app): fix storybook build error --- .../NumericalKeyboard/NumericalKeyboard.stories.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/NumericalKeyboard.stories.tsx b/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/NumericalKeyboard.stories.tsx index 710750697ff..3bd55835b85 100644 --- a/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/NumericalKeyboard.stories.tsx +++ b/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/NumericalKeyboard.stories.tsx @@ -4,8 +4,8 @@ import { Flex, POSITION_ABSOLUTE, SPACING, + VIEWPORT, } from '@opentrons/components' -import { touchScreenViewport } from '../../../DesignTokens/constants' import { InputField } from '../../InputField' import { NumericalKeyboard } from '.' import '../index.css' @@ -16,7 +16,7 @@ import type { Story, Meta } from '@storybook/react' export default { title: 'ODD/Atoms/SoftwareKeyboard/NumericalKeyboard', component: NumericalKeyboard, - parameters: touchScreenViewport, + parameters: VIEWPORT.touchScreenViewport, argTypes: { isDecimal: { control: { From 5efb48d99544879b94e02e402f36b5818c481b63 Mon Sep 17 00:00:00 2001 From: Jethary Rader <66035149+jerader@users.noreply.github.com> Date: Tue, 2 Apr 2024 15:26:33 -0400 Subject: [PATCH 022/194] feat(app, shared-data): create odd boolean and choice selection screen (#14775) closes AUTH-123 --- .../localization/en/protocol_setup.json | 2 + .../ProtocolRunRunTimeParameters.tsx | 5 +- .../ProtocolSetupParameters/ChooseEnum.tsx | 87 +++++++++++++++++++ .../ViewOnlyParameters.tsx | 4 +- ....test.tsx => AnalysisFailedModal.test.tsx} | 0 .../__tests__/ChooseEnum.test.tsx | 79 +++++++++++++++++ .../ProtocolSetupParameters.test.tsx | 15 ++++ .../ProtocolSetupParameters/index.tsx | 72 ++++++++++++--- app/src/pages/ProtocolDetails/Parameters.tsx | 4 +- .../src/molecules/ParametersTable/index.tsx | 4 +- .../formatRunTimeParameterValue.test.ts | 14 +-- .../formatRunTimeParameterDefaultValue.ts | 36 ++++++++ .../js/helpers/formatRunTimeParameterValue.ts | 21 ++--- shared-data/js/helpers/index.ts | 1 + 14 files changed, 304 insertions(+), 40 deletions(-) create mode 100644 app/src/organisms/ProtocolSetupParameters/ChooseEnum.tsx rename app/src/organisms/ProtocolSetupParameters/__tests__/{AnalysisFailedModa.test.tsx => AnalysisFailedModal.test.tsx} (100%) create mode 100644 app/src/organisms/ProtocolSetupParameters/__tests__/ChooseEnum.test.tsx create mode 100644 shared-data/js/helpers/formatRunTimeParameterDefaultValue.ts diff --git a/app/src/assets/localization/en/protocol_setup.json b/app/src/assets/localization/en/protocol_setup.json index 371ce03a791..99b496a3479 100644 --- a/app/src/assets/localization/en/protocol_setup.json +++ b/app/src/assets/localization/en/protocol_setup.json @@ -38,6 +38,7 @@ "calibration_status": "calibration status", "calibration": "Calibration", "cancel_and_restart_to_edit": "Cancel the run and restart setup to edit", + "choose_enum": "Choose {{displayName}}", "closing": "Closing...", "complete_setup_before_proceeding": "complete setup before continuing run", "configure": "Configure", @@ -161,6 +162,7 @@ "must_have_labware_and_pip": "Protocol must load labware and a pipette", "n_a": "N/A", "name": "Name", + "no_custom_values": "No custom values specified", "no_data": "no data", "no_labware_offset_data": "no labware offset data yet", "no_modules_or_fixtures": "No modules or fixtures are specified for this protocol.", diff --git a/app/src/organisms/Devices/ProtocolRun/ProtocolRunRunTimeParameters.tsx b/app/src/organisms/Devices/ProtocolRun/ProtocolRunRunTimeParameters.tsx index d16d7b8b8cb..af94400b80f 100644 --- a/app/src/organisms/Devices/ProtocolRun/ProtocolRunRunTimeParameters.tsx +++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunRunTimeParameters.tsx @@ -1,7 +1,7 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' import styled from 'styled-components' - +import { formatRunTimeParameterDefaultValue } from '@opentrons/shared-data' import { ALIGN_CENTER, BORDERS, @@ -17,7 +17,6 @@ import { useHoverTooltip, Icon, } from '@opentrons/components' -import { formatRunTimeParameterValue } from '@opentrons/shared-data' import { Banner } from '../../../atoms/Banner' import { Divider } from '../../../atoms/structure' @@ -151,7 +150,7 @@ const StyledTableRowComponent = ( - {formatRunTimeParameterValue(parameter, t)} + {formatRunTimeParameterDefaultValue(parameter, t)} {parameter.value !== parameter.default ? ( void + parameter: RunTimeParameter + setParameter: (value: boolean | string | number, variableName: string) => void + rawValue: number | string | boolean +} + +export function ChooseEnum({ + handleGoBack, + parameter, + setParameter, + rawValue, +}: ChooseEnumProps): JSX.Element { + const { makeSnackbar } = useToaster() + + const { t } = useTranslation(['protocol_setup', 'shared']) + if (parameter.type !== 'str') { + console.error( + `parameter type is expected to be a string for parameter ${parameter.displayName}` + ) + } + const options = parameter.type === 'str' ? parameter.choices : undefined + const handleOnClick = (newValue: string | number | boolean): void => { + setParameter(newValue, parameter.variableName) + } + const resetValueDisabled = parameter.default === rawValue + + return ( + <> + + resetValueDisabled + ? makeSnackbar(t('no_custom_values')) + : setParameter(parameter.default, parameter.variableName) + } + /> + + + {parameter.description} + + + {options?.map(option => { + return ( + handleOnClick(option.value)} + isSelected={option.value === rawValue} + /> + ) + })} + + + ) +} diff --git a/app/src/organisms/ProtocolSetupParameters/ViewOnlyParameters.tsx b/app/src/organisms/ProtocolSetupParameters/ViewOnlyParameters.tsx index e8aca7d8c9c..09dcaf26c47 100644 --- a/app/src/organisms/ProtocolSetupParameters/ViewOnlyParameters.tsx +++ b/app/src/organisms/ProtocolSetupParameters/ViewOnlyParameters.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' -import { formatRunTimeParameterValue } from '@opentrons/shared-data' +import { formatRunTimeParameterDefaultValue } from '@opentrons/shared-data' import { ALIGN_CENTER, BORDERS, @@ -94,7 +94,7 @@ export function ViewOnlyParameters({ gridGap={SPACING.spacing8} > - {formatRunTimeParameterValue(parameter, t)} + {formatRunTimeParameterDefaultValue(parameter, t)} {hasCustomValue ? ( ) => { + return renderWithProviders(, { + i18nInstance: i18n, + }) +} +describe('ChooseEnum', () => { + let props: React.ComponentProps + + beforeEach(() => { + props = { + setParameter: vi.fn(), + handleGoBack: vi.fn(), + parameter: { + displayName: 'Default Module Offsets', + variableName: 'DEFAULT_OFFSETS', + value: 'none', + description: '', + type: 'str', + choices: [ + { + displayName: 'no offsets', + value: 'none', + }, + { + displayName: 'temp offset', + value: '1', + }, + { + displayName: 'heater-shaker offset', + value: '2', + }, + ], + default: 'none', + }, + rawValue: '1', + } + }) + it('renders the back icon and calls the prop', () => { + render(props) + fireEvent.click(screen.getAllByRole('button')[0]) + expect(props.handleGoBack).toHaveBeenCalled() + }) + it('calls the prop if reset default is clicked when the default has changed', () => { + render(props) + fireEvent.click(screen.getByText('Restore default values')) + expect(props.setParameter).toHaveBeenCalled() + }) + it('calls does not call prop if reset default is clicked when the default has not changed', () => { + props = { + ...props, + rawValue: 'none', + } + render(props) + fireEvent.click(screen.getByText('Restore default values')) + expect(props.setParameter).not.toHaveBeenCalled() + }) + it('should render the text and buttons for choice param', () => { + render(props) + screen.getByText('no offsets') + screen.getByText('temp offset') + screen.getByText('heater-shaker offset') + const notSelectedOption = screen.getByRole('label', { name: 'no offsets' }) + const selectedOption = screen.getByRole('label', { + name: 'temp offset', + }) + expect(notSelectedOption).toHaveStyle(`background-color: ${COLORS.blue40}`) + expect(selectedOption).toHaveStyle(`background-color: ${COLORS.blue60}`) + }) +}) diff --git a/app/src/organisms/ProtocolSetupParameters/__tests__/ProtocolSetupParameters.test.tsx b/app/src/organisms/ProtocolSetupParameters/__tests__/ProtocolSetupParameters.test.tsx index 4873745356c..1dc55314d59 100644 --- a/app/src/organisms/ProtocolSetupParameters/__tests__/ProtocolSetupParameters.test.tsx +++ b/app/src/organisms/ProtocolSetupParameters/__tests__/ProtocolSetupParameters.test.tsx @@ -6,12 +6,14 @@ import { useCreateRunMutation, useHost } from '@opentrons/react-api-client' import { i18n } from '../../../i18n' import { renderWithProviders } from '../../../__testing-utils__' import { ProtocolSetupParameters } from '..' +import { ChooseEnum } from '../ChooseEnum' import { mockRunTimeParameterData } from '../../../pages/ProtocolDetails/fixtures' import type * as ReactRouterDom from 'react-router-dom' import type { HostConfig } from '@opentrons/api-client' const mockGoBack = vi.fn() +vi.mock('../ChooseEnum') vi.mock('@opentrons/react-api-client') vi.mock('../../LabwarePositionCheck/useMostRecentCompletedAnalysis') vi.mock('react-router-dom', async importOriginal => { @@ -39,6 +41,7 @@ describe('ProtocolSetupParameters', () => { labwareOffsets: [], runTimeParameters: mockRunTimeParameterData, } + vi.mocked(ChooseEnum).mockReturnValue(
mock ChooseEnum
) vi.mocked(useHost).mockReturnValue(MOCK_HOST_CONFIG) when(vi.mocked(useCreateRunMutation)) .calledWith(expect.anything()) @@ -52,6 +55,18 @@ describe('ProtocolSetupParameters', () => { screen.getByText('Dry Run') screen.getByText('a dry run description') }) + it('renders the ChooseEnum component when a str param is selected', () => { + render(props) + fireEvent.click(screen.getByText('Default Module Offsets')) + screen.getByText('mock ChooseEnum') + }) + it('renders the other setting when boolean param is selected', () => { + render(props) + screen.getByText('Off') + expect(screen.getAllByText('On')).toHaveLength(3) + fireEvent.click(screen.getByText('Dry Run')) + expect(screen.getAllByText('On')).toHaveLength(4) + }) it('renders the back icon and calls useHistory', () => { render(props) fireEvent.click(screen.getAllByRole('button')[0]) diff --git a/app/src/organisms/ProtocolSetupParameters/index.tsx b/app/src/organisms/ProtocolSetupParameters/index.tsx index a3cc0687b17..1312844b2ab 100644 --- a/app/src/organisms/ProtocolSetupParameters/index.tsx +++ b/app/src/organisms/ProtocolSetupParameters/index.tsx @@ -14,6 +14,7 @@ import { formatRunTimeParameterValue } from '@opentrons/shared-data' import { ProtocolSetupStep } from '../../pages/ProtocolSetup' import { ChildNavigation } from '../ChildNavigation' import { ResetValuesModal } from './ResetValuesModal' +import { ChooseEnum } from './ChooseEnum' import type { RunTimeParameter } from '@opentrons/shared-data' import type { LabwareOffsetCreateData } from '@opentrons/api-client' @@ -176,6 +177,10 @@ export function ProtocolSetupParameters({ const history = useHistory() const host = useHost() const queryClient = useQueryClient() + const [ + chooseValueScreen, + setChooseValueScreen, + ] = React.useState(null) const [resetValuesModal, showResetValuesModal] = React.useState( false ) @@ -187,6 +192,30 @@ export function ProtocolSetupParameters({ runTimeParametersOverrides, setRunTimeParametersOverrides, ] = React.useState(parameters) + + const updateParameters = ( + value: boolean | string | number, + variableName: string + ): void => { + const updatedParameters = parameters.map(parameter => { + if (parameter.variableName === variableName) { + return { ...parameter, value } + } + return parameter + }) + setRunTimeParametersOverrides(updatedParameters) + if (chooseValueScreen && chooseValueScreen.variableName === variableName) { + const updatedParameter = updatedParameters.find( + parameter => parameter.variableName === variableName + ) + if (updatedParameter != null) { + setChooseValueScreen(updatedParameter) + } + } + } + + // TODO(jr, 3/20/24): modify useCreateRunMutation to take in optional run time parameters + // newRunTimeParameters will be the param to plug in! const { createRun, isLoading } = useCreateRunMutation({ onSuccess: data => { queryClient @@ -199,17 +228,8 @@ export function ProtocolSetupParameters({ const handleConfirmValues = (): void => { createRun({ protocolId, labwareOffsets }) } - - return ( + let children = ( <> - {resetValuesModal ? ( - showResetValuesModal(false)} - /> - ) : null} - history.goBack()} @@ -230,14 +250,18 @@ export function ProtocolSetupParameters({ gridGap={SPACING.spacing8} paddingX={SPACING.spacing40} > - {parameters.map((parameter, index) => { + {runTimeParametersOverrides.map((parameter, index) => { return ( console.log('TODO: wire this up')} + onClickSetupStep={() => + parameter.type === 'bool' + ? updateParameters(!parameter.value, parameter.variableName) + : setChooseValueScreen(parameter) + } detail={formatRunTimeParameterValue(parameter, t)} description={parameter.description} fontSize="h4" @@ -248,4 +272,28 @@ export function ProtocolSetupParameters({
) + if (chooseValueScreen != null && chooseValueScreen.type === 'str') { + children = ( + setChooseValueScreen(null)} + parameter={chooseValueScreen} + setParameter={updateParameters} + rawValue={chooseValueScreen.value} + /> + ) + } + // TODO(jr, 4/1/24): add the int/float component + + return ( + <> + {resetValuesModal ? ( + showResetValuesModal(false)} + /> + ) : null} + {children} + + ) } diff --git a/app/src/pages/ProtocolDetails/Parameters.tsx b/app/src/pages/ProtocolDetails/Parameters.tsx index c43b56d7242..b8cbfa71155 100644 --- a/app/src/pages/ProtocolDetails/Parameters.tsx +++ b/app/src/pages/ProtocolDetails/Parameters.tsx @@ -1,7 +1,7 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' import styled from 'styled-components' -import { formatRunTimeParameterValue } from '@opentrons/shared-data' +import { formatRunTimeParameterDefaultValue } from '@opentrons/shared-data' import { BORDERS, COLORS, @@ -118,7 +118,7 @@ export const Parameters = (props: { protocolId: string }): JSX.Element => { - {formatRunTimeParameterValue(parameter, t)} + {formatRunTimeParameterDefaultValue(parameter, t)} diff --git a/components/src/molecules/ParametersTable/index.tsx b/components/src/molecules/ParametersTable/index.tsx index 358b09c65c0..671646f19d0 100644 --- a/components/src/molecules/ParametersTable/index.tsx +++ b/components/src/molecules/ParametersTable/index.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import styled from 'styled-components' -import { formatRunTimeParameterValue } from '@opentrons/shared-data' +import { formatRunTimeParameterDefaultValue } from '@opentrons/shared-data' import { BORDERS } from '../../helix-design-system' import { SPACING, TYPOGRAPHY } from '../../ui-style-constants/index' import { StyledText } from '../../atoms/StyledText' @@ -69,7 +69,7 @@ export function ParametersTable({
- {formatRunTimeParameterValue(parameter, t)} + {formatRunTimeParameterDefaultValue(parameter, t)} diff --git a/shared-data/js/helpers/__tests__/formatRunTimeParameterValue.test.ts b/shared-data/js/helpers/__tests__/formatRunTimeParameterValue.test.ts index fec7bd7f4b2..a405d5845d3 100644 --- a/shared-data/js/helpers/__tests__/formatRunTimeParameterValue.test.ts +++ b/shared-data/js/helpers/__tests__/formatRunTimeParameterValue.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect, vi } from 'vitest' -import { formatRunTimeParameterValue } from '../formatRunTimeParameterValue' +import { formatRunTimeParameterDefaultValue } from '../formatRunTimeParameterDefaultValue' import type { RunTimeParameter } from '../../types' @@ -9,7 +9,7 @@ const capitalizeFirstLetter = (str: string): string => { const mockTFunction = vi.fn(str => capitalizeFirstLetter(str)) -describe('utils-formatRunTimeParameterValue', () => { +describe('utils-formatRunTimeParameterDefaultValue', () => { it('should return value with suffix when type is int', () => { const mockData = { value: 6, @@ -21,7 +21,7 @@ describe('utils-formatRunTimeParameterValue', () => { max: 10, default: 6, } as RunTimeParameter - const result = formatRunTimeParameterValue(mockData, mockTFunction) + const result = formatRunTimeParameterDefaultValue(mockData, mockTFunction) expect(result).toEqual('6') }) @@ -37,7 +37,7 @@ describe('utils-formatRunTimeParameterValue', () => { max: 10.0, default: 6.5, } as RunTimeParameter - const result = formatRunTimeParameterValue(mockData, mockTFunction) + const result = formatRunTimeParameterDefaultValue(mockData, mockTFunction) expect(result).toEqual('6.5 mL') }) @@ -60,7 +60,7 @@ describe('utils-formatRunTimeParameterValue', () => { ], default: 'left', } as RunTimeParameter - const result = formatRunTimeParameterValue(mockData, mockTFunction) + const result = formatRunTimeParameterDefaultValue(mockData, mockTFunction) expect(result).toEqual('Left') }) @@ -73,7 +73,7 @@ describe('utils-formatRunTimeParameterValue', () => { type: 'bool', default: true, } as RunTimeParameter - const result = formatRunTimeParameterValue(mockData, mockTFunction) + const result = formatRunTimeParameterDefaultValue(mockData, mockTFunction) expect(result).toEqual('On') }) @@ -86,7 +86,7 @@ describe('utils-formatRunTimeParameterValue', () => { type: 'bool', default: false, } as RunTimeParameter - const result = formatRunTimeParameterValue(mockData, mockTFunction) + const result = formatRunTimeParameterDefaultValue(mockData, mockTFunction) expect(result).toEqual('Off') }) }) diff --git a/shared-data/js/helpers/formatRunTimeParameterDefaultValue.ts b/shared-data/js/helpers/formatRunTimeParameterDefaultValue.ts new file mode 100644 index 00000000000..78de4e78f02 --- /dev/null +++ b/shared-data/js/helpers/formatRunTimeParameterDefaultValue.ts @@ -0,0 +1,36 @@ +import type { RunTimeParameter } from '../types' + +export const formatRunTimeParameterDefaultValue = ( + runTimeParameter: RunTimeParameter, + t?: any +): string => { + const { type, default: defaultValue } = runTimeParameter + const suffix = + 'suffix' in runTimeParameter && runTimeParameter.suffix != null + ? runTimeParameter.suffix + : null + switch (type) { + case 'int': + case 'float': + return suffix !== null + ? `${defaultValue.toString()} ${suffix}` + : defaultValue.toString() + case 'bool': + if (t != null) { + return Boolean(defaultValue) ? t('on') : t('off') + } else { + return Boolean(defaultValue) ? 'On' : 'Off' + } + case 'str': + if ('choices' in runTimeParameter && runTimeParameter.choices != null) { + const choice = runTimeParameter.choices.find( + choice => choice.value === defaultValue + ) + if (choice != null) { + return choice.displayName + } + } + break + } + return '' +} diff --git a/shared-data/js/helpers/formatRunTimeParameterValue.ts b/shared-data/js/helpers/formatRunTimeParameterValue.ts index ed154bbcf8a..0aa0b72a194 100644 --- a/shared-data/js/helpers/formatRunTimeParameterValue.ts +++ b/shared-data/js/helpers/formatRunTimeParameterValue.ts @@ -1,10 +1,10 @@ -import { RunTimeParameter } from '../types' +import type { RunTimeParameter } from '../types' export const formatRunTimeParameterValue = ( runTimeParameter: RunTimeParameter, - t?: any + t: any ): string => { - const { type, default: defaultValue } = runTimeParameter + const { type, value } = runTimeParameter const suffix = 'suffix' in runTimeParameter && runTimeParameter.suffix != null ? runTimeParameter.suffix @@ -13,18 +13,15 @@ export const formatRunTimeParameterValue = ( case 'int': case 'float': return suffix !== null - ? `${defaultValue.toString()} ${suffix}` - : defaultValue.toString() - case 'bool': - if (t != null) { - return Boolean(defaultValue) ? t('on') : t('off') - } else { - return Boolean(defaultValue) ? 'On' : 'Off' - } + ? `${value.toString()} ${suffix}` + : value.toString() + case 'bool': { + return Boolean(value) ? t('on') : t('off') + } case 'str': if ('choices' in runTimeParameter && runTimeParameter.choices != null) { const choice = runTimeParameter.choices.find( - choice => choice.value === defaultValue + choice => choice.value === value ) if (choice != null) { return choice.displayName diff --git a/shared-data/js/helpers/index.ts b/shared-data/js/helpers/index.ts index 2d78f16ca1f..a65a83085de 100644 --- a/shared-data/js/helpers/index.ts +++ b/shared-data/js/helpers/index.ts @@ -28,6 +28,7 @@ export * from './getOccludedSlotCountForModule' export * from './labwareInference' export * from './getAddressableAreasInProtocol' export * from './getSimplestFlexDeckConfig' +export * from './formatRunTimeParameterDefaultValue' export * from './formatRunTimeParameterValue' export const getLabwareDefIsStandard = (def: LabwareDefinition2): boolean => From fc620165858b37fefb4a5262d7375959678e91fd Mon Sep 17 00:00:00 2001 From: Nick Diehl <47604184+ncdiehl11@users.noreply.github.com> Date: Tue, 2 Apr 2024 17:56:54 -0400 Subject: [PATCH 023/194] feat(app): add runtime parameters to ChooseProtocolSlideout (#14781) closes [AUTH-246](https://opentrons.atlassian.net/browse/AUTH-246) --- app/src/assets/localization/en/shared.json | 1 + .../ChooseProtocolSlideout/index.tsx | 318 ++++++++++++++++-- 2 files changed, 292 insertions(+), 27 deletions(-) diff --git a/app/src/assets/localization/en/shared.json b/app/src/assets/localization/en/shared.json index 8c8bed0a5af..adb939134f8 100644 --- a/app/src/assets/localization/en/shared.json +++ b/app/src/assets/localization/en/shared.json @@ -6,6 +6,7 @@ "before_you_begin": "Before you begin", "browse": "browse", "cancel": "cancel", + "change_protocol": "Change protocol", "change_robot": "Change robot", "clear_data": "clear data", "close_robot_door": "Close the robot door before starting the run.", diff --git a/app/src/organisms/ChooseProtocolSlideout/index.tsx b/app/src/organisms/ChooseProtocolSlideout/index.tsx index b6d1d2805ff..859b1ac4cd9 100644 --- a/app/src/organisms/ChooseProtocolSlideout/index.tsx +++ b/app/src/organisms/ChooseProtocolSlideout/index.tsx @@ -12,30 +12,42 @@ import { Box, COLORS, DIRECTION_COLUMN, + DIRECTION_ROW, DISPLAY_BLOCK, + DropdownOption, Flex, Icon, + Link as LinkComponent, JUSTIFY_CENTER, + JUSTIFY_END, + JUSTIFY_FLEX_START, OVERFLOW_WRAP_ANYWHERE, PrimaryButton, ProtocolDeck, - SIZE_1, SPACING, + SecondaryButton, StyledText, TYPOGRAPHY, + useHoverTooltip, } from '@opentrons/components' import { useLogger } from '../../logger' import { OPENTRONS_USB } from '../../redux/discovery' import { getStoredProtocols } from '../../redux/protocol-storage' import { appShellRequestor } from '../../redux/shell/remote' -import { Slideout } from '../../atoms/Slideout' +import { useFeatureFlag } from '../../redux/config' +import { MultiSlideout } from '../../atoms/Slideout/MultiSlideout' +import { Tooltip } from '../../atoms/Tooltip' +import { ToggleButton } from '../../atoms/buttons' +import { InputField } from '../../atoms/InputField' +import { DropdownMenu } from '../../atoms/MenuList/DropdownMenu' import { MiniCard } from '../../molecules/MiniCard' import { useTrackCreateProtocolRunEvent } from '../Devices/hooks' import { useCreateRunFromProtocol } from '../ChooseRobotToRunProtocolSlideout/useCreateRunFromProtocol' import { ApplyHistoricOffsets } from '../ApplyHistoricOffsets' import { useOffsetCandidatesForAnalysis } from '../ApplyHistoricOffsets/hooks/useOffsetCandidatesForAnalysis' import { getAnalysisStatus } from '../ProtocolsLanding/utils' +import type { RunTimeParameter } from '@opentrons/shared-data' import type { Robot } from '../../redux/discovery/types' import type { StoredProtocolData } from '../../redux/protocol-storage' import type { State } from '../../redux/types' @@ -65,6 +77,8 @@ export function ChooseProtocolSlideoutComponent( const { t } = useTranslation(['device_details', 'shared']) const history = useHistory() const logger = useLogger(new URL('', import.meta.url).pathname) + const [targetProps, tooltipProps] = useHoverTooltip() + const { robot, showSlideout, onCloseClick } = props const { name } = robot @@ -72,6 +86,24 @@ export function ChooseProtocolSlideoutComponent( selectedProtocol, setSelectedProtocol, ] = React.useState(null) + const [ + runTimeParametersOverrides, + setRunTimeParametersOverrides, + ] = React.useState([]) + const [currentPage, setCurrentPage] = React.useState(1) + const enableRunTimeParametersFF = useFeatureFlag('enableRunTimeParameters') + + React.useEffect(() => { + setRunTimeParametersOverrides( + selectedProtocol?.mostRecentAnalysis?.runTimeParameters ?? [] + ) + }, [selectedProtocol]) + const runTimeParametersFromAnalysis = + selectedProtocol?.mostRecentAnalysis?.runTimeParameters ?? [] + + const hasRunTimeParameters = + enableRunTimeParametersFF && runTimeParametersFromAnalysis.length > 0 + const analysisStatus = getAnalysisStatus( false, selectedProtocol?.mostRecentAnalysis @@ -128,7 +160,14 @@ export function ChooseProtocolSlideoutComponent( location, definitionUri, })) - : [] + : [], + runTimeParametersOverrides.reduce( + (acc, param) => + param.value !== param.default + ? { ...acc, [param.variableName]: param.value } + : acc, + {} + ) ) const handleProceed: React.MouseEventHandler = () => { if (selectedProtocol != null) { @@ -141,10 +180,226 @@ export function ChooseProtocolSlideoutComponent( logger.warn('failed to create protocol, no protocol selected') } } + + const isRestoreDefaultsLinkEnabled = + runTimeParametersOverrides?.some( + parameter => parameter.value !== parameter.default + ) ?? false + + const runTimeParametersInputs = + runTimeParametersOverrides?.map((runtimeParam, index) => { + if ('choices' in runtimeParam) { + const dropdownOptions = runtimeParam.choices.map(choice => { + return { name: choice.displayName, value: choice.value } + }) as DropdownOption[] + return ( + { + return choice.value === runtimeParam.value + }) ?? dropdownOptions[0] + } + onClick={choice => { + const clone = runTimeParametersOverrides.map((parameter, i) => { + if (i === index) { + return { + ...parameter, + value: + dropdownOptions.find(option => option.value === choice) + ?.value ?? parameter.default, + } + } + return parameter + }) + setRunTimeParametersOverrides(clone) + }} + title={runtimeParam.displayName} + caption={runtimeParam.description} + width="100%" + dropdownType="neutral" + /> + ) + } else if (runtimeParam.type === 'int' || runtimeParam.type === 'float') { + const value = runtimeParam.value as number + const id = `InputField_${runtimeParam.variableName}_${index.toString()}` + const error = + Number.isNaN(value) || + value < runtimeParam.min || + value > runtimeParam.max + ? t(`protocol_details:value_out_of_range`, { + min: + runtimeParam.type === 'int' + ? runtimeParam.min + : runtimeParam.min.toFixed(1), + max: + runtimeParam.type === 'int' + ? runtimeParam.max + : runtimeParam.max.toFixed(1), + }) + : null + return ( + { + const clone = runTimeParametersOverrides.map((parameter, i) => { + if (i === index) { + return { + ...parameter, + value: + runtimeParam.type === 'int' + ? Math.round(e.target.valueAsNumber) + : e.target.valueAsNumber, + } + } + return parameter + }) + setRunTimeParametersOverrides(clone) + }} + /> + ) + } else if (runtimeParam.type === 'bool') { + return ( + + + {runtimeParam.displayName} + + + { + const clone = runTimeParametersOverrides.map( + (parameter, i) => { + if (i === index) { + return { + ...parameter, + value: !parameter.value, + } + } + return parameter + } + ) + setRunTimeParametersOverrides(clone) + }} + height="0.813rem" + label={ + runtimeParam.value + ? t('protocol_details:on') + : t('protocol_details:off') + } + paddingTop={SPACING.spacing2} // manual alignment of SVG with value label + /> + + {runtimeParam.value + ? t('protocol_details:on') + : t('protocol_details:off')} + + + + {runtimeParam.description} + + + ) + } + }) ?? null + + const pageTwoBody = ( + + + { + const clone = runTimeParametersOverrides.map(parameter => ({ + ...parameter, + value: parameter.default, + })) + setRunTimeParametersOverrides(clone) + }} + paddingBottom={SPACING.spacing10} + {...targetProps} + > + {t('protocol_details:restore_defaults')} + + {!isRestoreDefaultsLinkEnabled && ( + + {t('protocol_details:no_custom_values')} + + )} + + + {runTimeParametersInputs} + + + ) + + const singlePageFooter = ( + + {isCreatingRun ? ( + + ) : ( + t('shared:proceed_to_setup') + )} + + ) + + const multiPageFooter = + currentPage === 1 ? ( + setCurrentPage(2)} + width="100%" + disabled={isCreatingRun || selectedProtocol == null} + > + {t('shared:continue_to_param')} + + ) : ( + + setCurrentPage(1)} width="51%"> + {t('shared:change_protocol')} + + + {isCreatingRun ? ( + + ) : ( + t('shared:confirm_values') + )} + + + ) + return ( - - - {isCreatingRun ? ( - - ) : ( - t('shared:proceed_to_setup') - )} - + {hasRunTimeParameters ? multiPageFooter : singlePageFooter} } > {showSlideout ? ( - { - if (!isCreatingRun) { - resetCreateRun() - setSelectedProtocol(storedProtocol) - } - }} - robotName={robot.name} - {...{ selectedProtocol, runCreationError, runCreationErrorCode }} - /> + currentPage === 1 ? ( + { + if (!isCreatingRun) { + resetCreateRun() + setSelectedProtocol(storedProtocol) + } + }} + robotName={robot.name} + {...{ selectedProtocol, runCreationError, runCreationErrorCode }} + /> + ) : ( + pageTwoBody + ) ) : null} - + ) } @@ -225,7 +474,7 @@ function StoredProtocolList(props: StoredProtocolListProps): JSX.Element { runCreationErrorCode, robotName, } = props - const { t } = useTranslation(['device_details', 'shared']) + const { t } = useTranslation(['device_details', 'protocol_details', 'shared']) const storedProtocols = useSelector((state: State) => getStoredProtocols(state) ) @@ -401,3 +650,18 @@ function StoredProtocolList(props: StoredProtocolListProps): JSX.Element {
) } + +const ENABLED_LINK_CSS = css` + ${TYPOGRAPHY.linkPSemiBold} + cursor: pointer; +` + +const DISABLED_LINK_CSS = css` + ${TYPOGRAPHY.linkPSemiBold} + color: ${COLORS.grey40}; + cursor: default; + + &:hover { + color: ${COLORS.grey40}; + } +` From a845ad01ac69b5a89e5fbaae6db71f793bce8eb1 Mon Sep 17 00:00:00 2001 From: Jamey Huffnagle Date: Wed, 3 Apr 2024 10:38:24 -0400 Subject: [PATCH 024/194] fix(app): fix excessive /runs network requests (#14783) Closes EXEC-255 UseNotifyService utilizes hostname instead of the host object, preventing a pass by reference issue causing excessive requests sent to /runs that often occurs on the RobotDetails page. Let's also ensure that if a robot loses connection while a component has passed a callback to appShellListener, we ensure we remove the correct callback from the store. --- .../resources/__tests__/useNotifyService.test.ts | 16 ++++++++++++++++ app/src/resources/useNotifyService.ts | 6 ++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/app/src/resources/__tests__/useNotifyService.test.ts b/app/src/resources/__tests__/useNotifyService.test.ts index 1e2ba78c744..32dad607a75 100644 --- a/app/src/resources/__tests__/useNotifyService.test.ts +++ b/app/src/resources/__tests__/useNotifyService.test.ts @@ -184,4 +184,20 @@ describe('useNotifyService', () => { unmount() expect(appShellListener).toHaveBeenCalled() }) + + it('should still clean up the listener if the hostname changes to null after subscribing', () => { + const { unmount, rerender } = renderHook(() => + useNotifyService({ + hostOverride: MOCK_HOST_CONFIG, + topic: MOCK_TOPIC, + setRefetchUsingHTTP: mockHTTPRefetch, + options: MOCK_OPTIONS, + }) + ) + rerender({ hostOverride: null }) + unmount() + expect(appShellListener).toHaveBeenCalledWith( + expect.objectContaining({ hostname: MOCK_HOST_CONFIG.hostname }) + ) + }) }) diff --git a/app/src/resources/useNotifyService.ts b/app/src/resources/useNotifyService.ts index f6cfaefa2b8..8068c2d4ade 100644 --- a/app/src/resources/useNotifyService.ts +++ b/app/src/resources/useNotifyService.ts @@ -43,6 +43,7 @@ export function useNotifyService({ const doTrackEvent = useTrackEvent() const isFlex = useIsFlex(host?.robotName ?? '') const hasUsedNotifyService = React.useRef(false) + const seenHostname = React.useRef(null) const { enabled, staleTime, forceHttpPolling } = options const shouldUseNotifications = @@ -62,6 +63,7 @@ export function useNotifyService({ }) dispatch(notifySubscribeAction(hostname, topic)) hasUsedNotifyService.current = true + seenHostname.current = hostname } else { setRefetchUsingHTTP('always') } @@ -69,14 +71,14 @@ export function useNotifyService({ return () => { if (hasUsedNotifyService.current) { appShellListener({ - hostname: hostname as string, + hostname: seenHostname.current as string, topic, callback: onDataEvent, isDismounting: true, }) } } - }, [topic, host, shouldUseNotifications]) + }, [topic, hostname, shouldUseNotifications]) function onDataEvent(data: NotifyResponseData): void { if (data === 'ECONNFAILED' || data === 'ECONNREFUSED') { From d803d78289da96ab82a33249dfaa2a50d810d2f6 Mon Sep 17 00:00:00 2001 From: Seth Foster Date: Wed, 3 Apr 2024 10:42:05 -0400 Subject: [PATCH 025/194] fix(api): set instrument cal tolerance to 4mm (#14769) This limit for the pipette offset consistency warning aligns better with the physical tolerance stackup of our pipettes and mounts. Closes EXEC-362 --- .../instruments/ot3/instrument_calibration.py | 2 +- .../instruments/test_instrument_calibration.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/api/src/opentrons/hardware_control/instruments/ot3/instrument_calibration.py b/api/src/opentrons/hardware_control/instruments/ot3/instrument_calibration.py index 7e7352170b9..b7eae1aa1fc 100644 --- a/api/src/opentrons/hardware_control/instruments/ot3/instrument_calibration.py +++ b/api/src/opentrons/hardware_control/instruments/ot3/instrument_calibration.py @@ -21,7 +21,7 @@ ) from opentrons.hardware_control.types import OT3Mount -PIPETTE_OFFSET_CONSISTENCY_LIMIT: Final = 1.5 +PIPETTE_OFFSET_CONSISTENCY_LIMIT: Final = 4.0 # These type aliases aid typechecking in tests that work the same on this and # the hardware_control.instruments.ot2 variant 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 6aa3ca2a009..d1f705d596f 100644 --- a/api/tests/opentrons/hardware_control/instruments/test_instrument_calibration.py +++ b/api/tests/opentrons/hardware_control/instruments/test_instrument_calibration.py @@ -134,9 +134,9 @@ def test_load_tip_length( (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), + (top_types.Point(0.1, -1, 4.3), top_types.Point(1.7, 0, 0.2), False), + (top_types.Point(0.1, -3.2, 1.5), top_types.Point(0.6, 0.9, 1.3), False), + (top_types.Point(0.1, -1, 1.5), top_types.Point(-0.2, -0.1, 6), False), ], ) def test_instrument_consistency_check_ot3( @@ -151,4 +151,4 @@ def test_instrument_consistency_check_ot3( top_types.Mount.LEFT: left, top_types.Mount.RIGHT: right, } - assert result[0].limit == 1.5 + assert result[0].limit == 4.0 From 23970442a926031627a65ee8492734580cf614db Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 3 Apr 2024 10:46:51 -0400 Subject: [PATCH 026/194] fix(app-testing): snapshot failure capture (#14786) This PR is an automated snapshot update request. Please review the changes and merge if they are acceptable or find you bug and fix it. Co-authored-by: y3rsh --- ...sis_snapshot[0f71566d05][OT2_P20S_None_2_7_Walkthrough].json | 2 +- ...t[20cefcac62][OT2_P300M_P20S_TC_HS_TM_2_13_SmokeTestV3].json | 2 +- ...ysis_snapshot[8df082e960][OT2_P300MLeft_MM_TM_2_4_Zymo].json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0f71566d05][OT2_P20S_None_2_7_Walkthrough].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0f71566d05][OT2_P20S_None_2_7_Walkthrough].json index a79130779de..8564dda276d 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0f71566d05][OT2_P20S_None_2_7_Walkthrough].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0f71566d05][OT2_P20S_None_2_7_Walkthrough].json @@ -3293,7 +3293,7 @@ "errorInfo": { "args": "()", "class": "AssertionError", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/protocol_engine.py\", line 442, in finish\n await exit_stack.aclose()\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 656, in aclose\n await self.__aexit__(None, None, None)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 714, in __aexit__\n raise exc_details[1]\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 697, in __aexit__\n cb_suppress = await cb(*exc_details)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 608, in _exit_wrapper\n await callback(*args, **kwds)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 102, in stop\n await p.teardown()\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 112, in teardown\n await self._action_dispatching_task\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 160, in _dispatch_all_actions\n self.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 37, in dispatch\n return self._action_dispatcher.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/actions/action_dispatcher.py\", line 30, in dispatch\n self._sink.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/state.py\", line 204, in handle_action\n substore.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/commands.py\", line 243, in handle_action\n self._state.command_history.set_command_running(running_command)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/command_history.py\", line 175, in set_command_running\n assert self.get_running_command() is None\n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/protocol_engine.py\", line 442, in finish\n await exit_stack.aclose()\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 656, in aclose\n await self.__aexit__(None, None, None)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 714, in __aexit__\n raise exc_details[1]\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 697, in __aexit__\n cb_suppress = await cb(*exc_details)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 608, in _exit_wrapper\n await callback(*args, **kwds)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 102, in stop\n await p.teardown()\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 112, in teardown\n await self._action_dispatching_task\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 160, in _dispatch_all_actions\n self.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 37, in dispatch\n return self._action_dispatcher.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/actions/action_dispatcher.py\", line 30, in dispatch\n self._sink.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/state.py\", line 207, in handle_action\n substore.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/commands.py\", line 243, in handle_action\n self._state.command_history.set_command_running(running_command)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/command_history.py\", line 175, in set_command_running\n assert self.get_running_command() is None\n" }, "errorType": "PythonException", "wrappedErrors": [] diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[20cefcac62][OT2_P300M_P20S_TC_HS_TM_2_13_SmokeTestV3].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[20cefcac62][OT2_P300M_P20S_TC_HS_TM_2_13_SmokeTestV3].json index d974b696058..ddce1f10c7f 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[20cefcac62][OT2_P300M_P20S_TC_HS_TM_2_13_SmokeTestV3].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[20cefcac62][OT2_P300M_P20S_TC_HS_TM_2_13_SmokeTestV3].json @@ -11889,7 +11889,7 @@ "errorInfo": { "args": "()", "class": "AssertionError", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/protocol_engine.py\", line 442, in finish\n await exit_stack.aclose()\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 656, in aclose\n await self.__aexit__(None, None, None)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 714, in __aexit__\n raise exc_details[1]\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 697, in __aexit__\n cb_suppress = await cb(*exc_details)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 608, in _exit_wrapper\n await callback(*args, **kwds)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 102, in stop\n await p.teardown()\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 112, in teardown\n await self._action_dispatching_task\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 160, in _dispatch_all_actions\n self.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 37, in dispatch\n return self._action_dispatcher.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/actions/action_dispatcher.py\", line 30, in dispatch\n self._sink.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/state.py\", line 204, in handle_action\n substore.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/commands.py\", line 243, in handle_action\n self._state.command_history.set_command_running(running_command)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/command_history.py\", line 175, in set_command_running\n assert self.get_running_command() is None\n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/protocol_engine.py\", line 442, in finish\n await exit_stack.aclose()\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 656, in aclose\n await self.__aexit__(None, None, None)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 714, in __aexit__\n raise exc_details[1]\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 697, in __aexit__\n cb_suppress = await cb(*exc_details)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 608, in _exit_wrapper\n await callback(*args, **kwds)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 102, in stop\n await p.teardown()\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 112, in teardown\n await self._action_dispatching_task\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 160, in _dispatch_all_actions\n self.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 37, in dispatch\n return self._action_dispatcher.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/actions/action_dispatcher.py\", line 30, in dispatch\n self._sink.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/state.py\", line 207, in handle_action\n substore.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/commands.py\", line 243, in handle_action\n self._state.command_history.set_command_running(running_command)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/command_history.py\", line 175, in set_command_running\n assert self.get_running_command() is None\n" }, "errorType": "PythonException", "wrappedErrors": [] diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8df082e960][OT2_P300MLeft_MM_TM_2_4_Zymo].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8df082e960][OT2_P300MLeft_MM_TM_2_4_Zymo].json index 02165d003c7..b32d3d55f65 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8df082e960][OT2_P300MLeft_MM_TM_2_4_Zymo].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8df082e960][OT2_P300MLeft_MM_TM_2_4_Zymo].json @@ -10913,7 +10913,7 @@ "errorInfo": { "args": "()", "class": "AssertionError", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/protocol_engine.py\", line 442, in finish\n await exit_stack.aclose()\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 656, in aclose\n await self.__aexit__(None, None, None)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 714, in __aexit__\n raise exc_details[1]\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 697, in __aexit__\n cb_suppress = await cb(*exc_details)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 608, in _exit_wrapper\n await callback(*args, **kwds)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 102, in stop\n await p.teardown()\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 112, in teardown\n await self._action_dispatching_task\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 160, in _dispatch_all_actions\n self.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 37, in dispatch\n return self._action_dispatcher.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/actions/action_dispatcher.py\", line 30, in dispatch\n self._sink.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/state.py\", line 204, in handle_action\n substore.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/commands.py\", line 243, in handle_action\n self._state.command_history.set_command_running(running_command)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/command_history.py\", line 175, in set_command_running\n assert self.get_running_command() is None\n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/protocol_engine.py\", line 442, in finish\n await exit_stack.aclose()\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 656, in aclose\n await self.__aexit__(None, None, None)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 714, in __aexit__\n raise exc_details[1]\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 697, in __aexit__\n cb_suppress = await cb(*exc_details)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 608, in _exit_wrapper\n await callback(*args, **kwds)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 102, in stop\n await p.teardown()\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 112, in teardown\n await self._action_dispatching_task\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 160, in _dispatch_all_actions\n self.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 37, in dispatch\n return self._action_dispatcher.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/actions/action_dispatcher.py\", line 30, in dispatch\n self._sink.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/state.py\", line 207, in handle_action\n substore.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/commands.py\", line 243, in handle_action\n self._state.command_history.set_command_running(running_command)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/command_history.py\", line 175, in set_command_running\n assert self.get_running_command() is None\n" }, "errorType": "PythonException", "wrappedErrors": [] From 6ccb243d8bb980b6fb3935839a68a95f6772b305 Mon Sep 17 00:00:00 2001 From: Nick Diehl <47604184+ncdiehl11@users.noreply.github.com> Date: Wed, 3 Apr 2024 11:08:01 -0400 Subject: [PATCH 027/194] feat(app, api-client): add optional runTimeParameterValues when cloning run (#14787) closes AUTH-257 --- api-client/src/runs/createRun.ts | 4 ++-- api-client/src/runs/types.ts | 3 ++- app/src/organisms/ChooseProtocolSlideout/index.tsx | 3 ++- .../useCreateRunFromProtocol.ts | 4 ++-- .../ProtocolUpload/hooks/__tests__/useCloneRun.test.tsx | 2 ++ app/src/organisms/ProtocolUpload/hooks/useCloneRun.ts | 8 ++++++-- 6 files changed, 16 insertions(+), 8 deletions(-) diff --git a/api-client/src/runs/createRun.ts b/api-client/src/runs/createRun.ts index 5b2883917c6..7f0fb1ad72d 100644 --- a/api-client/src/runs/createRun.ts +++ b/api-client/src/runs/createRun.ts @@ -5,13 +5,13 @@ import type { HostConfig } from '../types' import type { Run, LabwareOffsetCreateData, - RuntimeParameterCreateData, + RunTimeParameterCreateData, } from './types' export interface CreateRunData { protocolId?: string labwareOffsets?: LabwareOffsetCreateData[] - runTimeParameterValues?: RuntimeParameterCreateData + runTimeParameterValues?: RunTimeParameterCreateData } export function createRun( diff --git a/api-client/src/runs/types.ts b/api-client/src/runs/types.ts index 0be2a9973ed..7e6ec2b0ee7 100644 --- a/api-client/src/runs/types.ts +++ b/api-client/src/runs/types.ts @@ -47,6 +47,7 @@ export interface LegacyGoodRunData { modules: LoadedModule[] protocolId?: string labwareOffsets?: LabwareOffset[] + runTimeParameterValues?: RunTimeParameterCreateData } export interface KnownGoodRunData extends LegacyGoodRunData { @@ -125,7 +126,7 @@ export interface LabwareOffsetCreateData { vector: VectorOffset } -export interface RuntimeParameterCreateData { +export interface RunTimeParameterCreateData { [key: string]: string | boolean | number } diff --git a/app/src/organisms/ChooseProtocolSlideout/index.tsx b/app/src/organisms/ChooseProtocolSlideout/index.tsx index 859b1ac4cd9..6c1e11d9105 100644 --- a/app/src/organisms/ChooseProtocolSlideout/index.tsx +++ b/app/src/organisms/ChooseProtocolSlideout/index.tsx @@ -47,6 +47,7 @@ import { useCreateRunFromProtocol } from '../ChooseRobotToRunProtocolSlideout/us import { ApplyHistoricOffsets } from '../ApplyHistoricOffsets' import { useOffsetCandidatesForAnalysis } from '../ApplyHistoricOffsets/hooks/useOffsetCandidatesForAnalysis' import { getAnalysisStatus } from '../ProtocolsLanding/utils' +import type { RunTimeParameterCreateData } from '@opentrons/api-client' import type { RunTimeParameter } from '@opentrons/shared-data' import type { Robot } from '../../redux/discovery/types' import type { StoredProtocolData } from '../../redux/protocol-storage' @@ -161,7 +162,7 @@ export function ChooseProtocolSlideoutComponent( definitionUri, })) : [], - runTimeParametersOverrides.reduce( + runTimeParametersOverrides.reduce( (acc, param) => param.value !== param.default ? { ...acc, [param.variableName]: param.value } diff --git a/app/src/organisms/ChooseRobotToRunProtocolSlideout/useCreateRunFromProtocol.ts b/app/src/organisms/ChooseRobotToRunProtocolSlideout/useCreateRunFromProtocol.ts index 0e897881c5c..209e886fc29 100644 --- a/app/src/organisms/ChooseRobotToRunProtocolSlideout/useCreateRunFromProtocol.ts +++ b/app/src/organisms/ChooseRobotToRunProtocolSlideout/useCreateRunFromProtocol.ts @@ -14,7 +14,7 @@ import type { HostConfig, LabwareOffsetCreateData, Protocol, - RuntimeParameterCreateData, + RunTimeParameterCreateData, } from '@opentrons/api-client' import type { UseCreateRunMutationOptions } from '@opentrons/react-api-client/src/runs/useCreateRunMutation' import type { CreateProtocolVariables } from '@opentrons/react-api-client/src/protocols/useCreateProtocolMutation' @@ -37,7 +37,7 @@ export function useCreateRunFromProtocol( options: UseCreateRunMutationOptions, hostOverride?: HostConfig | null, labwareOffsets?: LabwareOffsetCreateData[], - runTimeParameterValues?: RuntimeParameterCreateData + runTimeParameterValues?: RunTimeParameterCreateData ): UseCreateRun { const contextHost = useHost() const host = diff --git a/app/src/organisms/ProtocolUpload/hooks/__tests__/useCloneRun.test.tsx b/app/src/organisms/ProtocolUpload/hooks/__tests__/useCloneRun.test.tsx index 4f4fb33ab00..af388d30930 100644 --- a/app/src/organisms/ProtocolUpload/hooks/__tests__/useCloneRun.test.tsx +++ b/app/src/organisms/ProtocolUpload/hooks/__tests__/useCloneRun.test.tsx @@ -30,6 +30,7 @@ describe('useCloneRun hook', () => { id: RUN_ID, protocolId: 'protocolId', labwareOffsets: 'someOffset', + runTimeParameterValues: 'someRtp', }, }, } as any) @@ -60,6 +61,7 @@ describe('useCloneRun hook', () => { expect(mockCreateRun).toHaveBeenCalledWith({ protocolId: 'protocolId', labwareOffsets: 'someOffset', + runTimeParameterValues: 'someRtp', }) }) }) diff --git a/app/src/organisms/ProtocolUpload/hooks/useCloneRun.ts b/app/src/organisms/ProtocolUpload/hooks/useCloneRun.ts index c7ba887ab54..0858544d93c 100644 --- a/app/src/organisms/ProtocolUpload/hooks/useCloneRun.ts +++ b/app/src/organisms/ProtocolUpload/hooks/useCloneRun.ts @@ -30,8 +30,12 @@ export function useCloneRun( }) const cloneRun = (): void => { if (runRecord != null) { - const { protocolId, labwareOffsets } = runRecord.data - createRun({ protocolId, labwareOffsets }) + const { + protocolId, + labwareOffsets, + runTimeParameterValues, + } = runRecord.data + createRun({ protocolId, labwareOffsets, runTimeParameterValues }) } else { console.info('failed to clone run record, source run record not found') } From 80abd2e0c8c4eee45820923140eb3b662e648fc7 Mon Sep 17 00:00:00 2001 From: Rhyann Clarke <146747548+rclarke0@users.noreply.github.com> Date: Wed, 3 Apr 2024 11:21:57 -0400 Subject: [PATCH 028/194] Automated ABR Calibration Data Uploading (#14782) # Overview Pulls Calibration Data from Robots and Uploads to google_drive/google_sheet # Test Plan Tested on ABR robots. Successfully pulls calibration data, uploads to google drive, and saves to google sheet. # Changelog - Adds abr_calibration_logs.py 1. Connects to google drive folder 2. Connects to google sheet 3. Pulls module, instrument, and deck calibration data and compiles into one .json file per robot via http requests 4. Uploads new files to google drive folder 5. adds new rows to instrument, module, and deck calibration sheets if the serial and calibration lastmodified timestamp pairing do not already exist - Split jira_tool up into a file with just jira_tools and a file that uses the tools with the robots. - For all scripts uploading to google drive, changed the folder_name argument to folder_id so that the service_account is writing to the correct folder. Adds email as argument to allow for permission sharing by service account. # Review requests # Risk assessment --- .../automation/google_drive_tool.py | 60 ++++- .../abr_testing/automation/jira_tool.py | 114 ---------- .../data_collection/abr_calibration_logs.py | 214 ++++++++++++++++++ .../data_collection/abr_google_drive.py | 23 +- .../data_collection/abr_robot_error.py | 165 ++++++++++++++ .../data_collection/get_run_logs.py | 13 +- .../data_collection/read_robot_logs.py | 67 +++++- abr-testing/abr_testing/tools/abr_scale.py | 23 +- 8 files changed, 514 insertions(+), 165 deletions(-) create mode 100644 abr-testing/abr_testing/data_collection/abr_calibration_logs.py create mode 100644 abr-testing/abr_testing/data_collection/abr_robot_error.py diff --git a/abr-testing/abr_testing/automation/google_drive_tool.py b/abr-testing/abr_testing/automation/google_drive_tool.py index 836ba2083b0..8b56d0390fe 100644 --- a/abr-testing/abr_testing/automation/google_drive_tool.py +++ b/abr-testing/abr_testing/automation/google_drive_tool.py @@ -1,6 +1,8 @@ """Google Drive Tool.""" import os -from typing import Set, Any +from typing import Set, Any, Optional +import webbrowser +import mimetypes from oauth2client.service_account import ServiceAccountCredentials # type: ignore[import] from googleapiclient.discovery import build from googleapiclient.http import MediaFileUpload @@ -14,15 +16,16 @@ class google_drive: """Google Drive Tool.""" - def __init__(self, credentials: Any, folder_name: str, parent_folder: Any) -> None: + def __init__(self, credentials: Any, folder_name: str, email: str) -> None: """Connects to google drive via credentials file.""" self.scope = ["https://www.googleapis.com/auth/drive"] self.credentials = ServiceAccountCredentials.from_json_keyfile_name( credentials, self.scope ) self.drive_service = build("drive", "v3", credentials=self.credentials) - self.folder_name = folder_name - self.parent_folder = parent_folder + self.parent_folder = folder_name + self.email = email + self.folder = self.open_folder() def list_folder(self, delete: Any = False) -> Set[str]: """List folders and files in Google Drive.""" @@ -72,10 +75,9 @@ def upload_file(self, file_path: str) -> str: """Upload file to Google Drive.""" file_metadata = { "name": os.path.basename(file_path), - "mimeType": "application/vnd.google-apps.folder", - "parents": [self.parent_folder] if self.parent_folder else "", + "mimeType": str(mimetypes.guess_type(file_path)[0]), + "parents": [self.parent_folder], } - media = MediaFileUpload(file_path, resumable=True) uploaded_file = ( @@ -83,15 +85,27 @@ def upload_file(self, file_path: str) -> str: .create(body=file_metadata, media_body=media, fields="id") # type: ignore .execute() ) - return uploaded_file["id"] - def upload_missing_files(self, storage_directory: str, missing_files: set) -> None: + def upload_missing_files(self, storage_directory: str) -> None: """Upload missing files to Google Drive.""" + # Read Google Drive .json files. + google_drive_files = self.list_folder() + google_drive_files_json = [ + file for file in google_drive_files if file.endswith(".json") + ] + # Read local directory. + local_files_json = set( + file for file in os.listdir(storage_directory) if file.endswith(".json") + ) + missing_files = local_files_json - set(google_drive_files_json) + print(f"Missing files: {len(missing_files)}") + # Upload missing files. uploaded_files = [] for file in missing_files: file_path = os.path.join(storage_directory, file) uploaded_file_id = google_drive.upload_file(self, file_path) + self.share_permissions(uploaded_file_id) uploaded_files.append( {"name": os.path.basename(file_path), "id": uploaded_file_id} ) @@ -108,3 +122,31 @@ def upload_missing_files(self, storage_directory: str, missing_files: set) -> No print( f"File '{this_name}' was not found in the list of files after uploading." ) + + def open_folder(self) -> Optional[str]: + """Open folder in web browser.""" + folder_metadata = ( + self.drive_service.files() + .get(fileId=self.parent_folder, fields="webViewLink") + .execute() + ) + folder_link = folder_metadata.get("webViewLink") + if folder_link: + print(f"Folder link: {folder_link}") + webbrowser.open( + folder_link + ) # Open the folder link in the default web browser + else: + print("Folder link not found.") + return folder_link + + def share_permissions(self, file_id: str) -> None: + """Share permissions with self.""" + new_permission = { + "type": "user", + "role": "writer", + "emailAddress": self.email, + } + self.drive_service.permissions().create( + fileId=file_id, body=new_permission, transferOwnership=False # type: ignore + ).execute() diff --git a/abr-testing/abr_testing/automation/jira_tool.py b/abr-testing/abr_testing/automation/jira_tool.py index a98b023a44a..5ed521c0430 100644 --- a/abr-testing/abr_testing/automation/jira_tool.py +++ b/abr-testing/abr_testing/automation/jira_tool.py @@ -6,77 +6,6 @@ import webbrowser import argparse from typing import List, Tuple -from abr_testing.data_collection import read_robot_logs, abr_google_drive, get_run_logs - - -def get_error_runs_from_robot(ip: str) -> List[str]: - """Get runs that have errors from robot.""" - error_run_ids = [] - response = requests.get( - f"http://{ip}:31950/runs", headers={"opentrons-version": "3"} - ) - run_data = response.json() - run_list = run_data["data"] - for run in run_list: - run_id = run["id"] - num_of_errors = len(run["errors"]) - if not run["current"] and num_of_errors > 0: - error_run_ids.append(run_id) - return error_run_ids - - -def get_error_info_from_robot( - ip: str, one_run: str, storage_directory: str -) -> Tuple[str, str, str, List[str], str, str]: - """Get error information from robot to fill out ticket.""" - description = dict() - # get run information - results = get_run_logs.get_run_data(one_run, ip) - # save run information to local directory as .json file - saved_file_path = read_robot_logs.save_run_log_to_json( - ip, results, storage_directory - ) - - # Error Printout - ( - num_of_errors, - error_type, - error_code, - error_instrument, - error_level, - ) = read_robot_logs.get_error_info(results) - # JIRA Ticket Fields - failure_level = "Level " + str(error_level) + " Failure" - components = [failure_level, "Flex-RABR"] - affects_version = results["API_Version"] - parent = results.get("robot_name", "") - print(parent) - summary = parent + "_" + str(one_run) + "_" + str(error_code) + "_" + error_type - # Description of error - description["protocol_name"] = results["protocol"]["metadata"].get( - "protocolName", "" - ) - description["error"] = " ".join([error_code, error_type, error_instrument]) - description["protocol_step"] = list(results["commands"])[-1] - description["right_mount"] = results.get("right", "No attachment") - description["left_mount"] = results.get("left", "No attachment") - description["gripper"] = results.get("extension", "No attachment") - all_modules = abr_google_drive.get_modules(results) - whole_description = {**description, **all_modules} - whole_description_str = ( - "{" - + "\n".join("{!r}: {!r},".format(k, v) for k, v in whole_description.items()) - + "}" - ) - - return ( - summary, - parent, - affects_version, - components, - whole_description_str, - saved_file_path, - ) class JiraTicket: @@ -193,20 +122,6 @@ def post_attachment_to_ticket(self, issue_id: str, attachment_path: str) -> None if __name__ == "__main__": """Create ticket for specified robot.""" parser = argparse.ArgumentParser(description="Pulls run logs from ABR robots.") - parser.add_argument( - "storage_directory", - metavar="STORAGE_DIRECTORY", - type=str, - nargs=1, - help="Path to long term storage directory for run logs.", - ) - parser.add_argument( - "robot_ip", - metavar="ROBOT_IP", - type=str, - nargs=1, - help="IP address of robot as string.", - ) parser.add_argument( "jira_api_token", metavar="JIRA_API_TOKEN", @@ -238,38 +153,9 @@ def post_attachment_to_ticket(self, issue_id: str, attachment_path: str) -> None help="JIRA Board ID. RABR is 217", ) args = parser.parse_args() - storage_directory = args.storage_directory[0] - ip = args.robot_ip[0] url = "https://opentrons.atlassian.net" api_token = args.jira_api_token[0] email = args.email[0] board_id = args.board_id[0] reporter_id = args.reporter_id[0] ticket = JiraTicket(url, api_token, email) - error_runs = get_error_runs_from_robot(ip) - one_run = error_runs[-1] # Most recent run with error. - ( - summary, - robot, - affects_version, - components, - whole_description_str, - saved_file_path, - ) = get_error_info_from_robot(ip, one_run, storage_directory) - print(f"Making ticket for run: {one_run} on robot {robot}.") - # TODO: make argument or see if I can get rid of with using board_id. - project_key = "RABR" - parent_key = project_key + "-" + robot[-1] - issue_url, issue_key = ticket.create_ticket( - summary, - whole_description_str, - project_key, - reporter_id, - "Bug", - "Medium", - components, - affects_version, - parent_key, - ) - ticket.open_issue(issue_key) - ticket.post_attachment_to_ticket(issue_key, saved_file_path) diff --git a/abr-testing/abr_testing/data_collection/abr_calibration_logs.py b/abr-testing/abr_testing/data_collection/abr_calibration_logs.py new file mode 100644 index 00000000000..6e897dd78eb --- /dev/null +++ b/abr-testing/abr_testing/data_collection/abr_calibration_logs.py @@ -0,0 +1,214 @@ +"""Get Calibration logs from robots.""" +from typing import Dict, Any, List +import argparse +import os +import json +import gspread # type: ignore[import] +import sys +from abr_testing.data_collection import read_robot_logs +from abr_testing.automation import google_drive_tool, google_sheets_tool + + +def check_for_duplicates( + sheet_location: str, + google_sheet: Any, + col_1: int, + col_2: int, + row: List[str], + headers: List[str], +) -> List[str]: + """Check google sheet for duplicates.""" + serials = google_sheet.get_column(col_1) + modify_dates = google_sheet.get_column(col_2) + for serial, modify_date in zip(serials, modify_dates): + if row[col_1 - 1] == serial and row[col_2 - 1] == modify_date: + print(f"Skipped row{row}. Already on Google Sheet.") + continue + read_robot_logs.write_to_sheets(sheet_location, google_sheet, row, headers) + return row + + +def upload_calibration_offsets( + calibration: Dict[str, Any], storage_directory: str +) -> None: + """Upload calibration data to google_sheet.""" + # Common Headers + headers_beg = list(calibration.keys())[:4] + headers_end = list(["X", "Y", "Z", "lastModified"]) + # INSTRUMENT SHEET + instrument_headers = ( + headers_beg + list(calibration["Instruments"][0].keys())[:7] + headers_end + ) + local_instrument_file = google_sheet_name + "-Instruments" + instrument_sheet_location = read_robot_logs.create_abr_data_sheet( + storage_directory, local_instrument_file, instrument_headers + ) + # INSTRUMENTS DATA + instruments = calibration["Instruments"] + for instrument in range(len(instruments)): + one_instrument = instruments[instrument] + x = one_instrument["data"]["calibratedOffset"]["offset"].get("x", "") + y = one_instrument["data"]["calibratedOffset"]["offset"].get("y", "") + z = one_instrument["data"]["calibratedOffset"]["offset"].get("z", "") + modified = one_instrument["data"]["calibratedOffset"].get("last_modified", "") + instrument_row = ( + list(calibration.values())[:4] + + list(one_instrument.values())[:7] + + list([x, y, z, modified]) + ) + check_for_duplicates( + instrument_sheet_location, + google_sheet_instruments, + 8, + 15, + instrument_row, + instrument_headers, + ) + # MODULE SHEET + if len(calibration.get("Modules", "")) > 0: + module_headers = ( + headers_beg + list(calibration["Modules"][0].keys())[:7] + headers_end + ) + local_modules_file = google_sheet_name + "-Modules" + modules_sheet_location = read_robot_logs.create_abr_data_sheet( + storage_directory, local_modules_file, module_headers + ) + # MODULES DATA + modules = calibration["Modules"] + for module in range(len(modules)): + one_module = modules[module] + x = one_module["moduleOffset"]["offset"].get("x", "") + y = one_module["moduleOffset"]["offset"].get("y", "") + z = one_module["moduleOffset"]["offset"].get("z", "") + modified = one_module["moduleOffset"].get("last_modified", "") + module_row = ( + list(calibration.values())[:4] + + list(one_module.values())[:7] + + list([x, y, z, modified]) + ) + check_for_duplicates( + modules_sheet_location, + google_sheet_modules, + 8, + 15, + module_row, + module_headers, + ) + # DECK SHEET + local_deck_file = google_sheet_name + "-Deck" + deck_headers = headers_beg + list(["pipetteCalibratedWith", "Slot"]) + headers_end + deck_sheet_location = read_robot_logs.create_abr_data_sheet( + storage_directory, local_deck_file, deck_headers + ) + # DECK DATA + deck = calibration["Deck"] + slots = ["D3", "D1", "A1"] + deck_modified = deck["data"].get("lastModified", "") + pipette_calibrated_with = deck["data"].get("pipetteCalibratedWith", "") + for i in range(len(deck["data"]["matrix"])): + coords = deck["data"]["matrix"][i] + x = coords[0] + y = coords[1] + z = coords[2] + deck_row = list(calibration.values())[:4] + list( + [pipette_calibrated_with, slots[i], x, y, z, deck_modified] + ) + check_for_duplicates( + deck_sheet_location, google_sheet_deck, 6, 10, deck_row, deck_headers + ) + + +if __name__ == "__main__": + """Get calibration logs.""" + parser = argparse.ArgumentParser( + description="Pulls calibration logs from ABR robots." + ) + parser.add_argument( + "storage_directory", + metavar="STORAGE_DIRECTORY", + type=str, + nargs=1, + help="Path to long term storage directory for run logs.", + ) + parser.add_argument( + "folder_name", + metavar="FOLDER_NAME", + type=str, + nargs=1, + help="Google Drive folder name. Open desired folder and copy string after drive/folders/.", + ) + parser.add_argument( + "google_sheet_name", + metavar="GOOGLE_SHEET_NAME", + type=str, + nargs=1, + help="Google sheet name.", + ) + parser.add_argument( + "email", metavar="EMAIL", type=str, nargs=1, help="opentrons gmail." + ) + parser.add_argument( + "ip_or_all", + metavar="IP_OR_ALL", + type=str, + nargs=1, + help="Enter 'ALL' to read IPs.json or type full IP address of 1 robot.", + ) + args = parser.parse_args() + storage_directory = args.storage_directory[0] + folder_name = args.folder_name[0] + google_sheet_name = args.google_sheet_name[0] + ip_or_all = args.ip_or_all[0] + email = args.email[0] + # Connect to google drive. + try: + credentials_path = os.path.join(storage_directory, "credentials.json") + except FileNotFoundError: + print(f"Add credentials.json file to: {storage_directory}.") + sys.exit() + try: + google_drive = google_drive_tool.google_drive( + credentials_path, folder_name, email + ) + # Upload calibration logs to google drive. + print("Connected to google drive.") + except json.decoder.JSONDecodeError: + print( + "Credential file is damaged. Get from https://console.cloud.google.com/apis/credentials" + ) + sys.exit() + # Connect to google sheet + try: + google_sheet_instruments = google_sheets_tool.google_sheet( + credentials_path, google_sheet_name, 0 + ) + google_sheet_modules = google_sheets_tool.google_sheet( + credentials_path, google_sheet_name, 1 + ) + google_sheet_deck = google_sheets_tool.google_sheet( + credentials_path, google_sheet_name, 2 + ) + print(f"Connected to google sheet: {google_sheet_name}") + except gspread.exceptions.APIError: + print("ERROR: Check google sheet name. Check credentials file.") + sys.exit() + ip_json_file = os.path.join(storage_directory, "IPs.json") + try: + ip_file = json.load(open(ip_json_file)) + except FileNotFoundError: + print(f"Add .json file with robot IPs to: {storage_directory}.") + sys.exit() + if ip_or_all == "ALL": + ip_address_list = ip_file["ip_address_list"] + for ip in ip_address_list: + saved_file_path, calibration = read_robot_logs.get_calibration_offsets( + ip, storage_directory + ) + upload_calibration_offsets(calibration, storage_directory) + else: + saved_file_path, calibration = read_robot_logs.get_calibration_offsets( + ip_or_all, storage_directory + ) + upload_calibration_offsets(calibration, storage_directory) + + google_drive.upload_missing_files(storage_directory) diff --git a/abr-testing/abr_testing/data_collection/abr_google_drive.py b/abr-testing/abr_testing/data_collection/abr_google_drive.py index 6dfc5e8f284..1d79bbe2ca2 100644 --- a/abr-testing/abr_testing/data_collection/abr_google_drive.py +++ b/abr-testing/abr_testing/data_collection/abr_google_drive.py @@ -122,7 +122,7 @@ def create_data_dictionary( metavar="FOLDER_NAME", type=str, nargs=1, - help="Google Drive folder name.", + help="Google Drive folder name. Open desired folder and copy string after drive/folders/.", ) parser.add_argument( "google_sheet_name", @@ -131,11 +131,14 @@ def create_data_dictionary( nargs=1, help="Google sheet name.", ) + parser.add_argument( + "email", metavar="EMAIL", type=str, nargs=1, help="opentrons gmail." + ) args = parser.parse_args() folder_name = args.folder_name[0] storage_directory = args.storage_directory[0] google_sheet_name = args.google_sheet_name[0] - parent_folder = False + email = args.email[0] try: credentials_path = os.path.join(storage_directory, "credentials.json") except FileNotFoundError: @@ -143,7 +146,7 @@ def create_data_dictionary( sys.exit() try: google_drive = google_drive_tool.google_drive( - credentials_path, folder_name, parent_folder + credentials_path, folder_name, email ) print("Connected to google drive.") except json.decoder.JSONDecodeError: @@ -162,21 +165,9 @@ def create_data_dictionary( sys.exit() run_ids_on_gs = google_sheet.get_column(2) run_ids_on_gs = set(run_ids_on_gs) - # Read Google Drive .json files - google_drive_files = google_drive.list_folder() - google_drive_files_json = [ - file for file in google_drive_files if file.endswith(".json") - ] - # read local directory - list_of_files = os.listdir(storage_directory) - local_files_json = set( - file for file in os.listdir(storage_directory) if file.endswith(".json") - ) - missing_files = local_files_json - set(google_drive_files_json) - print(f"Missing files: {len(missing_files)}") # Uploads files that are not in google drive directory - google_drive.upload_missing_files(storage_directory, missing_files) + google_drive.upload_missing_files(storage_directory) # Run ids in google_drive_folder run_ids_on_gd = read_robot_logs.get_run_ids_from_google_drive(google_drive) diff --git a/abr-testing/abr_testing/data_collection/abr_robot_error.py b/abr-testing/abr_testing/data_collection/abr_robot_error.py new file mode 100644 index 00000000000..9e9e2240a84 --- /dev/null +++ b/abr-testing/abr_testing/data_collection/abr_robot_error.py @@ -0,0 +1,165 @@ +"""Create ticket for robot with error.""" +from typing import List, Tuple +from abr_testing.data_collection import read_robot_logs, abr_google_drive, get_run_logs +import requests +import argparse +from abr_testing.automation import jira_tool + + +def get_error_runs_from_robot(ip: str) -> List[str]: + """Get runs that have errors from robot.""" + error_run_ids = [] + response = requests.get( + f"http://{ip}:31950/runs", headers={"opentrons-version": "3"} + ) + run_data = response.json() + run_list = run_data["data"] + for run in run_list: + run_id = run["id"] + num_of_errors = len(run["errors"]) + if not run["current"] and num_of_errors > 0: + error_run_ids.append(run_id) + return error_run_ids + + +def get_error_info_from_robot( + ip: str, one_run: str, storage_directory: str +) -> Tuple[str, str, str, List[str], str, str]: + """Get error information from robot to fill out ticket.""" + description = dict() + # get run information + results = get_run_logs.get_run_data(one_run, ip) + # save run information to local directory as .json file + saved_file_path = read_robot_logs.save_run_log_to_json( + ip, results, storage_directory + ) + # Error Printout + ( + num_of_errors, + error_type, + error_code, + error_instrument, + error_level, + ) = read_robot_logs.get_error_info(results) + # JIRA Ticket Fields + failure_level = "Level " + str(error_level) + " Failure" + components = [failure_level, "Flex-RABR"] + affects_version = results["API_Version"] + parent = results.get("robot_name", "") + print(parent) + summary = parent + "_" + str(one_run) + "_" + str(error_code) + "_" + error_type + # Description of error + description["protocol_name"] = results["protocol"]["metadata"].get( + "protocolName", "" + ) + description["error"] = " ".join([error_code, error_type, error_instrument]) + description["protocol_step"] = list(results["commands"])[-1] + description["right_mount"] = results.get("right", "No attachment") + description["left_mount"] = results.get("left", "No attachment") + description["gripper"] = results.get("extension", "No attachment") + all_modules = abr_google_drive.get_modules(results) + whole_description = {**description, **all_modules} + whole_description_str = ( + "{" + + "\n".join("{!r}: {!r},".format(k, v) for k, v in whole_description.items()) + + "}" + ) + + return ( + summary, + parent, + affects_version, + components, + whole_description_str, + saved_file_path, + ) + + +if __name__ == "__main__": + """Create ticket for specified robot.""" + parser = argparse.ArgumentParser(description="Pulls run logs from ABR robots.") + parser.add_argument( + "storage_directory", + metavar="STORAGE_DIRECTORY", + type=str, + nargs=1, + help="Path to long term storage directory for run logs.", + ) + parser.add_argument( + "robot_ip", + metavar="ROBOT_IP", + type=str, + nargs=1, + help="IP address of robot as string.", + ) + parser.add_argument( + "jira_api_token", + metavar="JIRA_API_TOKEN", + type=str, + nargs=1, + help="JIRA API Token. Get from https://id.atlassian.com/manage-profile/security.", + ) + parser.add_argument( + "email", + metavar="EMAIL", + type=str, + nargs=1, + help="Email connected to JIRA account.", + ) + # TODO: write function to get reporter_id from email. + parser.add_argument( + "reporter_id", + metavar="REPORTER_ID", + type=str, + nargs=1, + help="JIRA Reporter ID.", + ) + # TODO: improve help comment on jira board id. + parser.add_argument( + "board_id", + metavar="BOARD_ID", + type=str, + nargs=1, + help="JIRA Board ID. RABR is 217", + ) + args = parser.parse_args() + storage_directory = args.storage_directory[0] + ip = args.robot_ip[0] + url = "https://opentrons.atlassian.net" + api_token = args.jira_api_token[0] + email = args.email[0] + board_id = args.board_id[0] + reporter_id = args.reporter_id[0] + ticket = jira_tool.JiraTicket(url, api_token, email) + error_runs = get_error_runs_from_robot(ip) + one_run = error_runs[-1] # Most recent run with error. + ( + summary, + robot, + affects_version, + components, + whole_description_str, + saved_file_path, + ) = get_error_info_from_robot(ip, one_run, storage_directory) + print(f"Making ticket for run: {one_run} on robot {robot}.") + # TODO: make argument or see if I can get rid of with using board_id. + project_key = "RABR" + parent_key = project_key + "-" + robot[-1] + issue_url, issue_key = ticket.create_ticket( + summary, + whole_description_str, + project_key, + reporter_id, + "Bug", + "Medium", + components, + affects_version, + parent_key, + ) + ticket.open_issue(issue_key) + ticket.post_attachment_to_ticket(issue_key, saved_file_path) + # get calibration data + saved_file_path_calibration, calibration = read_robot_logs.get_calibration_offsets( + ip, storage_directory + ) + ticket.post_attachment_to_ticket(issue_key, saved_file_path_calibration) diff --git a/abr-testing/abr_testing/data_collection/get_run_logs.py b/abr-testing/abr_testing/data_collection/get_run_logs.py index 1511e3405e7..4034f076dc9 100644 --- a/abr-testing/abr_testing/data_collection/get_run_logs.py +++ b/abr-testing/abr_testing/data_collection/get_run_logs.py @@ -107,8 +107,8 @@ def get_all_run_logs(storage_directory: str) -> None: try: runs = get_run_ids_from_robot(ip) runs_to_save = read_robot_logs.get_unseen_run_ids(runs, runs_from_storage) - saved_file_paths = save_runs(runs_to_save, ip, storage_directory) - google_drive.upload_missing_files(storage_directory, saved_file_paths) + save_runs(runs_to_save, ip, storage_directory) + google_drive.upload_missing_files(storage_directory) except Exception: print(f"ERROR: Failed to read IP address: {ip}.") @@ -128,12 +128,15 @@ def get_all_run_logs(storage_directory: str) -> None: metavar="FOLDER_NAME", type=str, nargs=1, - help="Google Drive folder name.", + help="Google Drive folder name. Open desired folder and copy string after drive/folders/.", + ) + parser.add_argument( + "email", metavar="EMAIL", type=str, nargs=1, help="opentrons gmail." ) args = parser.parse_args() storage_directory = args.storage_directory[0] folder_name = args.folder_name[0] - parent_folder = False + email = args.email[0] try: credentials_path = os.path.join(storage_directory, "credentials.json") except FileNotFoundError: @@ -141,7 +144,7 @@ def get_all_run_logs(storage_directory: str) -> None: sys.exit() try: google_drive = google_drive_tool.google_drive( - credentials_path, folder_name, parent_folder + credentials_path, folder_name, email ) print("Connected to google drive.") except json.decoder.JSONDecodeError: diff --git a/abr-testing/abr_testing/data_collection/read_robot_logs.py b/abr-testing/abr_testing/data_collection/read_robot_logs.py index abc8efb095e..6a7276c142b 100644 --- a/abr-testing/abr_testing/data_collection/read_robot_logs.py +++ b/abr-testing/abr_testing/data_collection/read_robot_logs.py @@ -1,15 +1,17 @@ """ABR Read Robot Logs. -This library is downloading logs from robots, extracting wanted information, +This library has functions to download logs from robots, extracting wanted information, and uploading to a google sheet using credentials and google_sheets_tools module saved in a local directory. """ import csv +import datetime import os from abr_testing.data_collection.error_levels import ERROR_LEVELS_PATH from typing import List, Dict, Any, Tuple, Set import time as t import json +import requests def create_abr_data_sheet( @@ -26,7 +28,7 @@ def create_abr_data_sheet( writer = csv.DictWriter(csvfile, fieldnames=headers) writer.writeheader() print(f"Created file. Located: {sheet_location}.") - return file_name_csv + return sheet_location def get_error_info(file_results: Dict[str, Any]) -> Tuple[int, str, str, str, str]: @@ -158,3 +160,64 @@ def get_run_ids_from_google_drive(google_drive: Any) -> Set[str]: file_id = file.split(".json")[0].split("_")[1] run_ids_on_gd.add(file_id) return run_ids_on_gd + + +def write_to_sheets( + sheet_location: str, google_sheet: Any, row_list: List[Any], headers: List[str] +) -> None: + """Write list to google sheet and csv.""" + with open(sheet_location, "a", newline="") as f: + writer = csv.writer(f) + writer.writerow(row_list) + # Read Google Sheet + google_sheet.token_check() + google_sheet.write_header(headers) + google_sheet.update_row_index() + google_sheet.write_to_row(row_list) + t.sleep(5) # Sleep added to avoid API error. + + +def get_calibration_offsets( + ip: str, storage_directory: str +) -> Tuple[str, Dict[str, Any]]: + """Connect to robot via ip and get calibration data.""" + calibration = dict() + # Robot Information [Name, Software Version] + response = requests.get( + f"http://{ip}:31950/health", headers={"opentrons-version": "3"} + ) + health_data = response.json() + robot_name = health_data.get("name", "") + api_version = health_data.get("api_version", "") + pull_date_timestamp = datetime.datetime.now() + date = pull_date_timestamp.date().isoformat() + file_date = str(pull_date_timestamp).replace(":", "").split(".")[0] + calibration["Robot"] = robot_name + calibration["Software Version"] = api_version + calibration["Pull Date"] = date + calibration["Pull Timestamp"] = pull_date_timestamp.isoformat() + calibration["run_id"] = "calibration" + "_" + file_date + # Calibration [Instruments, modules, deck] + response = requests.get( + f"http://{ip}:31950/instruments", + headers={"opentrons-version": "3"}, + params={"cursor": 0, "pageLength": 0}, + ) + instruments: Dict[str, Any] = response.json() + calibration["Instruments"] = instruments.get("data", "") + response = requests.get( + f"http://{ip}:31950/modules", + headers={"opentrons-version": "3"}, + params={"cursor": 0, "pageLength": 0}, + ) + modules: Dict[str, Any] = response.json() + calibration["Modules"] = modules.get("data", "") + response = requests.get( + f"http://{ip}:31950/calibration/status", + headers={"opentrons-version": "3"}, + params={"cursor": 0, "pageLength": 0}, + ) + deck: Dict[str, Any] = response.json() + calibration["Deck"] = deck.get("deckCalibration", "") + saved_file_path = save_run_log_to_json(ip, calibration, storage_directory) + return saved_file_path, calibration diff --git a/abr-testing/abr_testing/tools/abr_scale.py b/abr-testing/abr_testing/tools/abr_scale.py index 04ed34c3f8e..0947091fe4b 100644 --- a/abr-testing/abr_testing/tools/abr_scale.py +++ b/abr-testing/abr_testing/tools/abr_scale.py @@ -3,28 +3,11 @@ import datetime from hardware_testing.drivers import find_port, list_ports_and_select # type: ignore[import] from hardware_testing.drivers.radwag import RadwagScale # type: ignore[import] -from typing import Any, List import argparse -import csv from abr_testing.data_collection import read_robot_logs from abr_testing.automation import google_sheets_tool -def write_to_sheets(file_name_csv: str, google_sheet: Any, row_list: List) -> None: - """Write list to google sheet and csv.""" - sheet_location = os.path.join(storage_directory, file_name_csv) - with open(sheet_location, "a", newline="") as f: - writer = csv.writer(f) - writer.writerow(row_list) - print(f"Written {row_list} point to {file_name_csv}") - # Read Google Sheet - google_sheet.token_check() - google_sheet.write_header(headers) - google_sheet.update_row_index() - google_sheet.write_to_row(row_list) - print(f"Written {row_list} to google sheet.") - - if __name__ == "__main__": # Adds Arguments parser = argparse.ArgumentParser(description="Record stable mass for labware.") @@ -76,7 +59,7 @@ def write_to_sheets(file_name_csv: str, google_sheet: Any, row_list: List) -> No is_stable = False # Set up csv sheet headers = ["Robot", "Date", "Timestamp", "Labware", "Mass (g)", "Measurement Step"] - all_data_csv = read_robot_logs.create_abr_data_sheet( + sheet_location = read_robot_logs.create_abr_data_sheet( storage_directory, file_name, headers ) # Set up google sheet @@ -100,7 +83,9 @@ def write_to_sheets(file_name_csv: str, google_sheet: Any, row_list: List) -> No row_list = list(row) while is_stable is True: print("is stable") - write_to_sheets(file_name_csv, google_sheet, row_list) + read_robot_logs.write_to_sheets( + sheet_location, google_sheet, row_list, headers + ) is_stable = False y_or_no = input("Do you want to weigh another sample? (Y/N): ") if y_or_no == "Y": From 8f2c5e339d4c7901ee1f3cf4393d2230130d7676 Mon Sep 17 00:00:00 2001 From: Brayan Almonte Date: Wed, 3 Apr 2024 12:24:38 -0400 Subject: [PATCH 029/194] fix(api): Change the camera device to /dev/camera2 (#14790) We updated the Linux Kernel recently which has changed the camera device from /dev/video0 to /dev/video2, so lets change it here. This pull request pertains to [Opentrons/oe-core#140](https://github.com/Opentrons/oe-core/pull/140). Closes: [EXEC-354](https://opentrons.atlassian.net/browse/EXEC-354) # Test Plan - [x] Make sure we can take a picture via `/camera/picture` endpoint. # Changelog - set the camera device to /dev/video2 when taking a picture # Review requests # Risk assessment [EXEC-354]: https://opentrons.atlassian.net/browse/EXEC-354?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ --------- Co-authored-by: Seth Foster --- api/release-notes.md | 1 + api/src/opentrons/system/camera.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/api/release-notes.md b/api/release-notes.md index 046b3e1e04b..ca9523121b4 100644 --- a/api/release-notes.md +++ b/api/release-notes.md @@ -17,6 +17,7 @@ Welcome to the v7.2.2 release of the Opentrons robot software! ### Bug Fixes - Restores the ability to use the speaker and camera on OT-2. +- Restores the ability to use the camera on Flex. --- diff --git a/api/src/opentrons/system/camera.py b/api/src/opentrons/system/camera.py index 1c2d09d8747..761a9ba66a1 100644 --- a/api/src/opentrons/system/camera.py +++ b/api/src/opentrons/system/camera.py @@ -1,6 +1,7 @@ import asyncio import os from pathlib import Path + from opentrons.config import ARCHITECTURE, SystemArchitecture from opentrons_shared_data.errors.exceptions import CommunicationError from opentrons_shared_data.errors.codes import ErrorCodes @@ -29,7 +30,7 @@ async def take_picture(filename: Path) -> None: pass if ARCHITECTURE == SystemArchitecture.YOCTO: - cmd = f"v4l2-ctl --device /dev/video0 --set-fmt-video=width=1280,height=720,pixelformat=MJPG --stream-mmap --stream-to={str(filename)} --stream-count=1" + cmd = f"v4l2-ctl --device /dev/video2 --set-fmt-video=width=1280,height=720,pixelformat=MJPG --stream-mmap --stream-to={str(filename)} --stream-count=1" elif ARCHITECTURE == SystemArchitecture.BUILDROOT: cmd = f"ffmpeg -f video4linux2 -s 640x480 -i /dev/video0 -ss 0:0:1 -frames 1 {str(filename)}" else: # HOST From 3b3c8e4aa2a34c7bdda84ccc02d9eb96ecb87efb Mon Sep 17 00:00:00 2001 From: Jethary Rader <66035149+jerader@users.noreply.github.com> Date: Wed, 3 Apr 2024 14:01:17 -0400 Subject: [PATCH 030/194] feat(protocol-designer, step-generation): x/Y tip positioning for asp, disp, mix (#14758) closes AUTH-5 --- .../cypress/integration/mixSettings.spec.js | 4 +- .../integration/transferSettings.spec.js | 20 +- .../protocol/8/doItAllV3MigratedToV8.json | 197 ++-- .../protocol/8/doItAllV4MigratedToV8.json | 76 +- .../protocol/8/doItAllV7MigratedToV8.json | 434 +++++--- .../fixtures/protocol/8/doItAllV8.json | 228 +++-- .../protocol/8/example_1_1_0MigratedToV8.json | 966 ++++++++++++------ .../fixtures/protocol/8/mix_8_0_0.json | 14 +- .../8/ninetySixChannelFullAndColumn.json | 60 +- .../components/BatchEditForm/BatchEditMix.tsx | 8 +- .../BatchEditForm/BatchEditMoveLiquid.tsx | 8 +- .../StepEditForm/fields/DelayFields.tsx | 3 +- .../TipPositionField/TipPositionAllViz.tsx | 52 + .../TipPositionInput.module.css | 13 +- .../TipPositionField/TipPositionModal.tsx | 461 +++++---- .../TipPositionField/ZTipPositionModal.tsx | 260 +++++ .../__tests__/TipPositionField.test.tsx | 113 ++ .../__tests__/TipPositionModal.test.tsx | 124 +++ .../fields/TipPositionField/constants.ts | 4 + .../fields/TipPositionField/index.tsx | 172 +++- .../fields/TipPositionField/utils.ts | 73 +- .../components/StepEditForm/forms/MixForm.tsx | 8 +- .../forms/MoveLiquidForm/SourceDestFields.tsx | 8 +- protocol-designer/src/form-types.ts | 25 +- .../src/load-file/migration/8_1_0.ts | 16 +- .../src/localization/en/modal.json | 13 +- .../src/localization/en/tooltip.json | 8 +- .../test/createPresavedStepForm.test.ts | 6 + .../formLevel/getDefaultsForStepType.ts | 6 + .../formLevel/stepFormToArgs/mixFormToArgs.ts | 13 +- .../stepFormToArgs/moveLiquidFormToArgs.ts | 8 + .../test/getDefaultsForStepType.test.ts | 7 +- .../generateRobotStateTimeline.test.ts | 12 + .../src/ui/steps/test/selectors.test.ts | 37 + shared-data/js/helpers/index.ts | 17 + .../src/__tests__/aspirate.test.ts | 34 + .../src/__tests__/consolidate.test.ts | 158 +++ .../src/__tests__/dispense.test.ts | 21 +- .../src/__tests__/distribute.test.ts | 28 + step-generation/src/__tests__/mix.test.ts | 4 + .../src/__tests__/transfer.test.ts | 187 ++++ .../src/commandCreators/atomic/aspirate.ts | 6 + .../src/commandCreators/atomic/dispense.ts | 10 +- .../commandCreators/compound/consolidate.ts | 22 + .../commandCreators/compound/distribute.ts | 18 + .../src/commandCreators/compound/mix.ts | 20 + .../src/commandCreators/compound/transfer.ts | 24 + .../src/fixtures/commandFixtures.ts | 4 + step-generation/src/types.ts | 14 + step-generation/src/utils/misc.ts | 11 + 50 files changed, 3054 insertions(+), 981 deletions(-) create mode 100644 protocol-designer/src/components/StepEditForm/fields/TipPositionField/TipPositionAllViz.tsx create mode 100644 protocol-designer/src/components/StepEditForm/fields/TipPositionField/ZTipPositionModal.tsx create mode 100644 protocol-designer/src/components/StepEditForm/fields/TipPositionField/__tests__/TipPositionField.test.tsx create mode 100644 protocol-designer/src/components/StepEditForm/fields/TipPositionField/__tests__/TipPositionModal.test.tsx create mode 100644 protocol-designer/src/components/StepEditForm/fields/TipPositionField/constants.ts diff --git a/protocol-designer/cypress/integration/mixSettings.spec.js b/protocol-designer/cypress/integration/mixSettings.spec.js index 809c92237b3..60fabb65d78 100644 --- a/protocol-designer/cypress/integration/mixSettings.spec.js +++ b/protocol-designer/cypress/integration/mixSettings.spec.js @@ -59,7 +59,7 @@ describe('Advanced Settings for Mix Form', () => { cy.get('input[name="aspirate_flowRate"]').should('be.disabled') // TipPosition Aspirate should be disabled - cy.get('[id=TipPositionField_mix_mmFromBottom]').should('be.disabled') + cy.get('[id=TipPositionIcon_mix_mmFromBottom]').should('not.be.enabled') // Dispense Flowrate disbled cy.get('input[name="dispense_flowRate"]').should('be.disabled') @@ -91,7 +91,7 @@ describe('Advanced Settings for Mix Form', () => { cy.get('input[name="dispense_flowRate"]').should('be.enabled') // TipPosition Aspirate should be enabled - cy.get('[id=TipPositionField_mix_mmFromBottom]').should('be.enabled') + cy.get('[id=TipPositionIcon_mix_mmFromBottom]').should('not.be.disabled') // Delay in aspirate and Dispense settings is enabled cy.get('input[name="aspirate_delay_checkbox"]').should('be.enabled') diff --git a/protocol-designer/cypress/integration/transferSettings.spec.js b/protocol-designer/cypress/integration/transferSettings.spec.js index a4c831fddd4..82fa26f8dae 100644 --- a/protocol-designer/cypress/integration/transferSettings.spec.js +++ b/protocol-designer/cypress/integration/transferSettings.spec.js @@ -53,7 +53,7 @@ describe('Advanced Settings for Transfer Form', () => { it('Verify functionality of advanced settings with different pipette and labware', () => { enterBatchEdit() - // Different Pipette disbales aspirate and dispense Flowrate and Mix settings + // Different Pipette disables aspirate and dispense Flowrate and Mix settings // step 6 has different pipette than step 1 cy.get('[data-test="StepItem_6"]').click(batchEditClickOptions) @@ -68,10 +68,14 @@ describe('Advanced Settings for Transfer Form', () => { cy.get('input[name="aspirate_mix_checkbox"]').should('be.disabled') // TipPosition Aspirate and Dispense should be disabled - cy.get('[id=TipPositionField_aspirate_mmFromBottom]').should('be.disabled') - cy.get('[id=TipPositionField_dispense_mmFromBottom]').should('be.disabled') + cy.get('[id=TipPositionIcon_aspirate_mmFromBottom]').should( + 'not.be.enabled' + ) + cy.get('[id=TipPositionIcon_dispense_mmFromBottom]').should( + 'not.be.enabled' + ) - // Dispense Flowrate and mix diabled + // Dispense Flowrate and mix disabled cy.get('input[name="dispense_flowRate"]').should('be.disabled') cy.get('input[name="dispense_mix_checkbox"]').should('be.disabled') @@ -108,8 +112,12 @@ describe('Advanced Settings for Transfer Form', () => { .should('be.empty') // TipPosition Aspirate and Dispense should be enabled - cy.get('[id=TipPositionField_aspirate_mmFromBottom]').should('be.enabled') - cy.get('[id=TipPositionField_dispense_mmFromBottom]').should('be.enabled') + cy.get('[id=TipPositionIcon_aspirate_mmFromBottom]').should( + 'not.be.disabled' + ) + cy.get('[id=TipPositionIcon_dispense_mmFromBottom]').should( + 'not.be.disabled' + ) // Delay in aspirate and Dispense settings is enabled cy.get('input[name="aspirate_delay_checkbox"]').should('be.enabled') diff --git a/protocol-designer/fixtures/protocol/8/doItAllV3MigratedToV8.json b/protocol-designer/fixtures/protocol/8/doItAllV3MigratedToV8.json index 9bc7b9e44ed..340c594e596 100644 --- a/protocol-designer/fixtures/protocol/8/doItAllV3MigratedToV8.json +++ b/protocol-designer/fixtures/protocol/8/doItAllV3MigratedToV8.json @@ -6,7 +6,7 @@ "author": "Fixture", "description": "Test all v3 commands", "created": 1585930833548, - "lastModified": 1709303240330, + "lastModified": 1711742442671, "category": null, "subcategory": null, "tags": [] @@ -15,7 +15,7 @@ "name": "opentrons/protocol-designer", "version": "8.1.0", "data": { - "_internalAppBuildDate": "Fri, 01 Mar 2024 14:22:27 GMT", + "_internalAppBuildDate": "Fri, 29 Mar 2024 20:00:04 GMT", "defaultValues": { "aspirate_mmFromBottom": 1, "dispense_mmFromBottom": 0.5, @@ -116,6 +116,10 @@ "dispense_delay_mmFromBottom": "0.5", "dropTip_location": "8053a205-f2dc-4b1d-8d05-bf8233949e2e:trashBin", "nozzles": null, + "dispense_x_position": 0, + "dispense_y_position": 0, + "aspirate_x_position": 0, + "aspirate_y_position": 0, "id": "3961e4c0-75c7-11ea-b42f-4b64e50f43e5", "stepType": "moveLiquid", "stepName": "transfer", @@ -170,6 +174,8 @@ "dropTip_location": "8053a205-f2dc-4b1d-8d05-bf8233949e2e:trashBin", "nozzles": null, "tipRack": "0b44c760-75c7-11ea-b42f-4b64e50f43e5:opentrons/opentrons_96_tiprack_300ul/1", + "mix_x_position": 0, + "mix_y_position": 0, "id": "a4cee9a0-75dc-11ea-b42f-4b64e50f43e5", "stepType": "mix", "stepName": "mix", @@ -2518,7 +2524,7 @@ "commandSchemaId": "opentronsCommandSchemaV8", "commands": [ { - "key": "db2d2973-9059-41a8-a6f7-3b70b747cb2d", + "key": "d371b7e2-71a8-4a60-90bc-7e865d9881b9", "commandType": "loadPipette", "params": { "pipetteName": "p300_single_gen2", @@ -2527,7 +2533,7 @@ } }, { - "key": "d9bb5f59-77e8-4794-af52-5ac18181a1c9", + "key": "424963b7-59f8-434a-bedc-9597e7b72c9f", "commandType": "loadLabware", "params": { "displayName": "Opentrons 96 Tip Rack 300 µL", @@ -2539,7 +2545,7 @@ } }, { - "key": "e375681d-7284-4f0c-9921-d16e4ce0649e", + "key": "05ef86f7-dec0-4134-a15d-5e38ef81cf8e", "commandType": "loadLabware", "params": { "displayName": "NEST 96 Well Plate 100 µL PCR Full Skirt", @@ -2551,7 +2557,7 @@ } }, { - "key": "9455cd08-4d3a-45e4-8614-7485193e824e", + "key": "ddefc5ef-b69a-4172-921b-959ba5e8d8d2", "commandType": "loadLabware", "params": { "displayName": "Opentrons 24 Well Aluminum Block with Generic 2 mL Screwcap", @@ -2564,7 +2570,7 @@ }, { "commandType": "loadLiquid", - "key": "27c67940-a745-41b9-b4d8-01a8dba8b4e9", + "key": "2a2084d5-67d8-4806-b919-5962a6258c1f", "params": { "liquidId": "0", "labwareId": "1e610d40-75c7-11ea-b42f-4b64e50f43e5:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1", @@ -2590,12 +2596,12 @@ }, { "commandType": "waitForDuration", - "key": "b7c8d36b-c9d6-4fa3-a696-da35a3cc5981", + "key": "c1a1eff4-7ef7-46be-aee7-ebca5924ace8", "params": { "seconds": 62, "message": "" } }, { "commandType": "pickUpTip", - "key": "d97988e7-e386-4965-a915-f4776a0d7720", + "key": "63ca0ab5-4cb6-4531-b912-1ba22e1b1a03", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "labwareId": "0b44c760-75c7-11ea-b42f-4b64e50f43e5:opentrons/opentrons_96_tiprack_300ul/1", @@ -2604,67 +2610,82 @@ }, { "commandType": "aspirate", - "key": "a60e86c1-8bf0-477f-8748-24ce798eb1de", + "key": "5ead7532-0eb2-4ad9-b704-856422fc9408", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "volume": 30, "labwareId": "1e610d40-75c7-11ea-b42f-4b64e50f43e5:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 46.43 } }, { "commandType": "dispense", - "key": "61afaad0-0566-4435-b03a-94498d2fc2aa", + "key": "3838f7d1-3450-49cc-a222-c8113eecf108", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "volume": 30, "labwareId": "1e610d40-75c7-11ea-b42f-4b64e50f43e5:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 46.43 } }, { "commandType": "aspirate", - "key": "4ccf6427-d404-4b4b-9974-935a6676d8d2", + "key": "25697ae7-169d-447a-906c-4e7f02950fe9", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "volume": 30, "labwareId": "1e610d40-75c7-11ea-b42f-4b64e50f43e5:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 46.43 } }, { "commandType": "dispense", - "key": "bcdd9d53-ff68-4264-bd61-e11422149144", + "key": "49a139f4-87ba-421d-9ef4-4ebe13beb987", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "volume": 30, "labwareId": "1e610d40-75c7-11ea-b42f-4b64e50f43e5:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 46.43 } }, { "commandType": "aspirate", - "key": "ce51dbac-b2ed-4edd-9657-33c106288844", + "key": "4e96faa5-c669-4b60-b15c-9d2f01c9c3fe", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "volume": 100, "labwareId": "1e610d40-75c7-11ea-b42f-4b64e50f43e5:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 46.43 } }, { "commandType": "touchTip", - "key": "a1993756-c789-4804-8ff0-f3f9577d68f4", + "key": "8eff88a1-fec9-46d7-b292-f6ce378e5ad9", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "labwareId": "1e610d40-75c7-11ea-b42f-4b64e50f43e5:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1", @@ -2674,19 +2695,22 @@ }, { "commandType": "dispense", - "key": "270e73e1-719a-4338-9a8d-7ef8cdab558e", + "key": "c95e323c-be69-4460-8acf-d1d4b74384bd", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "volume": 40, "labwareId": "21ed8f60-75c7-11ea-b42f-4b64e50f43e5:opentrons/opentrons_24_aluminumblock_generic_2ml_screwcap/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 46.43 } }, { "commandType": "touchTip", - "key": "45fd9725-1bef-4333-bce3-e4e81fc94fd4", + "key": "0da25745-5e25-4138-b67c-dfc4c89c8949", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "labwareId": "21ed8f60-75c7-11ea-b42f-4b64e50f43e5:opentrons/opentrons_24_aluminumblock_generic_2ml_screwcap/1", @@ -2696,19 +2720,22 @@ }, { "commandType": "dispense", - "key": "ede9c8e0-ced4-4a60-841e-c28476d28ab8", + "key": "28eeb3d1-6e83-4414-8c0d-e8761ca2f75a", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "volume": 40, "labwareId": "21ed8f60-75c7-11ea-b42f-4b64e50f43e5:opentrons/opentrons_24_aluminumblock_generic_2ml_screwcap/1", "wellName": "A2", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 46.43 } }, { "commandType": "touchTip", - "key": "d7fb1df9-ee04-4a6d-98e0-1ded591260bc", + "key": "8cd5d90d-df0b-4c3b-8cb3-cea6f1849fef", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "labwareId": "21ed8f60-75c7-11ea-b42f-4b64e50f43e5:opentrons/opentrons_24_aluminumblock_generic_2ml_screwcap/1", @@ -2718,7 +2745,7 @@ }, { "commandType": "moveToAddressableArea", - "key": "35f1f9b9-78f2-4a1a-9b7b-3c488881db2b", + "key": "35643d1f-ae0b-4a90-9de4-c9eb3c9b775e", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "addressableAreaName": "fixedTrash", @@ -2727,7 +2754,7 @@ }, { "commandType": "blowOutInPlace", - "key": "7dbec34d-5da6-41aa-9ff9-9368efa23407", + "key": "d540a57a-6968-44a0-8645-b221a9b7bfd7", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "flowRate": 46.43 @@ -2735,7 +2762,7 @@ }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "7c10381b-bee1-4908-94c5-11d76a966a12", + "key": "c721cfd7-fef8-4fcb-9d6f-1d78f2317729", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "addressableAreaName": "fixedTrash", @@ -2745,17 +2772,17 @@ }, { "commandType": "dropTipInPlace", - "key": "fb84a594-b8b9-4950-81f0-cc2be260346e", + "key": "a18788f3-cd5f-4470-8831-455d14883d1c", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5" } }, { "commandType": "waitForResume", - "key": "340df2fa-adf0-4b43-90d7-2e1d8f09ba71", + "key": "a54eb58b-ce5c-4a59-ba85-ed75438146a7", "params": { "message": "Wait until user intervention" } }, { "commandType": "pickUpTip", - "key": "9ff49b09-2860-4955-bd6f-a68ab3797208", + "key": "c1bddcd0-d5cf-4d7c-b830-a5b27a5a71cb", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "labwareId": "0b44c760-75c7-11ea-b42f-4b64e50f43e5:opentrons/opentrons_96_tiprack_300ul/1", @@ -2764,79 +2791,97 @@ }, { "commandType": "aspirate", - "key": "249a56b1-e68b-449d-aad1-9a4bd9113a34", + "key": "1660f6c2-9072-4348-b034-cb45712f8cd7", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "volume": 35, "labwareId": "1e610d40-75c7-11ea-b42f-4b64e50f43e5:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1", "wellName": "D2", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 40 } }, { "commandType": "dispense", - "key": "5503460e-fc28-4ebf-b476-88d2517ec4c5", + "key": "c3683fde-b4e0-4432-ad96-932292f2ebcd", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "volume": 35, "labwareId": "1e610d40-75c7-11ea-b42f-4b64e50f43e5:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1", "wellName": "D2", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 35 } }, { "commandType": "aspirate", - "key": "75c377d3-a93f-4310-9c06-1ee6e1d2fdb1", + "key": "59251222-f64d-400b-98a6-71f95f24bec7", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "volume": 35, "labwareId": "1e610d40-75c7-11ea-b42f-4b64e50f43e5:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1", "wellName": "D2", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 40 } }, { "commandType": "dispense", - "key": "c0ad7408-1f69-4092-8de0-524a0c3991e4", + "key": "a370936b-c12f-4039-88d0-97bb262cb80e", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "volume": 35, "labwareId": "1e610d40-75c7-11ea-b42f-4b64e50f43e5:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1", "wellName": "D2", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 35 } }, { "commandType": "aspirate", - "key": "2c04b095-4f0c-4cd7-a1bc-8daee4e05f38", + "key": "8f428646-3bd6-4a90-9674-23d3e3be8a63", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "volume": 35, "labwareId": "1e610d40-75c7-11ea-b42f-4b64e50f43e5:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1", "wellName": "D2", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 40 } }, { "commandType": "dispense", - "key": "8ead30e3-b057-4994-b00c-c18a838d86ad", + "key": "445797f5-5799-486a-b0e2-299e2f23ca2a", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "volume": 35, "labwareId": "1e610d40-75c7-11ea-b42f-4b64e50f43e5:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1", "wellName": "D2", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 35 } }, { "commandType": "moveToAddressableArea", - "key": "41dc50be-94fb-49f6-9ac6-9c8948622640", + "key": "d740d713-a3cb-4bdb-81a5-798059db8be7", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "addressableAreaName": "fixedTrash", @@ -2845,7 +2890,7 @@ }, { "commandType": "blowOutInPlace", - "key": "4147917f-bca1-4ef4-b055-0610002a3572", + "key": "ba227a58-a0b1-4d83-93f8-4a3566cbedf1", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "flowRate": 35 @@ -2853,7 +2898,7 @@ }, { "commandType": "touchTip", - "key": "5692383d-d3c3-4969-9b76-c5dfd265e4c5", + "key": "68b765bb-a232-49ec-b6be-fc6b375b0a15", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "labwareId": "1e610d40-75c7-11ea-b42f-4b64e50f43e5:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1", @@ -2863,7 +2908,7 @@ }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "e389e5ae-8109-4a68-a8e5-58d96f453a85", + "key": "1464952c-cb00-48eb-a9db-8a4367d3ce0b", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "addressableAreaName": "fixedTrash", @@ -2873,12 +2918,12 @@ }, { "commandType": "dropTipInPlace", - "key": "82048ca4-6ad6-4de9-ad94-4fc698e3aaff", + "key": "2d96c742-46d0-4efa-8e94-3118e975bdd4", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5" } }, { "commandType": "pickUpTip", - "key": "b868a416-8074-4e21-8483-39b0bbc89ba2", + "key": "75a6817c-7f41-4a8c-a184-5e6e7aad51e9", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "labwareId": "0b44c760-75c7-11ea-b42f-4b64e50f43e5:opentrons/opentrons_96_tiprack_300ul/1", @@ -2887,79 +2932,97 @@ }, { "commandType": "aspirate", - "key": "677f3413-0fb0-428d-875a-32d3c8971ca1", + "key": "3e1db7e3-a5eb-473c-a98b-1c91e9b70c3d", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "volume": 35, "labwareId": "1e610d40-75c7-11ea-b42f-4b64e50f43e5:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1", "wellName": "E2", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 40 } }, { "commandType": "dispense", - "key": "6cafa525-b6f6-4ab4-8919-6398ecdcad50", + "key": "d37facff-0753-4d92-9599-93141c97a90f", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "volume": 35, "labwareId": "1e610d40-75c7-11ea-b42f-4b64e50f43e5:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1", "wellName": "E2", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 35 } }, { "commandType": "aspirate", - "key": "ef66610b-0d69-405b-91e9-9d46ef6f9e49", + "key": "df03e618-352a-44e8-8890-859f53229f10", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "volume": 35, "labwareId": "1e610d40-75c7-11ea-b42f-4b64e50f43e5:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1", "wellName": "E2", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 40 } }, { "commandType": "dispense", - "key": "88fbf912-ebaf-4148-9339-2b8fe5d8381d", + "key": "0b93f43f-b456-47fa-b9d7-89086cd9c20b", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "volume": 35, "labwareId": "1e610d40-75c7-11ea-b42f-4b64e50f43e5:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1", "wellName": "E2", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 35 } }, { "commandType": "aspirate", - "key": "86107fa6-c935-4123-bf58-76643dc888d5", + "key": "310303b6-76e3-4765-bd82-042eac727669", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "volume": 35, "labwareId": "1e610d40-75c7-11ea-b42f-4b64e50f43e5:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1", "wellName": "E2", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 40 } }, { "commandType": "dispense", - "key": "2c8d07b1-3f65-4554-99aa-3bc8899a5bd6", + "key": "9881ac40-2932-4197-a03b-77c936651a3b", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "volume": 35, "labwareId": "1e610d40-75c7-11ea-b42f-4b64e50f43e5:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1", "wellName": "E2", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 35 } }, { "commandType": "moveToAddressableArea", - "key": "52740457-0f28-44b5-a053-80a7b8be7932", + "key": "f521a11f-1676-4dc2-a022-f5eba1c5d22e", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "addressableAreaName": "fixedTrash", @@ -2968,7 +3031,7 @@ }, { "commandType": "blowOutInPlace", - "key": "cff38f29-4334-4fe3-a361-465f2ce46be5", + "key": "daede461-9d74-4259-91e6-ecf7ddaa4897", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "flowRate": 35 @@ -2976,7 +3039,7 @@ }, { "commandType": "touchTip", - "key": "e9c841a0-f8e2-4f07-9eb6-6d03764259a6", + "key": "0cde152c-2aeb-4e86-9745-3732e0074ba7", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "labwareId": "1e610d40-75c7-11ea-b42f-4b64e50f43e5:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1", @@ -2986,7 +3049,7 @@ }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "f1dc8237-78ef-4116-88f5-42d426086e63", + "key": "cb24aade-655e-4f6f-83d7-1b60457b56e6", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "addressableAreaName": "fixedTrash", @@ -2996,7 +3059,7 @@ }, { "commandType": "dropTipInPlace", - "key": "c7ebd1ef-9d28-43dc-9fdd-6142a1b22c70", + "key": "6970ad16-6e47-4f5c-afba-3704abe0eabb", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5" } } ], diff --git a/protocol-designer/fixtures/protocol/8/doItAllV4MigratedToV8.json b/protocol-designer/fixtures/protocol/8/doItAllV4MigratedToV8.json index 6a3d3888cba..1e87c78fe87 100644 --- a/protocol-designer/fixtures/protocol/8/doItAllV4MigratedToV8.json +++ b/protocol-designer/fixtures/protocol/8/doItAllV4MigratedToV8.json @@ -6,7 +6,7 @@ "author": "Fixture", "description": "Test all v4 commands", "created": 1585930833548, - "lastModified": 1709303209919, + "lastModified": 1711742493128, "category": null, "subcategory": null, "tags": [] @@ -15,7 +15,7 @@ "name": "opentrons/protocol-designer", "version": "8.1.0", "data": { - "_internalAppBuildDate": "Fri, 01 Mar 2024 14:22:27 GMT", + "_internalAppBuildDate": "Fri, 29 Mar 2024 20:00:04 GMT", "defaultValues": { "aspirate_mmFromBottom": 1, "dispense_mmFromBottom": 0.5, @@ -150,6 +150,10 @@ "dispense_delay_mmFromBottom": "0.5", "dropTip_location": "84882326-9cd3-428e-8352-89f133a1fe5d:trashBin", "nozzles": null, + "dispense_x_position": 0, + "dispense_y_position": 0, + "aspirate_x_position": 0, + "aspirate_y_position": 0, "id": "3961e4c0-75c7-11ea-b42f-4b64e50f43e5", "stepType": "moveLiquid", "stepName": "transfer", @@ -2546,7 +2550,7 @@ "commandSchemaId": "opentronsCommandSchemaV8", "commands": [ { - "key": "ee3dbe0a-f7b1-4995-8449-dea339f61737", + "key": "b7185c84-9b15-4b6e-a315-e331249569fa", "commandType": "loadPipette", "params": { "pipetteName": "p300_single_gen2", @@ -2555,7 +2559,7 @@ } }, { - "key": "248415e4-9ae5-4741-9799-9184775c2d31", + "key": "0d1f6599-70d5-4e99-9608-7d249135b5a9", "commandType": "loadModule", "params": { "model": "magneticModuleV2", @@ -2564,7 +2568,7 @@ } }, { - "key": "94f5969a-7e98-47bc-aa0b-eea46b0271a8", + "key": "2ee81ffe-c8fa-4cac-be56-62a902e301f7", "commandType": "loadModule", "params": { "model": "temperatureModuleV2", @@ -2573,7 +2577,7 @@ } }, { - "key": "2ee5efc8-5c75-4cc6-8bea-0f258478f0af", + "key": "e1da2e62-ac25-405f-b896-99384ab081d8", "commandType": "loadLabware", "params": { "displayName": "Opentrons 96 Tip Rack 300 µL", @@ -2585,7 +2589,7 @@ } }, { - "key": "352f2e8e-87e1-4658-a86e-153e5307f35c", + "key": "2895d8a7-239c-4d6b-afc8-69defe261790", "commandType": "loadLabware", "params": { "displayName": "NEST 96 Well Plate 100 µL PCR Full Skirt", @@ -2599,7 +2603,7 @@ } }, { - "key": "95ee1321-124a-4e78-8b9a-517455c40ab0", + "key": "46b84345-0c06-41f8-860d-1dfafa424e80", "commandType": "loadLabware", "params": { "displayName": "Opentrons 24 Well Aluminum Block with Generic 2 mL Screwcap", @@ -2614,7 +2618,7 @@ }, { "commandType": "loadLiquid", - "key": "44de4f93-8550-465d-b26b-6a2f95d411c1", + "key": "25dd8768-7731-4dee-9f5a-d54b9eb0983c", "params": { "liquidId": "0", "labwareId": "1e610d40-75c7-11ea-b42f-4b64e50f43e5:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1", @@ -2640,7 +2644,7 @@ }, { "commandType": "magneticModule/engage", - "key": "eb54de80-449c-4287-ae26-5fe7cae3fa3a", + "key": "3471fe25-a3a8-4be0-b6d8-545819c4aea0", "params": { "moduleId": "0b419310-75c7-11ea-b42f-4b64e50f43e5:magneticModuleType", "height": 6 @@ -2648,7 +2652,7 @@ }, { "commandType": "temperatureModule/setTargetTemperature", - "key": "a0123190-8242-4c09-bb02-6f78d8c5e493", + "key": "610ae127-200b-48ae-8cbc-7ba4b5ca7b30", "params": { "moduleId": "0b4319b0-75c7-11ea-b42f-4b64e50f43e5:temperatureModuleType", "celsius": 25 @@ -2656,12 +2660,12 @@ }, { "commandType": "waitForDuration", - "key": "6eb18da1-b4ae-4adc-8384-a06b4c21d898", + "key": "94aa4488-7792-49bc-ac3d-6a260bad0f86", "params": { "seconds": 62, "message": "" } }, { "commandType": "pickUpTip", - "key": "ff0fb666-871c-43b8-87d9-9c71fdc0efc9", + "key": "1a838ef5-ea1a-4680-bac0-6eaf473465a4", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "labwareId": "0b44c760-75c7-11ea-b42f-4b64e50f43e5:opentrons/opentrons_96_tiprack_300ul/1", @@ -2670,31 +2674,37 @@ }, { "commandType": "aspirate", - "key": "398bbf30-90e7-4e50-b630-fb02ddd00160", + "key": "f74c2687-f02c-4034-aa03-9a73c1ee47af", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "volume": 30, "labwareId": "1e610d40-75c7-11ea-b42f-4b64e50f43e5:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 46.43 } }, { "commandType": "dispense", - "key": "0b0ff1c4-0167-4980-b710-794df0799956", + "key": "507c7fff-1193-4c14-a0b1-e4bb9fe9d96e", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "volume": 30, "labwareId": "21ed8f60-75c7-11ea-b42f-4b64e50f43e5:opentrons/opentrons_24_aluminumblock_generic_2ml_screwcap/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 46.43 } }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "7c8d4e34-5282-4ee8-bcae-36604b949bde", + "key": "5a050ced-d1a9-4031-bf16-ed49cb561e60", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "addressableAreaName": "fixedTrash", @@ -2704,12 +2714,12 @@ }, { "commandType": "dropTipInPlace", - "key": "7ba94010-e87e-448b-8535-70ad404a5f19", + "key": "8083dcbe-8c00-4178-90c0-4d4a921bca9c", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5" } }, { "commandType": "pickUpTip", - "key": "ceab71fc-ea60-4cbe-8302-7e38a8d27847", + "key": "e6db98b2-7239-4f6b-9e41-02e1dd108ad6", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "labwareId": "0b44c760-75c7-11ea-b42f-4b64e50f43e5:opentrons/opentrons_96_tiprack_300ul/1", @@ -2718,31 +2728,37 @@ }, { "commandType": "aspirate", - "key": "eda46364-da03-4582-9998-dd91945f08fc", + "key": "47cf3011-68e2-40cd-8563-145e460f93aa", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "volume": 30, "labwareId": "1e610d40-75c7-11ea-b42f-4b64e50f43e5:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1", "wellName": "B1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 46.43 } }, { "commandType": "dispense", - "key": "42d98996-7605-4d70-b3be-e6a802022a32", + "key": "1f1d966a-9095-4857-9137-36131c91bfd2", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "volume": 30, "labwareId": "21ed8f60-75c7-11ea-b42f-4b64e50f43e5:opentrons/opentrons_24_aluminumblock_generic_2ml_screwcap/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 46.43 } }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "97c7e6ee-b6c5-4708-bc85-e5cba1c93a1b", + "key": "ac6074f6-2f28-4012-914b-d3b28eb8453d", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "addressableAreaName": "fixedTrash", @@ -2752,12 +2768,12 @@ }, { "commandType": "dropTipInPlace", - "key": "11b30838-4205-4141-9d81-7e2bbde8c7aa", + "key": "074050d3-0c4c-4fc0-8036-a5dc9afe99ef", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5" } }, { "commandType": "temperatureModule/waitForTemperature", - "key": "3748a664-b9d8-49fa-9f6b-3ad35eec5c2b", + "key": "89672a34-bd2f-4e2a-bacc-407bb5f563a1", "params": { "moduleId": "0b4319b0-75c7-11ea-b42f-4b64e50f43e5:temperatureModuleType", "celsius": 25 @@ -2765,19 +2781,19 @@ }, { "commandType": "magneticModule/disengage", - "key": "a1c763ef-3712-495f-998b-651566f3e759", + "key": "26603c88-f0a7-49b3-a65c-37e9e23ac2ff", "params": { "moduleId": "0b419310-75c7-11ea-b42f-4b64e50f43e5:magneticModuleType" } }, { "commandType": "waitForResume", - "key": "f4c1a79c-d774-4a04-9858-2c58f77c93fd", + "key": "f0e0a8c0-01df-47d7-92e5-c3c16e962f4f", "params": { "message": "Wait until user intervention" } }, { "commandType": "temperatureModule/deactivate", - "key": "bb2a6fad-2767-45ad-bc5f-bac249004c00", + "key": "bde12c91-d991-4d57-8d7b-172706f3aa2a", "params": { "moduleId": "0b4319b0-75c7-11ea-b42f-4b64e50f43e5:temperatureModuleType" } diff --git a/protocol-designer/fixtures/protocol/8/doItAllV7MigratedToV8.json b/protocol-designer/fixtures/protocol/8/doItAllV7MigratedToV8.json index bddc1313927..1d78ba01433 100644 --- a/protocol-designer/fixtures/protocol/8/doItAllV7MigratedToV8.json +++ b/protocol-designer/fixtures/protocol/8/doItAllV7MigratedToV8.json @@ -6,7 +6,7 @@ "author": "", "description": "", "created": 1689346890165, - "lastModified": 1711047167434, + "lastModified": 1711742514037, "category": null, "subcategory": null, "tags": [] @@ -15,7 +15,7 @@ "name": "opentrons/protocol-designer", "version": "8.1.0", "data": { - "_internalAppBuildDate": "Fri, 01 Mar 2024 14:22:27 GMT", + "_internalAppBuildDate": "Fri, 29 Mar 2024 20:00:04 GMT", "defaultValues": { "aspirate_mmFromBottom": 1, "dispense_mmFromBottom": 0.5, @@ -194,6 +194,10 @@ "dispense_delay_mmFromBottom": null, "dropTip_location": "4824b094-5999-4549-9e6b-7098a9b30a8b:trashBin", "nozzles": null, + "dispense_x_position": 0, + "dispense_y_position": 0, + "aspirate_x_position": 0, + "aspirate_y_position": 0, "id": "f9a294f1-f42b-4cae-893a-592405349d56", "stepType": "moveLiquid", "stepName": "transfer", @@ -222,6 +226,8 @@ "dropTip_location": "4824b094-5999-4549-9e6b-7098a9b30a8b:trashBin", "nozzles": null, "tipRack": "23ed35de-5bfd-4bb0-8f54-da99a2804ed9:opentrons/opentrons_flex_96_filtertiprack_50ul/1", + "mix_x_position": 0, + "mix_y_position": 0, "id": "5fdb9a12-fab4-42fd-886f-40af107b15d6", "stepType": "mix", "stepName": "mix", @@ -3753,7 +3759,7 @@ "commandSchemaId": "opentronsCommandSchemaV8", "commands": [ { - "key": "6e489e69-6adb-4874-ad9f-4da035825829", + "key": "17a2f6e6-dc06-4c3a-8e97-52728d96dbd5", "commandType": "loadPipette", "params": { "pipetteName": "p1000_single_flex", @@ -3762,7 +3768,7 @@ } }, { - "key": "7536b20c-1416-4e5a-9e0a-2ac13f805fcd", + "key": "23762a87-4d05-4ce1-adaf-b2e7288bfef9", "commandType": "loadPipette", "params": { "pipetteName": "p50_multi_flex", @@ -3771,7 +3777,7 @@ } }, { - "key": "67136a9e-c10f-40ce-80de-920e33d78d44", + "key": "74ed5557-4813-4892-a2e3-4f7710b70d1c", "commandType": "loadModule", "params": { "model": "magneticBlockV1", @@ -3780,7 +3786,7 @@ } }, { - "key": "469e8246-7e19-4654-acdd-7c29a79ce67b", + "key": "00beb9a8-59c7-4c99-b386-0f4214d61350", "commandType": "loadModule", "params": { "model": "heaterShakerModuleV1", @@ -3789,7 +3795,7 @@ } }, { - "key": "29533de7-bd35-458c-9f60-6b9be67bd64b", + "key": "347f3697-2728-4c24-9067-8e9b7d9bd1d6", "commandType": "loadModule", "params": { "model": "temperatureModuleV2", @@ -3798,7 +3804,7 @@ } }, { - "key": "85e3ebf2-4d2f-49e2-8335-cf8c69d58372", + "key": "89c6d0b5-71ed-4bf9-9d94-15375788b86a", "commandType": "loadModule", "params": { "model": "thermocyclerModuleV2", @@ -3807,7 +3813,7 @@ } }, { - "key": "d05c0cc2-d6c2-4fd3-9918-33f7d07bd2fd", + "key": "07ba1a3a-9161-47ee-bf63-501e847bc84d", "commandType": "loadLabware", "params": { "displayName": "Opentrons 96 Flat Bottom Heater-Shaker Adapter", @@ -3821,7 +3827,7 @@ } }, { - "key": "7b515b7c-9d35-4d4e-a19c-1a73de8fdc65", + "key": "c9aafdba-c777-4609-b99f-87405a76a7ec", "commandType": "loadLabware", "params": { "displayName": "Opentrons Flex 96 Filter Tip Rack 50 µL", @@ -3833,7 +3839,7 @@ } }, { - "key": "583e9796-64e7-411e-b3e4-ce3c5f18a39a", + "key": "008af3b3-4557-4755-af65-4e263bcd4d52", "commandType": "loadLabware", "params": { "displayName": "NEST 96 Well Plate 100 µL PCR Full Skirt", @@ -3847,7 +3853,7 @@ } }, { - "key": "bb089d2b-b8f8-4306-b0b8-e5d38d81aba6", + "key": "df64c3d8-c74b-468e-b663-f88c59ed927c", "commandType": "loadLabware", "params": { "displayName": "Opentrons 24 Well Aluminum Block with NEST 1.5 mL Snapcap", @@ -3861,7 +3867,7 @@ } }, { - "key": "aa80f4db-d94f-407c-9ea1-6df86119d200", + "key": "23249708-2910-493b-aa56-a05e687f13ee", "commandType": "loadLabware", "params": { "displayName": "NEST 96 Well Plate 200 µL Flat", @@ -3876,7 +3882,7 @@ }, { "commandType": "loadLiquid", - "key": "52dfe64f-29b5-4d3e-838d-aecf6c0df8e0", + "key": "46b4c996-8800-432b-824a-9f9fb2ae033e", "params": { "liquidId": "1", "labwareId": "a793a135-06aa-4ed6-a1d3-c176c7810afa:opentrons/opentrons_24_aluminumblock_nest_1.5ml_snapcap/1", @@ -3885,7 +3891,7 @@ }, { "commandType": "loadLiquid", - "key": "68e4b018-1e5b-48d4-b858-93b3154e63a5", + "key": "b8e21e25-5da0-426b-a1da-8d87751e48cc", "params": { "liquidId": "0", "labwareId": "fcba73e7-b88e-438e-963e-f8b9a5de0983:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2", @@ -3903,7 +3909,7 @@ }, { "commandType": "temperatureModule/setTargetTemperature", - "key": "18dcba87-324d-4483-a9be-e561c9b47bf0", + "key": "0b60938b-1bd4-4ffb-89f6-dac42a87ac0e", "params": { "moduleId": "ef44ad7f-0fd9-46d6-8bc0-c70785644cc8:temperatureModuleType", "celsius": 4 @@ -3911,7 +3917,7 @@ }, { "commandType": "heaterShaker/waitForTemperature", - "key": "84ae2f28-38f6-4314-9d34-3ff9af5a875c", + "key": "7d5fd109-43cd-4dea-b0fb-2efa3f727e38", "params": { "moduleId": "c19dffa3-cb34-4702-bcf6-dcea786257d1:heaterShakerModuleType", "celsius": 4 @@ -3919,14 +3925,14 @@ }, { "commandType": "thermocycler/closeLid", - "key": "0ec55f7b-4f82-46ad-a450-aac71d8ca198", + "key": "31bb9bbe-9c53-407a-ac73-e789b800466d", "params": { "moduleId": "627b7a27-5bb7-46de-a530-67af45652e3b:thermocyclerModuleType" } }, { "commandType": "thermocycler/setTargetLidTemperature", - "key": "883d4fba-7b4c-410d-aeff-79ce4c3d106e", + "key": "0d83be22-5cec-4603-b42c-03ffb6e6d8ba", "params": { "moduleId": "627b7a27-5bb7-46de-a530-67af45652e3b:thermocyclerModuleType", "celsius": 40 @@ -3934,14 +3940,14 @@ }, { "commandType": "thermocycler/waitForLidTemperature", - "key": "8da2d0c4-c9b2-4acb-8230-6b68f33b92b7", + "key": "1ac36b4e-b0df-4d43-9cfc-a10cc64ccda3", "params": { "moduleId": "627b7a27-5bb7-46de-a530-67af45652e3b:thermocyclerModuleType" } }, { "commandType": "thermocycler/runProfile", - "key": "6df23192-439c-429c-89ab-c932751096f0", + "key": "0917c6de-9fd8-4afa-b496-f62ae18fa290", "params": { "moduleId": "627b7a27-5bb7-46de-a530-67af45652e3b:thermocyclerModuleType", "profile": [ @@ -3953,28 +3959,28 @@ }, { "commandType": "thermocycler/deactivateBlock", - "key": "c99151db-add8-41e4-9e5b-0516198f06b4", + "key": "4e5e9302-fac9-438d-83c9-fabd4c65791f", "params": { "moduleId": "627b7a27-5bb7-46de-a530-67af45652e3b:thermocyclerModuleType" } }, { "commandType": "thermocycler/deactivateLid", - "key": "2cf60177-6b03-4706-a2fb-7a211eb974e1", + "key": "a0fe06fa-e4cc-4de2-97a9-388a3df08111", "params": { "moduleId": "627b7a27-5bb7-46de-a530-67af45652e3b:thermocyclerModuleType" } }, { "commandType": "thermocycler/openLid", - "key": "968e4a04-1cda-4730-a26d-810c0af827ad", + "key": "8706cf32-b7c8-41ee-901a-6e62ef7b6824", "params": { "moduleId": "627b7a27-5bb7-46de-a530-67af45652e3b:thermocyclerModuleType" } }, { "commandType": "pickUpTip", - "key": "39f9b118-55f7-4b32-b28a-689255ecf69a", + "key": "90d3558e-e3ef-4e11-8e18-9e1312b212b0", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "23ed35de-5bfd-4bb0-8f54-da99a2804ed9:opentrons/opentrons_flex_96_filtertiprack_50ul/1", @@ -3983,31 +3989,37 @@ }, { "commandType": "aspirate", - "key": "cbd4de4c-8de3-407e-aace-9d0993c48214", + "key": "c7ac4218-4698-48f4-b00d-8eeb1ffddb3a", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "a793a135-06aa-4ed6-a1d3-c176c7810afa:opentrons/opentrons_24_aluminumblock_nest_1.5ml_snapcap/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "dispense", - "key": "df9dcd8b-54a2-4ea3-9014-8d1e5a8769c7", + "key": "604c9a1d-1ada-4159-850f-3bc9e4f802bc", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "fcba73e7-b88e-438e-963e-f8b9a5de0983:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "41586872-d7ba-47a4-b545-aca0f924e1e5", + "key": "c120780c-b4f4-4b11-a7f6-ab3b2621106f", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "addressableAreaName": "movableTrashA3", @@ -4017,12 +4029,12 @@ }, { "commandType": "dropTipInPlace", - "key": "1a253e2c-f44c-414b-929e-fb071233caa5", + "key": "2b9bb184-749e-4652-a2cb-31e427ae0472", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe" } }, { "commandType": "pickUpTip", - "key": "903f0cbd-36fc-46ad-9629-af0e713a2551", + "key": "24425f50-40ff-453a-9c3e-ba35f07a4b93", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "23ed35de-5bfd-4bb0-8f54-da99a2804ed9:opentrons/opentrons_flex_96_filtertiprack_50ul/1", @@ -4031,31 +4043,37 @@ }, { "commandType": "aspirate", - "key": "15e1819f-a75f-4a99-b248-ef9c44f742bb", + "key": "3eacc9b8-99bf-448b-b178-1638c2217d4f", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "a793a135-06aa-4ed6-a1d3-c176c7810afa:opentrons/opentrons_24_aluminumblock_nest_1.5ml_snapcap/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "dispense", - "key": "1746f838-436c-4524-99ca-79a89807e7c6", + "key": "b2f71d3b-13b3-4ba5-9672-3a5ae85b402e", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "fcba73e7-b88e-438e-963e-f8b9a5de0983:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "139ac8eb-77bc-4dd5-8358-5cd3352b2841", + "key": "982eb315-0f07-4db4-804d-3650a7ef3371", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "addressableAreaName": "movableTrashA3", @@ -4065,12 +4083,12 @@ }, { "commandType": "dropTipInPlace", - "key": "7d3f53de-e3e9-4cce-aca3-a1c84ec8e4f5", + "key": "fd1e4fcb-3f57-4e0e-9a07-f5710d713b2b", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe" } }, { "commandType": "pickUpTip", - "key": "519b1444-568a-49af-87cc-d251a28d5c74", + "key": "d1aa96b8-8218-497f-92d1-9d145d65cacd", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "23ed35de-5bfd-4bb0-8f54-da99a2804ed9:opentrons/opentrons_flex_96_filtertiprack_50ul/1", @@ -4079,31 +4097,37 @@ }, { "commandType": "aspirate", - "key": "eef96aa3-ed34-4a6c-a121-9bb1556016aa", + "key": "49b8562e-7d04-409e-b96e-60c04d82f890", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "a793a135-06aa-4ed6-a1d3-c176c7810afa:opentrons/opentrons_24_aluminumblock_nest_1.5ml_snapcap/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "dispense", - "key": "ee44942b-3da5-43c1-ad0b-17fc2ea46b68", + "key": "16da2628-d7fa-45e9-9911-cb06a61e488e", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "fcba73e7-b88e-438e-963e-f8b9a5de0983:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2", "wellName": "B1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "1cad272e-a562-4abe-975a-209d5b29f6b8", + "key": "a7a1c2f8-6fdf-4322-a216-ca06fe064299", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "addressableAreaName": "movableTrashA3", @@ -4113,12 +4137,12 @@ }, { "commandType": "dropTipInPlace", - "key": "b25eeb63-22a3-4a45-9a34-2588f6e28034", + "key": "dcfb2a3c-fec6-467e-8ea4-0655e070857c", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe" } }, { "commandType": "pickUpTip", - "key": "3b4257ba-7e37-4dc7-8630-628695a993b0", + "key": "c54b1b14-a78e-4b3b-a7fd-df600c143996", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "23ed35de-5bfd-4bb0-8f54-da99a2804ed9:opentrons/opentrons_flex_96_filtertiprack_50ul/1", @@ -4127,31 +4151,37 @@ }, { "commandType": "aspirate", - "key": "9ee178b8-6918-477a-ac4a-3b94b3257ded", + "key": "1f586aaa-a2c3-4f35-98d4-514f30f8afde", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "a793a135-06aa-4ed6-a1d3-c176c7810afa:opentrons/opentrons_24_aluminumblock_nest_1.5ml_snapcap/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "dispense", - "key": "5b1fa15c-d50a-465e-99d6-d3c5631ba18b", + "key": "8491a928-c8ae-4b73-8fd3-43e6e520ea7d", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "fcba73e7-b88e-438e-963e-f8b9a5de0983:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2", "wellName": "B1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "8ca34094-5af4-4c19-bbc9-e6a95ebe7dc7", + "key": "3ddc68fb-3f9e-4395-b234-a8f00b35cf97", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "addressableAreaName": "movableTrashA3", @@ -4161,12 +4191,12 @@ }, { "commandType": "dropTipInPlace", - "key": "ed570c7c-bec9-4320-92c6-c5dc4e8ea039", + "key": "c1596fb8-587a-4a9c-9dd0-252dd821085c", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe" } }, { "commandType": "pickUpTip", - "key": "24af8dc9-1d39-4ead-9fb7-c106cf81c4ca", + "key": "9e130b45-4d49-4588-adef-2e4055be2e09", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "23ed35de-5bfd-4bb0-8f54-da99a2804ed9:opentrons/opentrons_flex_96_filtertiprack_50ul/1", @@ -4175,31 +4205,37 @@ }, { "commandType": "aspirate", - "key": "3fb7136f-156c-4ef2-ba9c-4010cbee7c45", + "key": "7e014576-f260-4b18-aad5-f45423adb35f", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "a793a135-06aa-4ed6-a1d3-c176c7810afa:opentrons/opentrons_24_aluminumblock_nest_1.5ml_snapcap/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "dispense", - "key": "70415053-47db-400e-a765-59930e782fba", + "key": "07e28184-9669-432a-9b68-8dd692680fa5", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "fcba73e7-b88e-438e-963e-f8b9a5de0983:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2", "wellName": "C1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "6bc262a7-6619-4c1d-90ab-694881833ef2", + "key": "4f591d38-4cc1-496b-90dc-fdcff81d3155", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "addressableAreaName": "movableTrashA3", @@ -4209,12 +4245,12 @@ }, { "commandType": "dropTipInPlace", - "key": "61c26a5d-05d6-490b-9881-2505f334e148", + "key": "fab6cdf0-a1c5-4643-9d0c-4fce01d88c7f", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe" } }, { "commandType": "pickUpTip", - "key": "e7db6545-1f4f-40bd-a440-08afc48c8a6d", + "key": "7407659a-a612-4209-967b-af9750324a07", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "23ed35de-5bfd-4bb0-8f54-da99a2804ed9:opentrons/opentrons_flex_96_filtertiprack_50ul/1", @@ -4223,31 +4259,37 @@ }, { "commandType": "aspirate", - "key": "a6a1af44-5a04-4fdf-aecb-afec57d49809", + "key": "3d307bba-026c-4a9a-8d01-ae93e8cdce1f", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "a793a135-06aa-4ed6-a1d3-c176c7810afa:opentrons/opentrons_24_aluminumblock_nest_1.5ml_snapcap/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "dispense", - "key": "fa36f3c3-8a96-4643-adc3-1c856446c432", + "key": "f45088fb-f102-4edf-ad26-5d1d0ac4f215", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "fcba73e7-b88e-438e-963e-f8b9a5de0983:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2", "wellName": "C1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "a3b84d03-7b16-4c8b-ab04-5e80d53c5008", + "key": "3b44aeec-fd56-4fcf-badf-5cdc42ed42c7", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "addressableAreaName": "movableTrashA3", @@ -4257,12 +4299,12 @@ }, { "commandType": "dropTipInPlace", - "key": "b3b763fc-d538-4b1c-9a72-8a1c85238e55", + "key": "7a58db8b-f053-46b5-bd89-3a7cba9c1af1", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe" } }, { "commandType": "pickUpTip", - "key": "aeb3764a-c819-4151-acb9-77d07399b13d", + "key": "6449dbc6-430e-468c-863d-3233689c8a63", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "23ed35de-5bfd-4bb0-8f54-da99a2804ed9:opentrons/opentrons_flex_96_filtertiprack_50ul/1", @@ -4271,31 +4313,37 @@ }, { "commandType": "aspirate", - "key": "9b15d50e-e1a3-4966-8584-5e32da3afa4c", + "key": "fe2b869a-8d1f-47bf-9688-2deae97b30f9", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "a793a135-06aa-4ed6-a1d3-c176c7810afa:opentrons/opentrons_24_aluminumblock_nest_1.5ml_snapcap/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "dispense", - "key": "ada5949a-0085-4f09-b65e-db092a2ddab6", + "key": "e9a20fb6-f0ba-4e25-b1e5-67dbef00f2d0", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "fcba73e7-b88e-438e-963e-f8b9a5de0983:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2", "wellName": "D1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "fbfd8391-eb1e-416f-97b0-4ea4e631b8b5", + "key": "c0c7ae2d-6b13-4ce7-b170-5a2ffb3cc066", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "addressableAreaName": "movableTrashA3", @@ -4305,12 +4353,12 @@ }, { "commandType": "dropTipInPlace", - "key": "edc2c62c-4f1e-4b9d-958c-1f9d4594759b", + "key": "eea51b62-8fd2-4c34-8929-48e26c670640", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe" } }, { "commandType": "pickUpTip", - "key": "e9f79e00-bc1f-4f92-9299-401a7d85d78b", + "key": "0a59af4d-5196-4c16-b609-98c565c320da", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "23ed35de-5bfd-4bb0-8f54-da99a2804ed9:opentrons/opentrons_flex_96_filtertiprack_50ul/1", @@ -4319,31 +4367,37 @@ }, { "commandType": "aspirate", - "key": "f361f6e5-a558-48dd-b808-383d48305944", + "key": "cc1387fe-4e22-407f-b1f6-8e57153d24d1", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "a793a135-06aa-4ed6-a1d3-c176c7810afa:opentrons/opentrons_24_aluminumblock_nest_1.5ml_snapcap/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "dispense", - "key": "ad9f20e5-c018-4241-8ee4-0b94bd4b4b13", + "key": "fdbb2c46-7e42-4dc9-95dd-528397fe2a49", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "fcba73e7-b88e-438e-963e-f8b9a5de0983:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2", "wellName": "D1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "c41c44f9-7969-4b9d-966f-7133927d1746", + "key": "f8000789-3db0-4edc-adaa-234a89c0a2e8", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "addressableAreaName": "movableTrashA3", @@ -4353,12 +4407,12 @@ }, { "commandType": "dropTipInPlace", - "key": "e2deb31d-b531-46a0-95b9-894c6143b51e", + "key": "4a6423a4-3fb3-41cb-a2bb-769f882da188", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe" } }, { "commandType": "pickUpTip", - "key": "ad15920f-80f3-4b31-8c69-7a9753760ac5", + "key": "58db6a04-8af4-4580-8b3b-71d27448d36c", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "23ed35de-5bfd-4bb0-8f54-da99a2804ed9:opentrons/opentrons_flex_96_filtertiprack_50ul/1", @@ -4367,31 +4421,37 @@ }, { "commandType": "aspirate", - "key": "ee11c611-bd0f-4039-b631-738aceae4b8b", + "key": "6c053630-6298-4bae-8b1b-b7c0fd60cd64", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "a793a135-06aa-4ed6-a1d3-c176c7810afa:opentrons/opentrons_24_aluminumblock_nest_1.5ml_snapcap/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "dispense", - "key": "dace1ad5-22a3-4731-8a0e-54378e936e41", + "key": "60bddd52-347b-4e97-af4f-227172c9e383", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "fcba73e7-b88e-438e-963e-f8b9a5de0983:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2", "wellName": "E1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "c0075e01-b1d6-41fc-a482-541eba3dd9ce", + "key": "dcc5e7a5-ce62-40b0-94a8-19ccd9ec7783", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "addressableAreaName": "movableTrashA3", @@ -4401,12 +4461,12 @@ }, { "commandType": "dropTipInPlace", - "key": "287153c4-baee-46c6-81e0-db0cd90a8d7b", + "key": "2d0d4405-02e0-44d3-9aa9-093b2bcf8693", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe" } }, { "commandType": "pickUpTip", - "key": "e956cb88-1387-4014-aea0-06237c9ea125", + "key": "8f943b62-e5cc-423b-962d-c9f06a3c39e6", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "23ed35de-5bfd-4bb0-8f54-da99a2804ed9:opentrons/opentrons_flex_96_filtertiprack_50ul/1", @@ -4415,31 +4475,37 @@ }, { "commandType": "aspirate", - "key": "3d870b55-af49-42b0-b877-7a3777fea82d", + "key": "d38287f1-db91-4479-a811-6190c472a797", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "a793a135-06aa-4ed6-a1d3-c176c7810afa:opentrons/opentrons_24_aluminumblock_nest_1.5ml_snapcap/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "dispense", - "key": "8d06248a-65a5-4372-a666-70ec956df1d4", + "key": "10074111-ee60-4602-8749-326cc7c978ef", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "fcba73e7-b88e-438e-963e-f8b9a5de0983:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2", "wellName": "E1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "3304feb9-69e8-4e70-a3b6-308706f06d0c", + "key": "f0b5078d-30e7-4ae8-bd9c-2380a2acc248", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "addressableAreaName": "movableTrashA3", @@ -4449,12 +4515,12 @@ }, { "commandType": "dropTipInPlace", - "key": "2f73eac1-8a7a-4bb2-b634-3c938bb6a541", + "key": "babbd4a6-95d0-46ef-9616-15435bf83e0c", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe" } }, { "commandType": "pickUpTip", - "key": "b08587bf-ac3a-4322-baa4-2d54ab4c74d8", + "key": "44873109-2a10-4925-a393-b3f05ac65cc8", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "23ed35de-5bfd-4bb0-8f54-da99a2804ed9:opentrons/opentrons_flex_96_filtertiprack_50ul/1", @@ -4463,31 +4529,37 @@ }, { "commandType": "aspirate", - "key": "627b7b39-4646-42e5-8a6b-ab85c073631a", + "key": "956b196e-e6a0-4e04-9fe4-e54e8f366cd3", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "a793a135-06aa-4ed6-a1d3-c176c7810afa:opentrons/opentrons_24_aluminumblock_nest_1.5ml_snapcap/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "dispense", - "key": "f96068bb-8730-46d2-9649-e77060a67d96", + "key": "d9fe1d4f-558e-48e9-9c4f-3349a513da68", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "fcba73e7-b88e-438e-963e-f8b9a5de0983:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2", "wellName": "F1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "717da31c-723f-4761-8a9e-c46e3a0d95d9", + "key": "3410b8d0-d4be-4009-be92-13d7165fa45d", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "addressableAreaName": "movableTrashA3", @@ -4497,12 +4569,12 @@ }, { "commandType": "dropTipInPlace", - "key": "9b69289d-3b6c-4caa-ab71-eee65591d5d7", + "key": "b610b324-aa96-44ed-95d0-fa7b6b2771f7", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe" } }, { "commandType": "pickUpTip", - "key": "61f66087-286a-4309-8f56-e95e1d3450db", + "key": "37fe97fb-40d5-449a-ab57-995eb34db25b", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "23ed35de-5bfd-4bb0-8f54-da99a2804ed9:opentrons/opentrons_flex_96_filtertiprack_50ul/1", @@ -4511,31 +4583,37 @@ }, { "commandType": "aspirate", - "key": "fcd20bdc-8cd3-4dc4-88ff-572317dfeafa", + "key": "f8fe5dca-1294-4f9a-8b05-7b818317070a", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "a793a135-06aa-4ed6-a1d3-c176c7810afa:opentrons/opentrons_24_aluminumblock_nest_1.5ml_snapcap/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "dispense", - "key": "cc4e76bc-f89a-45e5-8200-1bfd3ba3c950", + "key": "9ae4ac38-6188-4e0a-82b1-c8682052eab7", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "fcba73e7-b88e-438e-963e-f8b9a5de0983:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2", "wellName": "F1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "f98a9dd9-6368-45ad-bf34-374ebd304017", + "key": "5a3d6103-e920-419f-8541-6f42aead55b4", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "addressableAreaName": "movableTrashA3", @@ -4545,12 +4623,12 @@ }, { "commandType": "dropTipInPlace", - "key": "28f596a5-d900-4335-82d1-c69c2a4f3476", + "key": "ae7fa272-1052-4b9b-9141-832de7f191ae", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe" } }, { "commandType": "pickUpTip", - "key": "cf3e0222-4b78-4c11-8f7c-853edb173ef0", + "key": "001e1eff-7e3a-4762-889b-81bbdd95624e", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "23ed35de-5bfd-4bb0-8f54-da99a2804ed9:opentrons/opentrons_flex_96_filtertiprack_50ul/1", @@ -4559,31 +4637,37 @@ }, { "commandType": "aspirate", - "key": "87bf3bc3-d986-4d4e-9303-531db931ecf2", + "key": "4c857bd6-9ee5-4abc-b8f1-93f263421d4f", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "a793a135-06aa-4ed6-a1d3-c176c7810afa:opentrons/opentrons_24_aluminumblock_nest_1.5ml_snapcap/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "dispense", - "key": "7d4a2f34-efc4-4c69-ac95-12e90da24846", + "key": "d54ee4e1-019a-4043-a9d6-73f2728ade40", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "fcba73e7-b88e-438e-963e-f8b9a5de0983:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2", "wellName": "G1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "c26ac9f4-f86a-410b-9524-3a107df76154", + "key": "b3d1a836-8198-4543-9c69-5af4340f5e7b", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "addressableAreaName": "movableTrashA3", @@ -4593,12 +4677,12 @@ }, { "commandType": "dropTipInPlace", - "key": "cc9ad177-40b1-4063-904f-bdcd5f534ba3", + "key": "d09a4c10-5d65-46b2-aa72-04ebd1e69616", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe" } }, { "commandType": "pickUpTip", - "key": "6f27f601-b3a2-4d72-b1c7-d29fbef1e2fb", + "key": "a18317f2-d1e8-4960-8294-d041900be78c", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "23ed35de-5bfd-4bb0-8f54-da99a2804ed9:opentrons/opentrons_flex_96_filtertiprack_50ul/1", @@ -4607,31 +4691,37 @@ }, { "commandType": "aspirate", - "key": "0d614bd3-9723-467a-a8f4-6793875071a6", + "key": "7b5f0098-2f53-4e57-b60a-46c06f4fe167", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "a793a135-06aa-4ed6-a1d3-c176c7810afa:opentrons/opentrons_24_aluminumblock_nest_1.5ml_snapcap/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "dispense", - "key": "321bc387-8de7-4456-99d2-f61e31f49ac4", + "key": "e6497c4f-50da-481e-b76d-a6787df6a779", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "fcba73e7-b88e-438e-963e-f8b9a5de0983:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2", "wellName": "G1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "23c76ed2-6751-4f8d-b061-02e54bae4b92", + "key": "7c357bd8-9b73-43d0-a143-57d9b24d651f", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "addressableAreaName": "movableTrashA3", @@ -4641,12 +4731,12 @@ }, { "commandType": "dropTipInPlace", - "key": "a33f1f9c-c1eb-42c5-b2d2-dc81d0c92207", + "key": "5ea9d3e3-5c64-4610-bbfc-b71d7e4d3282", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe" } }, { "commandType": "pickUpTip", - "key": "fa8827df-09aa-41df-b2af-794dafab3f36", + "key": "17f52737-8fa4-45df-95e1-e95011c308fd", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "23ed35de-5bfd-4bb0-8f54-da99a2804ed9:opentrons/opentrons_flex_96_filtertiprack_50ul/1", @@ -4655,31 +4745,37 @@ }, { "commandType": "aspirate", - "key": "222b1e4d-81fb-44c5-beff-d39ae967766c", + "key": "43f318b4-d316-462b-9d38-d4969cac5494", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "a793a135-06aa-4ed6-a1d3-c176c7810afa:opentrons/opentrons_24_aluminumblock_nest_1.5ml_snapcap/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "dispense", - "key": "d6644b59-fd5b-44cf-a5b4-da4bec2ffcd1", + "key": "ed84c3b2-b095-49bc-939b-fd1f5faa6ddd", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "fcba73e7-b88e-438e-963e-f8b9a5de0983:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2", "wellName": "H1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "62a05dff-fd3d-47ec-a852-7ec365fdc60a", + "key": "bdde31de-c35d-403a-bc01-d249c21100dd", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "addressableAreaName": "movableTrashA3", @@ -4689,12 +4785,12 @@ }, { "commandType": "dropTipInPlace", - "key": "dcbbb300-64a6-4deb-9733-19b86574106a", + "key": "2d5caff3-718e-4835-90c1-3a0d2ec57a20", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe" } }, { "commandType": "pickUpTip", - "key": "882ee6ef-f2a3-4478-b88a-fd650c11cbc9", + "key": "a9e33581-f053-47cb-9bc4-069dca4fbc1c", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "23ed35de-5bfd-4bb0-8f54-da99a2804ed9:opentrons/opentrons_flex_96_filtertiprack_50ul/1", @@ -4703,31 +4799,37 @@ }, { "commandType": "aspirate", - "key": "4745b4e5-0fed-427b-961a-c1e346e5f591", + "key": "5b34a48a-fdf2-4ad1-8c14-3da9ffb680ed", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "a793a135-06aa-4ed6-a1d3-c176c7810afa:opentrons/opentrons_24_aluminumblock_nest_1.5ml_snapcap/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "dispense", - "key": "916ce3bf-32a1-45e1-b704-32b75db4e572", + "key": "13cfa89d-7337-4358-86d2-0da34380835d", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "fcba73e7-b88e-438e-963e-f8b9a5de0983:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2", "wellName": "H1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "62fc2bbb-ee9f-4769-9c6d-7eae1d56b7f3", + "key": "ec94b555-dba0-4757-be27-7b8634c55a9a", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "addressableAreaName": "movableTrashA3", @@ -4737,12 +4839,12 @@ }, { "commandType": "dropTipInPlace", - "key": "459eed51-26e1-4f67-946c-5f281d9ff5c4", + "key": "efba76a3-5a32-4f02-9dfc-2f1e5ff3e9b6", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe" } }, { "commandType": "pickUpTip", - "key": "a11d26ea-07d0-4fbe-95a8-3229bf3a7974", + "key": "844f8618-5db6-48ba-b0af-ffc12e84eea7", "params": { "pipetteId": "6d1e53c3-2db3-451b-ad60-3fe13781a193", "labwareId": "23ed35de-5bfd-4bb0-8f54-da99a2804ed9:opentrons/opentrons_flex_96_filtertiprack_50ul/1", @@ -4751,7 +4853,7 @@ }, { "commandType": "configureForVolume", - "key": "8355523c-68f9-4d67-b7f0-c19b39a20d58", + "key": "5d899711-013e-460b-845b-9a8ef207dc24", "params": { "pipetteId": "6d1e53c3-2db3-451b-ad60-3fe13781a193", "volume": 10 @@ -4759,55 +4861,67 @@ }, { "commandType": "aspirate", - "key": "cb6f6fd9-ce96-4788-930a-bcb4fc73cab8", + "key": "71923e56-ac8f-486c-9509-c809a994e006", "params": { "pipetteId": "6d1e53c3-2db3-451b-ad60-3fe13781a193", "volume": 10, "labwareId": "fcba73e7-b88e-438e-963e-f8b9a5de0983:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 8 } }, { "commandType": "dispense", - "key": "d72adc76-8ece-47d3-86d2-5ccec5b507a4", + "key": "abde93a4-98e5-428c-9dd9-2a65dc3d99bf", "params": { "pipetteId": "6d1e53c3-2db3-451b-ad60-3fe13781a193", "volume": 10, "labwareId": "fcba73e7-b88e-438e-963e-f8b9a5de0983:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 8 } }, { "commandType": "aspirate", - "key": "aefc83b0-fcba-4010-b71a-9b79e6134779", + "key": "f9a0576f-5764-478e-bf16-03ef8ab46d3b", "params": { "pipetteId": "6d1e53c3-2db3-451b-ad60-3fe13781a193", "volume": 10, "labwareId": "fcba73e7-b88e-438e-963e-f8b9a5de0983:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 8 } }, { "commandType": "dispense", - "key": "955873e5-8fcd-43d4-9f6c-3281922fc97d", + "key": "e1e8644f-f0d0-4946-b599-55d62174b5af", "params": { "pipetteId": "6d1e53c3-2db3-451b-ad60-3fe13781a193", "volume": 10, "labwareId": "fcba73e7-b88e-438e-963e-f8b9a5de0983:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 8 } }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "a237c669-fe56-434a-a5fc-225ba5403b28", + "key": "9bb9217e-3c87-4b11-81f4-01aeb6d12bcd", "params": { "pipetteId": "6d1e53c3-2db3-451b-ad60-3fe13781a193", "addressableAreaName": "movableTrashA3", @@ -4817,12 +4931,12 @@ }, { "commandType": "dropTipInPlace", - "key": "87d1c5d5-c40e-480a-a0bf-4a8a63bae3cd", + "key": "dd0506d4-cd19-4fa3-85db-64aef25d8f75", "params": { "pipetteId": "6d1e53c3-2db3-451b-ad60-3fe13781a193" } }, { "commandType": "moveLabware", - "key": "98e741c6-01c6-43d5-8d98-e3950b7dabda", + "key": "bd579612-fa2a-4808-ade0-8e38b9d8b7da", "params": { "labwareId": "fcba73e7-b88e-438e-963e-f8b9a5de0983:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2", "strategy": "usingGripper", @@ -4831,12 +4945,12 @@ }, { "commandType": "waitForDuration", - "key": "da873c66-a7e4-4810-b2d1-ab037e9156d1", + "key": "da8a328a-2870-4259-b3be-89d3255154fb", "params": { "seconds": 60, "message": "" } }, { "commandType": "moveLabware", - "key": "58ced158-ae4d-4275-814b-cf29bacda1c0", + "key": "64ac3bcc-4ab8-4d15-9b42-d2462686153d", "params": { "labwareId": "fcba73e7-b88e-438e-963e-f8b9a5de0983:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2", "strategy": "usingGripper", @@ -4845,21 +4959,21 @@ }, { "commandType": "heaterShaker/closeLabwareLatch", - "key": "2706f0f9-5898-47e3-b53f-06df74598b4e", + "key": "cd0e65dc-cd6c-4d0f-b05f-3d8a979d7d09", "params": { "moduleId": "c19dffa3-cb34-4702-bcf6-dcea786257d1:heaterShakerModuleType" } }, { "commandType": "heaterShaker/deactivateHeater", - "key": "073eac12-998e-4075-bcb5-feb215d5f251", + "key": "c980a10c-a99c-4583-831b-8f09f89822fd", "params": { "moduleId": "c19dffa3-cb34-4702-bcf6-dcea786257d1:heaterShakerModuleType" } }, { "commandType": "heaterShaker/setAndWaitForShakeSpeed", - "key": "37fedb11-cfa0-494d-905f-e5539c2960e6", + "key": "15a3aeed-9bd0-49d6-8a6e-43f226e7acfe", "params": { "moduleId": "c19dffa3-cb34-4702-bcf6-dcea786257d1:heaterShakerModuleType", "rpm": 500 @@ -4867,28 +4981,28 @@ }, { "commandType": "heaterShaker/deactivateHeater", - "key": "6431c63c-608d-4f82-bc68-ade76b8c1cc2", + "key": "001d2bdd-b8a2-4285-8aa3-9d9318566b47", "params": { "moduleId": "c19dffa3-cb34-4702-bcf6-dcea786257d1:heaterShakerModuleType" } }, { "commandType": "heaterShaker/deactivateShaker", - "key": "a22834db-00b7-4f66-a681-9cc5dd17031e", + "key": "609e5b71-9dda-47d7-a7c4-0da3802e7e99", "params": { "moduleId": "c19dffa3-cb34-4702-bcf6-dcea786257d1:heaterShakerModuleType" } }, { "commandType": "heaterShaker/openLabwareLatch", - "key": "af548bbd-5620-405a-bda0-ac08ca06fbbd", + "key": "bb80d557-573b-4b09-a0b8-5d73ea22e4a4", "params": { "moduleId": "c19dffa3-cb34-4702-bcf6-dcea786257d1:heaterShakerModuleType" } }, { "commandType": "moveLabware", - "key": "ffb00dfa-99db-4744-9bde-538d0aa7b1a7", + "key": "a37c38e0-7abe-433f-ab9d-adf0774565f6", "params": { "labwareId": "a793a135-06aa-4ed6-a1d3-c176c7810afa:opentrons/opentrons_24_aluminumblock_nest_1.5ml_snapcap/1", "strategy": "manualMoveWithPause", @@ -4897,14 +5011,14 @@ }, { "commandType": "temperatureModule/deactivate", - "key": "8e0e035d-33a2-4995-a557-26c7951de915", + "key": "1558d15f-e4b6-48bb-8c9c-c3ff69812504", "params": { "moduleId": "ef44ad7f-0fd9-46d6-8bc0-c70785644cc8:temperatureModuleType" } }, { "commandType": "moveLabware", - "key": "36c8ae59-9d10-428a-b6fd-3e3b6b49ed09", + "key": "d805d58b-f6e7-406d-8262-5bf3d03448b6", "params": { "labwareId": "239ceac8-23ec-4900-810a-70aeef880273:opentrons/nest_96_wellplate_200ul_flat/2", "strategy": "manualMoveWithPause", diff --git a/protocol-designer/fixtures/protocol/8/doItAllV8.json b/protocol-designer/fixtures/protocol/8/doItAllV8.json index 79c866f5399..a6b1f61a737 100644 --- a/protocol-designer/fixtures/protocol/8/doItAllV8.json +++ b/protocol-designer/fixtures/protocol/8/doItAllV8.json @@ -6,7 +6,7 @@ "author": "", "description": "", "created": 1701659107408, - "lastModified": 1711047424926, + "lastModified": 1711742533084, "category": null, "subcategory": null, "tags": [] @@ -15,7 +15,7 @@ "name": "opentrons/protocol-designer", "version": "8.1.0", "data": { - "_internalAppBuildDate": "Thu, 21 Mar 2024 18:51:59 GMT", + "_internalAppBuildDate": "Fri, 29 Mar 2024 20:00:04 GMT", "defaultValues": { "aspirate_mmFromBottom": 1, "dispense_mmFromBottom": 0.5, @@ -154,6 +154,10 @@ "dispense_delay_mmFromBottom": null, "dropTip_location": "9d61f642-8f9b-467d-b2f7-b67fb162fd26:wasteChute", "nozzles": null, + "dispense_x_position": 0, + "dispense_y_position": 0, + "aspirate_x_position": 0, + "aspirate_y_position": 0, "id": "d2f74144-a7bf-4ba2-aaab-30d70b2b62c7", "stepType": "moveLiquid", "stepName": "transfer", @@ -3421,7 +3425,7 @@ "commandSchemaId": "opentronsCommandSchemaV8", "commands": [ { - "key": "1809fd39-db28-4928-8773-31bc536fe765", + "key": "f8a4cabe-7cb9-4e38-b937-6655680e2a31", "commandType": "loadPipette", "params": { "pipetteName": "p1000_single_flex", @@ -3430,7 +3434,7 @@ } }, { - "key": "3a5f75b2-15c9-404f-9b87-f102beeb1a45", + "key": "cd2e6185-8d57-4881-9b0c-ebcbd2468c55", "commandType": "loadModule", "params": { "model": "heaterShakerModuleV1", @@ -3439,7 +3443,7 @@ } }, { - "key": "a13ba2f1-e557-4d2f-a304-87847ce68887", + "key": "b2d44cd2-73db-45b3-ab22-e9e765beed75", "commandType": "loadModule", "params": { "model": "thermocyclerModuleV2", @@ -3448,7 +3452,7 @@ } }, { - "key": "e3f1abb9-b076-4b56-a593-0b4033462fea", + "key": "bbd3ee7e-35b8-4168-9df5-13b871c6dfba", "commandType": "loadLabware", "params": { "displayName": "Opentrons 96 PCR Heater-Shaker Adapter", @@ -3462,7 +3466,7 @@ } }, { - "key": "9d92792f-e5d1-4259-8e4b-da8ea83f28df", + "key": "198896f6-4d0e-49ee-b060-bc9d17fbb9bc", "commandType": "loadLabware", "params": { "displayName": "Opentrons Flex 96 Tip Rack 1000 µL", @@ -3474,7 +3478,7 @@ } }, { - "key": "d03df580-7915-4bba-9d34-e92039cfe24d", + "key": "880af66e-2905-4102-b655-0351b30252b1", "commandType": "loadLabware", "params": { "displayName": "Opentrons Tough 96 Well Plate 200 µL PCR Full Skirt", @@ -3488,7 +3492,7 @@ } }, { - "key": "d1e4cf27-a1db-48c4-b784-a21014bb234b", + "key": "478e31cc-12f4-4a30-9cd4-03181a538513", "commandType": "loadLabware", "params": { "displayName": "Axygen 1 Well Reservoir 90 mL", @@ -3501,7 +3505,7 @@ }, { "commandType": "loadLiquid", - "key": "64129bfd-92d7-4c70-9380-33785a6041ff", + "key": "56bffeaa-ee2b-4cb8-91dc-a9e21e8f1655", "params": { "liquidId": "1", "labwareId": "54370838-4fca-4a14-b88a-7840e4903649:opentrons/opentrons_96_wellplate_200ul_pcr_full_skirt/2", @@ -3519,7 +3523,7 @@ }, { "commandType": "loadLiquid", - "key": "ac47f11d-0d9c-48d7-b45b-9ecb269a9a50", + "key": "e95ef8f9-fef7-4dfe-b5db-86a5dff7e5b5", "params": { "liquidId": "0", "labwareId": "8bacda22-9e05-45e8-bef4-cc04414a204f:opentrons/axygen_1_reservoir_90ml/1", @@ -3528,14 +3532,14 @@ }, { "commandType": "thermocycler/openLid", - "key": "bfa8af0c-4cb2-49d3-912b-b07e90a1f752", + "key": "63d31323-1217-4a56-9392-c1c28dc703d7", "params": { "moduleId": "fd6da9f1-d63b-414b-929e-c646b64790e9:thermocyclerModuleType" } }, { "commandType": "moveLabware", - "key": "a991e2d5-5be6-43b1-9a71-2f229aea392f", + "key": "716ec050-c597-490d-b261-20ac8e3b4c2f", "params": { "labwareId": "8bacda22-9e05-45e8-bef4-cc04414a204f:opentrons/axygen_1_reservoir_90ml/1", "strategy": "usingGripper", @@ -3544,7 +3548,7 @@ }, { "commandType": "pickUpTip", - "key": "55826f7b-111e-4768-a6d3-d0a4c4a5e20d", + "key": "635b128e-5cdc-4bdc-9975-c04a49fb7670", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "labwareId": "f2d371ea-5146-4c89-8200-9c056a7f321a:opentrons/opentrons_flex_96_tiprack_1000ul/1", @@ -3553,31 +3557,37 @@ }, { "commandType": "aspirate", - "key": "bb5688fe-2909-4755-be74-1850d4d05735", + "key": "1a26a0e0-11c2-4940-b32d-8c747e6969a7", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "volume": 100, "labwareId": "8bacda22-9e05-45e8-bef4-cc04414a204f:opentrons/axygen_1_reservoir_90ml/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 160 } }, { "commandType": "dispense", - "key": "33f7aa0b-80e4-41f0-a841-d8aacb4c7f32", + "key": "17f82c54-3e03-46f4-9c65-666aacc5bab3", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "volume": 100, "labwareId": "54370838-4fca-4a14-b88a-7840e4903649:opentrons/opentrons_96_wellplate_200ul_pcr_full_skirt/2", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 160 } }, { "commandType": "moveToAddressableArea", - "key": "650a3b63-379d-4327-ae55-9752d04497ab", + "key": "d38dc37e-e466-47c9-a7bc-85322487af8c", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "addressableAreaName": "1ChannelWasteChute", @@ -3586,12 +3596,12 @@ }, { "commandType": "dropTipInPlace", - "key": "40c51d0f-5a80-4355-91c1-aaaba7489f37", + "key": "69952335-9a0e-4b69-a903-00454f162e8f", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" } }, { "commandType": "pickUpTip", - "key": "006d7584-e3ad-43a9-8fa1-0688f1d74304", + "key": "2a6d6805-bb22-42c6-9d38-321bdbd9f941", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "labwareId": "f2d371ea-5146-4c89-8200-9c056a7f321a:opentrons/opentrons_flex_96_tiprack_1000ul/1", @@ -3600,31 +3610,37 @@ }, { "commandType": "aspirate", - "key": "562c0ad9-1f97-4e74-af40-107e12019e41", + "key": "087e94b5-a8f7-4637-a830-eb99e2d3a631", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "volume": 100, "labwareId": "8bacda22-9e05-45e8-bef4-cc04414a204f:opentrons/axygen_1_reservoir_90ml/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 160 } }, { "commandType": "dispense", - "key": "cbd55dd4-a746-4bf5-bf43-73afd95ebff2", + "key": "6edf7c6f-858c-4170-9b69-9f230144ba8a", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "volume": 100, "labwareId": "54370838-4fca-4a14-b88a-7840e4903649:opentrons/opentrons_96_wellplate_200ul_pcr_full_skirt/2", "wellName": "B1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 160 } }, { "commandType": "moveToAddressableArea", - "key": "1bac0a50-7a55-4abe-905c-547f006fd62c", + "key": "129a19fb-6a84-4196-a712-7400142cfff2", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "addressableAreaName": "1ChannelWasteChute", @@ -3633,12 +3649,12 @@ }, { "commandType": "dropTipInPlace", - "key": "480d48a6-b825-406a-bc6c-b95b457a1eba", + "key": "46e0edd9-a8eb-4dc4-840d-496ce6ecb732", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" } }, { "commandType": "pickUpTip", - "key": "aed7d916-7957-4608-8678-895cd03f2bb8", + "key": "2c31e97a-5821-4fd9-b171-d29ac18cda36", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "labwareId": "f2d371ea-5146-4c89-8200-9c056a7f321a:opentrons/opentrons_flex_96_tiprack_1000ul/1", @@ -3647,31 +3663,37 @@ }, { "commandType": "aspirate", - "key": "6c2a45d8-449f-4d46-858d-01c349ec7481", + "key": "c5d54202-b261-497f-aa71-3bbdb73f2441", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "volume": 100, "labwareId": "8bacda22-9e05-45e8-bef4-cc04414a204f:opentrons/axygen_1_reservoir_90ml/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 160 } }, { "commandType": "dispense", - "key": "2259e5af-9e35-45bc-b869-105e0d6bda3e", + "key": "df57bdd7-104c-4923-a561-002043500c74", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "volume": 100, "labwareId": "54370838-4fca-4a14-b88a-7840e4903649:opentrons/opentrons_96_wellplate_200ul_pcr_full_skirt/2", "wellName": "C1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 160 } }, { "commandType": "moveToAddressableArea", - "key": "4422ed17-8cf6-47f4-b945-352f17a81fb0", + "key": "eddd8f7b-ccd6-4919-885d-bf20bbbc675f", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "addressableAreaName": "1ChannelWasteChute", @@ -3680,12 +3702,12 @@ }, { "commandType": "dropTipInPlace", - "key": "33bf2ffd-b472-4d01-a063-e6d78cd10f6e", + "key": "2f5e18c4-1436-47f1-9010-975fe41ca901", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" } }, { "commandType": "pickUpTip", - "key": "0b9fe44a-1d94-48ed-9d52-058fb8639425", + "key": "c4508229-340b-42af-850c-f8d4d10caeae", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "labwareId": "f2d371ea-5146-4c89-8200-9c056a7f321a:opentrons/opentrons_flex_96_tiprack_1000ul/1", @@ -3694,31 +3716,37 @@ }, { "commandType": "aspirate", - "key": "d617d4ec-ae3c-4517-acea-7ff57af655ef", + "key": "7b548807-dd81-479e-a00f-b4cd9d2080ff", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "volume": 100, "labwareId": "8bacda22-9e05-45e8-bef4-cc04414a204f:opentrons/axygen_1_reservoir_90ml/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 160 } }, { "commandType": "dispense", - "key": "99bf9993-2553-4adc-9131-be9fe370b9df", + "key": "8d8053f6-f155-416c-986c-1893f87d979f", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "volume": 100, "labwareId": "54370838-4fca-4a14-b88a-7840e4903649:opentrons/opentrons_96_wellplate_200ul_pcr_full_skirt/2", "wellName": "D1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 160 } }, { "commandType": "moveToAddressableArea", - "key": "4f05b8d1-319d-40b5-a006-31a41ad5742f", + "key": "92fa7df4-7cd5-42fd-8405-7baf417b46e3", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "addressableAreaName": "1ChannelWasteChute", @@ -3727,12 +3755,12 @@ }, { "commandType": "dropTipInPlace", - "key": "8faee0ed-2458-45d7-b09f-8021317417cd", + "key": "b2cc5f6e-dc14-4a5e-8f54-1fbcf779e850", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" } }, { "commandType": "pickUpTip", - "key": "bf3176ac-63db-4218-8042-d5683092a66d", + "key": "149f4bc1-ecb0-49c8-bf2a-9e1dc7d241dc", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "labwareId": "f2d371ea-5146-4c89-8200-9c056a7f321a:opentrons/opentrons_flex_96_tiprack_1000ul/1", @@ -3741,31 +3769,37 @@ }, { "commandType": "aspirate", - "key": "fbea3f6f-0421-428a-bf21-6cda35b30407", + "key": "43ee041e-de88-4f88-8d40-700334aaf355", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "volume": 100, "labwareId": "8bacda22-9e05-45e8-bef4-cc04414a204f:opentrons/axygen_1_reservoir_90ml/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 160 } }, { "commandType": "dispense", - "key": "fedd8c6f-777b-4913-afd9-63c919394a5c", + "key": "779c450d-0d43-4b71-aa73-5f29ed51f5dd", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "volume": 100, "labwareId": "54370838-4fca-4a14-b88a-7840e4903649:opentrons/opentrons_96_wellplate_200ul_pcr_full_skirt/2", "wellName": "E1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 160 } }, { "commandType": "moveToAddressableArea", - "key": "a1934186-6d8b-4fdf-b17a-8f9e93f63417", + "key": "b2be4778-5e00-4bc1-8431-cdecb7ad74ad", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "addressableAreaName": "1ChannelWasteChute", @@ -3774,12 +3808,12 @@ }, { "commandType": "dropTipInPlace", - "key": "4fb7ea89-471a-47c4-8af8-0a6bfdae1d74", + "key": "4fa0e93d-1f79-4af5-9bbf-c0e41f131053", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" } }, { "commandType": "pickUpTip", - "key": "f66afc4e-9476-4ca4-9cdc-a66257031413", + "key": "77a07fa4-8e68-49c2-aad8-74f04328a34b", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "labwareId": "f2d371ea-5146-4c89-8200-9c056a7f321a:opentrons/opentrons_flex_96_tiprack_1000ul/1", @@ -3788,31 +3822,37 @@ }, { "commandType": "aspirate", - "key": "a629a9e7-e34f-4693-8479-3cb27d44d0b6", + "key": "06c28a5b-53c6-4aa5-89e0-30b509d2c68f", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "volume": 100, "labwareId": "8bacda22-9e05-45e8-bef4-cc04414a204f:opentrons/axygen_1_reservoir_90ml/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 160 } }, { "commandType": "dispense", - "key": "f831d4dd-c2c2-4429-9314-2fbef18546d6", + "key": "0caa3ced-9327-48aa-b59f-07ea65a81702", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "volume": 100, "labwareId": "54370838-4fca-4a14-b88a-7840e4903649:opentrons/opentrons_96_wellplate_200ul_pcr_full_skirt/2", "wellName": "F1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 160 } }, { "commandType": "moveToAddressableArea", - "key": "e05ddba8-7f1b-45a6-a8d9-9de8b01146bc", + "key": "592051e7-385f-49eb-aeb2-aca173c7e8d4", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "addressableAreaName": "1ChannelWasteChute", @@ -3821,12 +3861,12 @@ }, { "commandType": "dropTipInPlace", - "key": "059f01dc-eb9f-4cfd-92cf-0b67113e4c2d", + "key": "10c97227-329e-453d-bc1c-16b929cc7ad5", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" } }, { "commandType": "pickUpTip", - "key": "2d85f593-c882-45b2-89ec-f3bd9cd7c645", + "key": "a85a3cb6-68e8-43d4-8c87-218bca8fe3ae", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "labwareId": "f2d371ea-5146-4c89-8200-9c056a7f321a:opentrons/opentrons_flex_96_tiprack_1000ul/1", @@ -3835,31 +3875,37 @@ }, { "commandType": "aspirate", - "key": "860d1800-6f8d-46d6-a939-81569e9641fc", + "key": "8804e9b7-b0e6-4814-bf38-48a5b05fb106", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "volume": 100, "labwareId": "8bacda22-9e05-45e8-bef4-cc04414a204f:opentrons/axygen_1_reservoir_90ml/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 160 } }, { "commandType": "dispense", - "key": "1d9ef0b0-926e-446c-b0df-c57dfc97f34e", + "key": "5cf8eaf7-c60d-41e2-bb90-c10b3dcb092f", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "volume": 100, "labwareId": "54370838-4fca-4a14-b88a-7840e4903649:opentrons/opentrons_96_wellplate_200ul_pcr_full_skirt/2", "wellName": "G1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 160 } }, { "commandType": "moveToAddressableArea", - "key": "c2954781-c45e-46ff-a8fa-36faea77630c", + "key": "f3e72ab1-d7ea-4857-aa42-8f25b2ec5d1b", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "addressableAreaName": "1ChannelWasteChute", @@ -3868,12 +3914,12 @@ }, { "commandType": "dropTipInPlace", - "key": "05db8e46-e6c4-4039-84ca-cf7a11042eb9", + "key": "2a0395ec-7363-407b-a391-e8e361d5098b", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" } }, { "commandType": "pickUpTip", - "key": "2cbabc82-4412-4bc5-a7d2-12b74b39b641", + "key": "3246289c-9e03-43d4-8451-e6736a8a709d", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "labwareId": "f2d371ea-5146-4c89-8200-9c056a7f321a:opentrons/opentrons_flex_96_tiprack_1000ul/1", @@ -3882,31 +3928,37 @@ }, { "commandType": "aspirate", - "key": "9f9e94a0-4a33-441c-8864-e64f9a0fda07", + "key": "470b2170-edec-412a-beeb-56de7f85c0ea", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "volume": 100, "labwareId": "8bacda22-9e05-45e8-bef4-cc04414a204f:opentrons/axygen_1_reservoir_90ml/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 160 } }, { "commandType": "dispense", - "key": "d2144ca8-ca39-484a-a8a0-9c70e613be8a", + "key": "dec80858-857c-4ca9-89d1-235affcdfbc8", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "volume": 100, "labwareId": "54370838-4fca-4a14-b88a-7840e4903649:opentrons/opentrons_96_wellplate_200ul_pcr_full_skirt/2", "wellName": "H1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 160 } }, { "commandType": "moveToAddressableArea", - "key": "e8f7d982-7346-4e25-81b1-98e0412553d2", + "key": "998c55f5-86d6-4ba3-ac30-33d818357753", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "addressableAreaName": "1ChannelWasteChute", @@ -3915,19 +3967,19 @@ }, { "commandType": "dropTipInPlace", - "key": "880baa31-8fdb-4e11-9183-d90052fca1e2", + "key": "47eadfc8-8244-4509-9462-2fa624b8488a", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" } }, { "commandType": "thermocycler/closeLid", - "key": "e1c31c80-51e8-47db-be63-29d861843b56", + "key": "15e90989-96e1-4e86-9381-d56db11b7659", "params": { "moduleId": "fd6da9f1-d63b-414b-929e-c646b64790e9:thermocyclerModuleType" } }, { "commandType": "thermocycler/setTargetBlockTemperature", - "key": "ae59fc04-b753-482e-87f0-8680cdccb6c4", + "key": "0dc52334-283f-458d-91a7-3b19c722a8f6", "params": { "moduleId": "fd6da9f1-d63b-414b-929e-c646b64790e9:thermocyclerModuleType", "celsius": 40 @@ -3935,47 +3987,47 @@ }, { "commandType": "thermocycler/waitForBlockTemperature", - "key": "66261c91-97d7-4170-b2f6-462ad85b660e", + "key": "78800364-855d-467f-8f52-8838892375d2", "params": { "moduleId": "fd6da9f1-d63b-414b-929e-c646b64790e9:thermocyclerModuleType" } }, { "commandType": "waitForDuration", - "key": "d09638c2-a49c-4b38-b22f-d581fb68feca", + "key": "264eed35-aa11-454f-83e1-3771ca54b87a", "params": { "seconds": 60, "message": "" } }, { "commandType": "thermocycler/openLid", - "key": "b5017439-7aa1-483a-a475-3b03ce1a4505", + "key": "80009058-c8ad-4da4-80da-9167e79188aa", "params": { "moduleId": "fd6da9f1-d63b-414b-929e-c646b64790e9:thermocyclerModuleType" } }, { "commandType": "thermocycler/deactivateBlock", - "key": "b9e78735-3881-4493-82cb-4bd628bd288d", + "key": "e8109b8f-f380-44b5-965a-40867be7765b", "params": { "moduleId": "fd6da9f1-d63b-414b-929e-c646b64790e9:thermocyclerModuleType" } }, { "commandType": "heaterShaker/deactivateHeater", - "key": "af91feaf-c12a-4059-abbf-91d33820a1c0", + "key": "389a88e8-7267-4cd8-bd5b-22e86d06150d", "params": { "moduleId": "23347241-80bb-4a7e-9c91-5d9727a9e483:heaterShakerModuleType" } }, { "commandType": "heaterShaker/openLabwareLatch", - "key": "7a7352ad-9879-4b2e-bc48-540ac0b2ad3b", + "key": "de12dc4b-89b8-42be-801d-02b70e3b04ff", "params": { "moduleId": "23347241-80bb-4a7e-9c91-5d9727a9e483:heaterShakerModuleType" } }, { "commandType": "moveLabware", - "key": "a2fe52a9-4acf-4599-afc3-5ed26bd579a8", + "key": "8822ab1b-89a9-4b0c-abac-1e3abb792d63", "params": { "labwareId": "54370838-4fca-4a14-b88a-7840e4903649:opentrons/opentrons_96_wellplate_200ul_pcr_full_skirt/2", "strategy": "usingGripper", @@ -3986,21 +4038,21 @@ }, { "commandType": "heaterShaker/closeLabwareLatch", - "key": "b057b4d6-57ae-4443-b798-2e6d9103c2e5", + "key": "91e9ed0e-4d2e-4eb9-b49b-0e30e5b5ea9d", "params": { "moduleId": "23347241-80bb-4a7e-9c91-5d9727a9e483:heaterShakerModuleType" } }, { "commandType": "heaterShaker/deactivateHeater", - "key": "cb6fba71-dfd4-468b-a351-22279bfad1c1", + "key": "1c03bbae-0989-4d1a-87c9-ee73003298ab", "params": { "moduleId": "23347241-80bb-4a7e-9c91-5d9727a9e483:heaterShakerModuleType" } }, { "commandType": "heaterShaker/setAndWaitForShakeSpeed", - "key": "a8c7211e-11f9-41e2-b977-68b513a2db5d", + "key": "af3f5cbc-801c-425f-a4c7-04c5bac0826c", "params": { "moduleId": "23347241-80bb-4a7e-9c91-5d9727a9e483:heaterShakerModuleType", "rpm": 200 @@ -4008,40 +4060,40 @@ }, { "commandType": "waitForDuration", - "key": "91be8718-5404-496e-957d-011a33f9cfe0", + "key": "af1c659a-fcbb-46aa-9c1b-6f233dee281e", "params": { "seconds": 60 } }, { "commandType": "heaterShaker/deactivateShaker", - "key": "11592571-7419-4880-b987-ace8edf90b8a", + "key": "ca120664-8293-4e0f-b8fd-2feb4c75cbf9", "params": { "moduleId": "23347241-80bb-4a7e-9c91-5d9727a9e483:heaterShakerModuleType" } }, { "commandType": "heaterShaker/deactivateHeater", - "key": "3da43478-0315-4c19-aaf2-087b174e1ecf", + "key": "abb2cb21-1848-4b51-a769-0bb74b8b0aa0", "params": { "moduleId": "23347241-80bb-4a7e-9c91-5d9727a9e483:heaterShakerModuleType" } }, { "commandType": "heaterShaker/deactivateHeater", - "key": "7038cc9a-87e9-4554-a69d-3828d1cf9273", + "key": "bd384e07-ddc3-430b-aa2d-04c9b874b130", "params": { "moduleId": "23347241-80bb-4a7e-9c91-5d9727a9e483:heaterShakerModuleType" } }, { "commandType": "heaterShaker/openLabwareLatch", - "key": "4a2c5d7f-31c0-40ff-ab77-b5eb167f4008", + "key": "25b0e4d1-ebd9-419f-ba55-691724c6ab66", "params": { "moduleId": "23347241-80bb-4a7e-9c91-5d9727a9e483:heaterShakerModuleType" } }, { "commandType": "moveLabware", - "key": "b2f3676b-da1a-411e-b106-fc761a5ce11b", + "key": "26c1f526-457b-46c2-9fe6-30fd595feabc", "params": { "labwareId": "54370838-4fca-4a14-b88a-7840e4903649:opentrons/opentrons_96_wellplate_200ul_pcr_full_skirt/2", "strategy": "usingGripper", @@ -4050,7 +4102,7 @@ }, { "commandType": "moveLabware", - "key": "f76dccda-5917-48cf-97eb-efd0ae2138f2", + "key": "b64778b0-86e3-495a-809d-90a4a636c3ff", "params": { "labwareId": "f2d371ea-5146-4c89-8200-9c056a7f321a:opentrons/opentrons_flex_96_tiprack_1000ul/1", "strategy": "usingGripper", diff --git a/protocol-designer/fixtures/protocol/8/example_1_1_0MigratedToV8.json b/protocol-designer/fixtures/protocol/8/example_1_1_0MigratedToV8.json index 531adb047e9..ed550749d7a 100644 --- a/protocol-designer/fixtures/protocol/8/example_1_1_0MigratedToV8.json +++ b/protocol-designer/fixtures/protocol/8/example_1_1_0MigratedToV8.json @@ -6,7 +6,7 @@ "author": "Author name", "description": "Description here", "created": 1560957631666, - "lastModified": 1711650670235, + "lastModified": 1711902162091, "category": null, "subcategory": null, "tags": [] @@ -15,7 +15,7 @@ "name": "opentrons/protocol-designer", "version": "8.1.0", "data": { - "_internalAppBuildDate": "Thu, 28 Mar 2024 18:30:23 GMT", + "_internalAppBuildDate": "Sun, 31 Mar 2024 16:22:18 GMT", "defaultValues": { "aspirate_mmFromBottom": 1, "dispense_mmFromBottom": 0.5, @@ -114,7 +114,7 @@ "disposalVolume_checkbox": true, "disposalVolume_volume": "1", "blowout_checkbox": true, - "blowout_location": "a1a3a3ee-84f5-44f2-b6c5-015be69c0208:trashBin", + "blowout_location": "9b1c0d01-9d4f-4016-afe6-9e08b46acf5e:trashBin", "preWetTip": false, "aspirate_airGap_checkbox": false, "aspirate_airGap_volume": null, @@ -126,8 +126,12 @@ "dispense_delay_checkbox": false, "dispense_delay_seconds": "1", "dispense_delay_mmFromBottom": "0.5", - "dropTip_location": "a1a3a3ee-84f5-44f2-b6c5-015be69c0208:trashBin", + "dropTip_location": "9b1c0d01-9d4f-4016-afe6-9e08b46acf5e:trashBin", "nozzles": null, + "dispense_x_position": 0, + "dispense_y_position": 0, + "aspirate_x_position": 0, + "aspirate_y_position": 0, "id": "e7d36200-92a5-11e9-ac62-1b173f839d9e", "stepType": "moveLiquid", "stepName": "transfer things", @@ -153,9 +157,11 @@ "dispense_delay_seconds": "1", "mix_touchTip_checkbox": true, "mix_touchTip_mmFromBottom": 30.5, - "dropTip_location": "a1a3a3ee-84f5-44f2-b6c5-015be69c0208:trashBin", + "dropTip_location": "9b1c0d01-9d4f-4016-afe6-9e08b46acf5e:trashBin", "nozzles": null, "tipRack": "c6f4ec70-92a5-11e9-ac62-1b173f839d9e:tiprack-10ul", + "mix_x_position": 0, + "mix_y_position": 0, "id": "18113c80-92a6-11e9-ac62-1b173f839d9e", "stepType": "mix", "stepName": "mix", @@ -3336,7 +3342,7 @@ "commandSchemaId": "opentronsCommandSchemaV8", "commands": [ { - "key": "6f4d2d94-4cab-4ead-9827-36a729f06652", + "key": "818878e2-9a2b-498e-be2d-1d317f6f7af8", "commandType": "loadPipette", "params": { "pipetteName": "p10_single", @@ -3345,7 +3351,7 @@ } }, { - "key": "fc0d5cb8-d53b-4629-abf6-b0935b8b4812", + "key": "1ae8e180-58c4-4970-b372-9a8f1869f297", "commandType": "loadPipette", "params": { "pipetteName": "p50_single", @@ -3354,7 +3360,7 @@ } }, { - "key": "a7c0b1ac-b2c6-4e2a-9e4a-b6a7787b48f9", + "key": "ce9f8375-8577-4062-a9ff-12bc33d3bec5", "commandType": "loadLabware", "params": { "displayName": "tiprack 10ul (1)", @@ -3366,7 +3372,7 @@ } }, { - "key": "55d92dea-5339-4f0b-b771-fb1089f281ed", + "key": "8f2f7622-476b-40ff-b692-768a69158aa2", "commandType": "loadLabware", "params": { "displayName": "tiprack 200ul (1)", @@ -3378,7 +3384,7 @@ } }, { - "key": "bfdd6d43-a127-48a5-9bd2-0e2693edf78e", + "key": "6802ec5e-204e-4a63-87a9-c6066788e537", "commandType": "loadLabware", "params": { "displayName": "96 deep well (1)", @@ -3391,7 +3397,7 @@ }, { "commandType": "loadLiquid", - "key": "73a8dbd7-47e8-441a-8c8f-1dbeee57241d", + "key": "c63af547-a330-4e04-96ea-f04ef3c93ca1", "params": { "liquidId": "1", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -3400,7 +3406,7 @@ }, { "commandType": "loadLiquid", - "key": "c1c5b6cf-8bb5-49a0-b887-2a4b0cddfefc", + "key": "d1af9a18-bb2f-4929-b952-7b1e21eadac8", "params": { "liquidId": "0", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -3415,7 +3421,7 @@ }, { "commandType": "pickUpTip", - "key": "ee2227a1-11d2-447c-b97b-9079725370ca", + "key": "24f9ab3b-48fd-42cb-8e0d-2128427459fe", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "c6f4ec70-92a5-11e9-ac62-1b173f839d9e:tiprack-10ul", @@ -3424,91 +3430,112 @@ }, { "commandType": "aspirate", - "key": "7dccc871-ae50-4281-a55d-71628dd2475d", + "key": "426ca672-56a0-430d-bdba-23632ad728b0", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "4b42680f-634b-47b5-95b9-293d73ef6f4a", + "key": "ea2eab58-723d-462e-ae8b-d0daa9462ece", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "c3a5efec-5dfc-41ce-98c2-983f31ca659d", + "key": "fa061fa1-e5d5-42cf-b9dd-d4b9a6b6eabe", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "5ce878dc-f20f-40fc-89d0-8b5551028f5a", + "key": "1e21ebe5-4e6f-4bc5-8dc3-1f1aa9158ff5", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "7ee113bc-9d41-470d-a909-bffb2510d00f", + "key": "7ad7bdad-84eb-42a0-b4ac-48949808a041", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "24cf716a-a105-4817-838c-817755dbb986", + "key": "dd723bb6-9eba-4ab6-bc80-03f6f6db17df", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "dbd9da16-cf9e-44ea-aabe-b6a0bf4f7a60", + "key": "eddfcde7-5497-42e7-bff4-56d2052bc552", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "touchTip", - "key": "7e8803d0-9788-4780-94fa-3a336747cb5a", + "key": "e3e8b3d6-a118-43de-9155-7d1a1da67dbd", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -3518,67 +3545,82 @@ }, { "commandType": "dispense", - "key": "7af6bccd-f70d-40fd-9026-146d24f45606", + "key": "080a9a26-92ba-48ba-84ed-0a10743b7918", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "E8", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "a2b37a9d-35ee-4151-ae60-221144efeaf9", + "key": "ac6f0caf-5fe8-4d45-9659-1265fd022295", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "E8", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "85d9b492-381f-4412-b9de-9343fabd06e2", + "key": "b9e03bec-0741-4dc9-b953-cadd7e7c40b6", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "E8", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "3eab4b4b-ff8f-4e99-a98e-0e8f181aaca1", + "key": "017fd13a-0e3a-4f54-94c3-8d5fc8eb4ba4", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "E8", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "83f8b202-581a-416b-863d-5cbc19d5cfdb", + "key": "7961e88d-1b9b-4615-bcbd-31320a03f81c", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "E8", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "moveToAddressableArea", - "key": "0f0d93dc-f745-46e5-a75d-7d939503d930", + "key": "4d60aa9f-e59b-491a-b494-aef4b877f6fa", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -3587,7 +3629,7 @@ }, { "commandType": "blowOutInPlace", - "key": "d3468c15-f33f-4bb2-aed2-571abb2a0195", + "key": "8bf8312b-7058-430e-8344-84ed35dda280", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "flowRate": 1000 @@ -3595,7 +3637,7 @@ }, { "commandType": "touchTip", - "key": "243f8f03-d546-48f3-8641-439e79bfef83", + "key": "728468cd-08a9-4811-b5a8-ce0649835d29", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -3605,7 +3647,7 @@ }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "631c7562-faa3-4ee2-95d7-6bdbefaec4bb", + "key": "1b5e20e3-85d5-4d87-89f9-7d9568696f6d", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -3615,12 +3657,12 @@ }, { "commandType": "dropTipInPlace", - "key": "fc391196-ed70-44b5-ba11-8abae97462eb", + "key": "3f520a13-6e4f-4ade-bbf8-2fdd35b875c3", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e" } }, { "commandType": "pickUpTip", - "key": "9e8eb31c-3e34-4281-aa18-5de6d0cd195e", + "key": "4b8db7a7-609e-431a-bf9d-7cf858c4b8f7", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "c6f4ec70-92a5-11e9-ac62-1b173f839d9e:tiprack-10ul", @@ -3629,91 +3671,112 @@ }, { "commandType": "aspirate", - "key": "c5e81bdb-1992-40f7-869a-ff0325c199de", + "key": "0355948e-57ca-4572-baa5-7a64b7ef28cc", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "8c0865e8-0d42-4f15-8b70-845e5d9b45fa", + "key": "193a745f-0698-4427-8d0d-d1e4fe24de24", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "97c816e1-3045-4f09-bc33-150e256cde65", + "key": "8d205199-aa0a-4640-9a23-b3adcca61be2", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "06d2d102-35f5-468d-b23c-900bd1df2789", + "key": "fe86a1bb-8c8e-4307-b06e-c92a8e231679", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "7556aad7-86b4-4606-a5cc-5f7f7b56f0d9", + "key": "1976e9d0-ee3f-4ca0-a039-147dd8c21399", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "edbbed85-7ab1-4aad-a603-06654028c9d0", + "key": "b75876f5-cbf6-43ae-8bb5-1b71641ccc6a", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "6ccfd5ad-d683-48e6-a4db-fd911a6803be", + "key": "c6ff48bc-a06c-4e5b-9172-986375d8a934", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "touchTip", - "key": "6b77c1fa-dbb6-4933-b04c-c043b8f183ac", + "key": "7a15666d-4676-41b5-8752-26cc8a07f17e", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -3723,67 +3786,82 @@ }, { "commandType": "dispense", - "key": "1ab3a31c-ee75-4918-a89c-443b6a160d9b", + "key": "ec56b383-c163-402e-9996-d4cc69a1cffd", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "D8", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "d38199b9-9ea1-4994-8124-af29d5bacd69", + "key": "cabfdd05-1309-43e2-bfbd-d04bc7de85c9", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "D8", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "39df2363-edb6-4b3f-9226-3e1e40f49a83", + "key": "05cb631d-9092-46e9-b802-6175fbae1e1f", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "D8", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "05046dbb-2bd5-4d5f-9029-592630619967", + "key": "ea50ada1-23d9-4ecf-af9d-3246930afd26", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "D8", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "04bd6a9a-012a-49cb-ba87-e96e3b42febc", + "key": "2523b9ed-ef76-40c9-8947-18c039e50939", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "D8", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "moveToAddressableArea", - "key": "4381a5c3-9f62-44f0-9030-cecbd7116762", + "key": "58c4751a-5628-4596-a171-1ac260259c28", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -3792,7 +3870,7 @@ }, { "commandType": "blowOutInPlace", - "key": "bca261f2-6071-4457-a47c-2bb76109e746", + "key": "ba5016a9-cd7a-41c8-bf17-aadb64664190", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "flowRate": 1000 @@ -3800,7 +3878,7 @@ }, { "commandType": "touchTip", - "key": "5f923682-3cae-4d33-9dfc-29ac10adb4ae", + "key": "1314e2d9-8d46-4663-9bf3-458a300b0add", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -3810,7 +3888,7 @@ }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "65c6a620-3fbb-41f0-b185-91c6fa6dbda6", + "key": "8527d992-4185-4f20-99a9-864541aaa7b6", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -3820,12 +3898,12 @@ }, { "commandType": "dropTipInPlace", - "key": "d063d2b8-234c-4e38-b66a-85a4011cbf94", + "key": "8c564bbd-34dd-44d2-ace8-995097f571b9", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e" } }, { "commandType": "pickUpTip", - "key": "432b72d6-f0c8-4cea-8bc2-b98fdae69445", + "key": "5377f188-8a31-4ff3-8ed3-ff5b651e467b", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "c6f4ec70-92a5-11e9-ac62-1b173f839d9e:tiprack-10ul", @@ -3834,91 +3912,112 @@ }, { "commandType": "aspirate", - "key": "62c975b5-3adc-4900-9119-a87d8f7098b6", + "key": "70c291fd-f5c9-4216-9446-de8191fff376", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "10139a39-fb4a-4080-88ca-ebe511cb2d56", + "key": "7f1299ec-8930-457d-a2d9-c18876da3769", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "2bc81ba4-9b04-4e2c-88b7-f75f6c3dd3ec", + "key": "d04dee6f-90a4-4b4b-89b8-05f1104431fd", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "bf7df5f4-1c18-46b7-b8f1-cc0853d1244a", + "key": "c983ed9b-783b-411a-8df2-50ef254b4deb", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "b86464a1-ee5d-4fce-b073-f14730bff0aa", + "key": "678dc318-94d9-488b-b2e3-f04ed29a2863", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "9e4fb406-bf5e-4571-b4e5-dfb1ff8f2b98", + "key": "6aee8385-14b4-48fa-bef0-3a642d38c1cd", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "fb8ce4d1-79f9-4ddf-b11e-ebed2414333b", + "key": "c9e9500e-5c89-450c-a56e-7058720a74ce", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "touchTip", - "key": "4be36f15-e8e3-4d6b-84b7-fe64db61ead3", + "key": "eeabdbf7-0dda-4246-859f-de8b643184c0", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -3928,67 +4027,82 @@ }, { "commandType": "dispense", - "key": "e3fdb442-d127-4b6d-8829-b688b55397a6", + "key": "60f965e4-60af-4183-99de-15c77232416d", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "C8", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "55c1e1fa-78a6-4605-b6b5-8953cbbf7010", + "key": "7a40b467-9754-4c02-ae2e-4644cb997555", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "C8", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "61620e17-2c1f-4a35-a64c-ef224b5b2a52", + "key": "a24675b2-41c7-4908-97ce-6bcf04c3d149", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "C8", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "39adc386-6ab8-4664-a0f4-5196f475e19f", + "key": "71a467a6-4c67-46e1-b829-f9a02fb6669e", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "C8", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "e5349e3a-6d1f-481a-8f37-6716b88d93a5", + "key": "b58fb6c6-17f0-44cf-add2-5ad3a99a06fe", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "C8", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "moveToAddressableArea", - "key": "89ae9ed8-0d2c-4b64-af9c-cf0c7bda3fd9", + "key": "b97a7e69-13c0-444b-9405-c84d8ab431bf", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -3997,7 +4111,7 @@ }, { "commandType": "blowOutInPlace", - "key": "6ac0f84b-1da9-41e7-a9e7-e5d7c5823077", + "key": "7e767220-28ab-4b59-ae54-1df3a59ac491", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "flowRate": 1000 @@ -4005,7 +4119,7 @@ }, { "commandType": "touchTip", - "key": "0e94a3f7-0bc1-42ea-bf18-b03b600ec548", + "key": "a4329dfb-0547-498b-a132-5314bdc37453", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -4015,7 +4129,7 @@ }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "4e04ac60-3844-4f1c-afcb-753d8efa8073", + "key": "222528ae-afc3-459f-bd12-291fb6e92977", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -4025,12 +4139,12 @@ }, { "commandType": "dropTipInPlace", - "key": "9c27a051-f55a-4859-9ee0-12cb2e4cc127", + "key": "a2b1c413-6b6d-4db7-b39f-36e801bb67bf", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e" } }, { "commandType": "pickUpTip", - "key": "83b37a71-e721-4454-98ba-a0e4c3311b06", + "key": "ee7cca8e-9d5a-4308-b437-91b3ac59e95c", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "c6f4ec70-92a5-11e9-ac62-1b173f839d9e:tiprack-10ul", @@ -4039,91 +4153,112 @@ }, { "commandType": "aspirate", - "key": "193df488-664a-4f29-8d62-4165930cde80", + "key": "9c65eb65-086b-4535-8dd4-fcdc3b1ce711", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "81ab7f7a-4c7b-4b74-9749-9cd2d146716c", + "key": "de99e84e-c816-42d7-bbaf-c685cf196c84", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "96680762-7d73-4c16-98d4-6ae783afd729", + "key": "2bb3b611-e413-4866-9f88-2093be26c559", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "1be89f92-b2bd-4e14-b230-4e72ebc6fc77", + "key": "51c61ed1-215a-4304-b0bc-f7c0787d9759", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "f693e0d2-1aff-4dc7-b6e3-cdd6ef614c01", + "key": "a5cb7070-9db9-4d93-94a0-baafdb9e1246", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "7918d2ba-e312-438c-8f15-ca28e8724bae", + "key": "b4812aa0-2c04-4f9f-a060-dcddb31655eb", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "8ac0c540-ff45-4cfd-995b-3fb6870ba09f", + "key": "09657153-451a-4ce8-a0aa-d238e97b5d4a", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "touchTip", - "key": "4b930577-57af-4907-859a-f54bc71dc58d", + "key": "1ba61ffa-26f7-4258-806e-459483f8aee2", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -4133,67 +4268,82 @@ }, { "commandType": "dispense", - "key": "cdb0573e-5982-40c7-95c2-4d884d69a313", + "key": "3e54188d-9608-4976-b2a8-0262bc6cd9a8", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "E7", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "6a3055ee-44ca-43fd-b1ea-caac89343321", + "key": "12abbaa6-4354-4635-86c7-53da228b89e9", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "E7", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "d7d8b056-6979-4840-aa44-b527e116aeff", + "key": "75989dac-fb90-46e0-8510-05946f0bb820", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "E7", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "54dcd384-1fad-4071-bd6f-8f09a4eebb3d", + "key": "970cd398-3ad1-46ee-a917-9781c74964c8", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "E7", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "4710bca2-6bb3-4d86-8a27-192c431b525a", + "key": "224042a5-8347-4867-b30c-ea349eee0eb0", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "E7", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "moveToAddressableArea", - "key": "d3d0b4cd-c86a-43b2-99b0-9c9818dca0f3", + "key": "5bca8d87-fae2-4082-92f1-5da5e9b0b01a", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -4202,7 +4352,7 @@ }, { "commandType": "blowOutInPlace", - "key": "621e6320-03b1-4d3a-82f9-000c120042ce", + "key": "6a40c11f-2894-4c0d-ae8c-3069aa7a3ac6", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "flowRate": 1000 @@ -4210,7 +4360,7 @@ }, { "commandType": "touchTip", - "key": "23c285de-7aa1-4a16-a457-015e2fb7abb7", + "key": "9667d8ab-87f8-4af8-a61c-39fa46e15928", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -4220,7 +4370,7 @@ }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "b0268cc2-ae71-4f29-94cb-032b56e36252", + "key": "54efaffe-8b67-45b0-8a1b-34eb9929230b", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -4230,12 +4380,12 @@ }, { "commandType": "dropTipInPlace", - "key": "980de7a4-b9ad-40c5-af04-a989bf3ff807", + "key": "4732e9c8-8b22-447d-9e8a-04360782f50c", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e" } }, { "commandType": "pickUpTip", - "key": "3e68bb44-ba33-484e-88cc-c931435e0c48", + "key": "55fbea4b-e8d2-4cc9-84f1-e531eedc46c8", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "c6f4ec70-92a5-11e9-ac62-1b173f839d9e:tiprack-10ul", @@ -4244,91 +4394,112 @@ }, { "commandType": "aspirate", - "key": "b02f553c-c223-4fb7-8899-7db1a60186d0", + "key": "d735d944-73ff-4713-ac51-c1341e5cc1a9", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "b012ac38-0070-4ff4-acfe-d42b6c5f9674", + "key": "33e8c95b-801c-42c3-9048-fa14b6aa7f29", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "537bf097-77dd-43bd-a67b-77a146f5349e", + "key": "b25b278a-8b01-4bc2-a1f8-456c7bf8c526", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "2931c986-44ae-4ba2-bb36-bd705feb875c", + "key": "23d673c8-d769-480b-858b-43ac62636220", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "bc5af19c-0dd5-4791-a5b1-34002997cb3d", + "key": "3452e515-d862-40d0-99e1-34dd0404337f", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "dc5d2b3e-8efd-41a9-b84e-d7debee06ac9", + "key": "36c73f15-d9cd-410c-8699-f19396584618", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "0de5e018-4a9f-4cfe-9f58-f50901663c3c", + "key": "7b78234d-4513-49cc-83e7-10b662ff8675", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "touchTip", - "key": "af42fb71-74da-41b8-9b50-41048e949434", + "key": "b1b3ee6f-a9be-4220-8004-7296970de788", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -4338,67 +4509,82 @@ }, { "commandType": "dispense", - "key": "72efd216-e92f-4103-a71e-85be208865ec", + "key": "f7c5a31f-1a71-478f-a145-eb5c5c567c6d", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "D7", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "0387ffa2-40c4-4280-86d1-8c1fd39b6356", + "key": "5e4a8c3c-5a80-488b-898d-d1074f2c426c", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "D7", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "c09fbe67-3e6c-4f82-8bc5-25db6a3d5a50", + "key": "da0e8d29-8619-47e1-b8da-98ccaf2c56fc", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "D7", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "1939bb32-88c2-4d55-bb3f-7d31535a3403", + "key": "3fd622c1-93bc-4e5d-92cb-3dc40f38d92d", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "D7", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "b3bdd7bd-5cc2-42fa-b938-24fbc32931d6", + "key": "7fe8ecbf-6872-4c45-9f41-b3f5e31b8c42", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "D7", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "moveToAddressableArea", - "key": "ff827e3d-8136-44c1-a29a-33e0a0abf081", + "key": "7e7f40a5-1b19-414d-b1ec-b0f632ee81eb", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -4407,7 +4593,7 @@ }, { "commandType": "blowOutInPlace", - "key": "ee65b14e-529d-4116-81a7-ff50f28bc1a4", + "key": "e7d928ca-d918-43a1-973a-e56361029dcd", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "flowRate": 1000 @@ -4415,7 +4601,7 @@ }, { "commandType": "touchTip", - "key": "e31dd584-a774-41c7-9176-62749596b7e6", + "key": "1665f0f5-1778-49ed-a765-bcdcc3a9c13a", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -4425,7 +4611,7 @@ }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "cd354a43-9b7d-48d5-8e2a-f6c369ac10f4", + "key": "5f71c216-2dd4-4b3f-9958-feac1e0ba419", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -4435,12 +4621,12 @@ }, { "commandType": "dropTipInPlace", - "key": "a435f546-520d-4e38-bc22-f5f084f95d5d", + "key": "0d98fee0-4ada-4ddd-98cc-ee4f51763615", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e" } }, { "commandType": "pickUpTip", - "key": "ba2e7ee3-715f-4588-93e8-05d4b1eed1cc", + "key": "1eca1b12-6dda-4a57-84cc-48ed09a5dcc7", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "c6f4ec70-92a5-11e9-ac62-1b173f839d9e:tiprack-10ul", @@ -4449,91 +4635,112 @@ }, { "commandType": "aspirate", - "key": "1c2d5f90-6dbd-4b61-b97e-a4bf38f056d9", + "key": "6468842b-d755-431a-8f39-63390afc45aa", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "2fbc684b-57c7-4e89-8d53-85c7f6f806de", + "key": "3f19926d-5262-4869-8830-7eb13951f4fe", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "ef0f4077-3692-41b0-ad2d-0bcf94a1a075", + "key": "0816f07a-7ddf-41da-91a8-6c55bcf902ff", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "a386c011-855b-4f41-be57-623647498c1a", + "key": "6ac9d9b6-b45e-4b0a-90c5-835a680ab914", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "9dd98cc7-2557-48bd-baf9-2e54ab47883c", + "key": "2c0b977d-cc77-44bb-b0a3-62339279f8d4", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "3d57ac48-bf99-498b-b523-1be901efcc1e", + "key": "b15ab048-c8ae-491b-ba0a-ddb84af43b8a", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "e64a4b94-cd07-4eb2-9edc-ab83093fc4bc", + "key": "ebb52c59-bc4d-4f3a-b1b4-10ceea23ecd4", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "touchTip", - "key": "99567709-ebe8-4244-8252-dedb5aeb666c", + "key": "1c48b0b0-c786-4278-a95b-180d8bc8d7fb", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -4543,67 +4750,82 @@ }, { "commandType": "dispense", - "key": "94901710-b6db-4d27-b893-71108cc6186c", + "key": "6db1da99-4bfc-4723-a37b-db57a913a5a0", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "C7", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "0467798b-8ec8-4d1e-afee-2a73a8422bcd", + "key": "b040900a-f61c-462e-9238-87746a45c0b8", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "C7", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "fc858419-1723-4c54-85d1-2d2ef53637ee", + "key": "8e2de19c-a6b1-4af7-a614-8f692815d667", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "C7", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "b30572d5-f396-41f3-8662-a4285508710d", + "key": "a72f4e61-2874-4af0-a471-d97434970e2b", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "C7", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "5da67cc2-d056-4d4c-abc5-3a70269c38bc", + "key": "ff833f33-6c7e-417a-8293-f9a2c2eead8c", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "C7", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "moveToAddressableArea", - "key": "c847fdfb-bb85-4335-9821-8fded3c15f0e", + "key": "40d74de4-9953-43ae-b4bc-518d39005303", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -4612,7 +4834,7 @@ }, { "commandType": "blowOutInPlace", - "key": "d269fea9-b30e-488f-a2b2-37ae88547251", + "key": "7570e6a2-b2a3-4836-aaa0-13c90ceb08f4", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "flowRate": 1000 @@ -4620,7 +4842,7 @@ }, { "commandType": "touchTip", - "key": "da6ec212-7d12-411a-9f2b-2beff5ed197d", + "key": "5de67294-430d-4856-aa25-0177b32ef514", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -4630,7 +4852,7 @@ }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "d8bef8c0-954a-4293-a75f-2589a37fc982", + "key": "b25ac8f3-fe61-4f87-b5f2-40936132a6dd", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -4640,12 +4862,12 @@ }, { "commandType": "dropTipInPlace", - "key": "784c0470-5513-4f60-bb7e-f039db7b170f", + "key": "aa3d17b8-8d52-462f-9e39-b0d2d83e5407", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e" } }, { "commandType": "pickUpTip", - "key": "5729228a-64f0-443e-91fe-31179efbdd1a", + "key": "188da1f2-486b-4dfd-b2c8-e0903544fa8d", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "c6f4ec70-92a5-11e9-ac62-1b173f839d9e:tiprack-10ul", @@ -4654,91 +4876,112 @@ }, { "commandType": "aspirate", - "key": "288895f0-14c7-4909-a300-178801bd08b4", + "key": "df11a136-0f66-4502-ad52-443adc71ca2b", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "2f718ed4-0d72-45c4-bb4d-cc8265cbbf9a", + "key": "00502ab3-b649-4532-ba39-184ff41b00cb", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "7fde7d76-b68e-43d2-a00f-3203fdcfd95e", + "key": "cdc0749e-e66b-480e-afe0-3ad6c5e739e4", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "ca2845d1-33bf-49cf-8bfe-48bbe544419e", + "key": "65529980-e475-4f51-a8dc-cd1f7e5a5020", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "b5b15b72-dce1-430e-8050-e5e4b6fd9d54", + "key": "d9e94497-0439-4675-bb57-cc2e62ea7a84", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "c3bc77de-b5d0-43fa-a7a5-bc9e6b6fd765", + "key": "27bd35c9-4ef4-471f-954b-289db56992ad", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "c08f49f2-c0c8-488b-beab-160ad57f46c5", + "key": "9241c560-e1d0-4468-ac78-10c9511d0113", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "touchTip", - "key": "1d53f469-6c0b-4264-bb92-abb8299f650d", + "key": "67e511d9-8198-4c0d-808e-c9600f2aff6b", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -4748,67 +4991,82 @@ }, { "commandType": "dispense", - "key": "122d4fc9-e63d-430e-8ea0-6c1b17c3f1a7", + "key": "ea876b75-dbb7-445e-afb4-efa1fd12eda8", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "E6", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "7d9df411-c0a1-4e91-8716-c80643cbd868", + "key": "7551fb8d-3899-42f4-ba52-9e03c2410ae5", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "E6", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "73a6bb03-d083-475d-99de-452fb093e44b", + "key": "dae940af-8337-439f-83c5-39745994b216", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "E6", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "818098d4-ddd1-4853-875f-eeaf28898e12", + "key": "d9c4b87f-8e3f-415b-9c61-b14cff73fa6e", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "E6", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "4b43d7c0-d2cc-4721-8675-98c0357889fd", + "key": "6e1ae4be-0622-490d-811a-1442a54f38c6", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "E6", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "moveToAddressableArea", - "key": "4461238e-6823-489c-9b95-59529d34c5e6", + "key": "2172c551-8f66-49ec-b092-3cecb3ecd1e6", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -4817,7 +5075,7 @@ }, { "commandType": "blowOutInPlace", - "key": "abcefb59-b32e-4b9e-8ac3-fb8589565405", + "key": "70f94de0-45c2-4082-85c7-000a3c7d4e05", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "flowRate": 1000 @@ -4825,7 +5083,7 @@ }, { "commandType": "touchTip", - "key": "6e1c8052-ebab-401a-a3de-1a20d61a1b40", + "key": "7a8c6027-3547-4415-97e2-e4a8839cefcb", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -4835,7 +5093,7 @@ }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "72caf8d6-745c-4bb8-997b-c6b2685935b6", + "key": "9e76549d-de35-4be7-b42f-83e81eb148e5", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -4845,12 +5103,12 @@ }, { "commandType": "dropTipInPlace", - "key": "e4f6c6e4-58b0-466c-972a-56ee8b56735c", + "key": "edb7a124-0334-41a3-b82f-237bf2a63e37", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e" } }, { "commandType": "pickUpTip", - "key": "a5de52b2-a015-4377-9adc-2e784a8a3514", + "key": "f040345b-250f-4fa6-abc0-62e27fe59938", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "c6f4ec70-92a5-11e9-ac62-1b173f839d9e:tiprack-10ul", @@ -4859,91 +5117,112 @@ }, { "commandType": "aspirate", - "key": "f46ecf37-8a53-4f96-87b4-45b58807c754", + "key": "cd942842-7300-40c1-87a6-28f073ea3dc5", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "cbedd7bd-637c-4767-a6a6-694b76138850", + "key": "f6a45b15-269b-482d-983b-d3bc5db57d26", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "a200a845-574f-4f0b-9ad7-39f095b6d732", + "key": "7d61c0b4-4555-435c-b837-b559b360a82e", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "96a8c1d7-bd45-44a7-ba7c-44b4d1067f4e", + "key": "9f9dfc52-5ca3-42e2-b9d5-3bfa8521de49", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "b1aae64c-98fe-402a-8a6e-38046dc2d375", + "key": "11346b4b-af47-46f0-9461-52664eec0d39", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "9ec5ab4b-5da0-4859-b713-e849f806a4c7", + "key": "23982cac-52ae-484f-b3e7-c52c029b1e9a", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "f6849f46-5724-4643-92bf-b526f5e263fa", + "key": "148dd2de-1425-482f-8fec-32731007bbff", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "touchTip", - "key": "9dc1c842-947a-4e0f-8601-bae5edf58bd0", + "key": "41e664b1-6199-4a33-9857-76df944f516d", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -4953,67 +5232,82 @@ }, { "commandType": "dispense", - "key": "f9c66ebe-764a-4d16-975a-b9d275f7e6e3", + "key": "152340ce-cde0-469e-9882-a8ef3d4a1cde", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "D6", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "ccf1ab0e-c50f-4c41-9eb9-5f84ec9c8d8c", + "key": "e4e8529f-89fc-4a94-a49d-410b799aa539", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "D6", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "0f734cf9-c9d8-40ee-82f7-a34d97e43ed9", + "key": "01461514-1395-4f09-95db-29dea71c1f5b", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "D6", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "0139e4ec-529e-4080-8926-37c140621866", + "key": "ff195ab9-cb65-45d1-93a8-a071d0bbed98", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "D6", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "e3a5a1bf-0a24-4787-b3fe-2f60075de339", + "key": "8ba714b7-bcc2-48c3-8c57-0d0ac933b976", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "D6", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "moveToAddressableArea", - "key": "7b9216cc-c1d4-469e-a5d8-7683a943bb0c", + "key": "8c2017b4-9145-46bc-a91f-83f27cc0a828", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -5022,7 +5316,7 @@ }, { "commandType": "blowOutInPlace", - "key": "cdeb2bad-74b0-4160-9984-ebb55bb04bc3", + "key": "6dba0671-c83f-4fc2-8d9c-3e309448d0e9", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "flowRate": 1000 @@ -5030,7 +5324,7 @@ }, { "commandType": "touchTip", - "key": "8d938cfa-0484-4692-bac6-143f3f52e75b", + "key": "15c49bf0-ce06-4687-aeb5-a5dd0736f2f5", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -5040,7 +5334,7 @@ }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "ec4eb309-173d-452e-a601-6ea966a7254e", + "key": "5e494f88-ee95-42f1-bbd4-23b449649b93", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -5050,12 +5344,12 @@ }, { "commandType": "dropTipInPlace", - "key": "f92c4c88-0208-44f5-81b9-056546a45e49", + "key": "e1f4d20a-b36c-4da1-9b1f-529aef638f1f", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e" } }, { "commandType": "pickUpTip", - "key": "1ede6001-67c7-4d54-b866-4eb2d9b1d82b", + "key": "c3d944d3-abe8-4f4c-8e4d-70792c3303f2", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "c6f4ec70-92a5-11e9-ac62-1b173f839d9e:tiprack-10ul", @@ -5064,91 +5358,112 @@ }, { "commandType": "aspirate", - "key": "ccf06eed-b517-4b14-b31d-736dcdc8c3b4", + "key": "4432786d-94e4-4958-ae49-8d0679c97fc0", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "49f837ff-5dd9-4f54-b4a0-ffd492c4c969", + "key": "3efc13e5-aac5-4f23-b060-52003c8c827f", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "530ffdc6-b112-4f88-b25e-745bb9c86516", + "key": "5ec72861-9ac4-4a9b-91e2-907932819e58", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "515f7c58-c506-4bad-95c4-4adfcdadea5d", + "key": "994b0746-ea15-4cfb-afa7-d00ff124e0f1", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "fc5e49e8-cadb-4a8a-addb-4525a0640254", + "key": "2acee0bb-366c-4f1d-b165-f69a1c03b05f", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "26838934-eaf7-4a76-bb3b-070e3aab3bcb", + "key": "a44857c1-e5d2-4ce7-a428-41a68e426f3c", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "eef5c160-b9b0-43cf-8e8e-9b836431a606", + "key": "09f55bdd-61ff-4667-878f-c79e0a21b9c5", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "touchTip", - "key": "4e84dbb8-53b6-400f-8530-eb2ee326dc13", + "key": "4daa0f4c-e10e-488e-9d19-3a8602a548f4", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -5158,67 +5473,82 @@ }, { "commandType": "dispense", - "key": "19c3e661-d308-4544-babf-fd4cefd23331", + "key": "5f54be1c-fff2-41ae-b512-01a9bb28cc4a", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "C6", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "2bfda325-1526-4178-8fa5-338c9dc9d92b", + "key": "6e42ea13-01ed-461b-8dfa-9bd360982ddf", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "C6", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "adabc3a8-3e76-423d-949e-8d5146862421", + "key": "63d6f42e-0caa-47c4-9341-e3a950f85128", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "C6", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "957dda98-4628-4029-90bd-d1a2e0c280c3", + "key": "c8791232-20bd-4068-a778-4630548b49ae", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "C6", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "538985a7-8e67-4abd-94cf-68387fd80e7d", + "key": "98e4d5e2-4b75-435f-8809-099806e98694", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "C6", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "moveToAddressableArea", - "key": "71eaf4c8-c8a5-400b-b094-46bdcaa60daf", + "key": "921371a0-2df9-4f3e-b28f-0282399e98a3", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -5227,7 +5557,7 @@ }, { "commandType": "blowOutInPlace", - "key": "f935fe77-d02e-4bb8-95e2-5f25e8312dad", + "key": "f9c7ae2a-b401-4c92-8e6a-4366ffb93643", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "flowRate": 1000 @@ -5235,7 +5565,7 @@ }, { "commandType": "touchTip", - "key": "6cbaaafd-f358-4779-9cb2-3622e3285ae1", + "key": "70fbf7e3-cae6-49e7-bfd3-65a5376b5e3e", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -5245,7 +5575,7 @@ }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "756c761d-66fe-4fc7-8e53-cf258c4b95c4", + "key": "74d53fee-f9c6-4a27-a54b-80a79e906b6c", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -5255,12 +5585,12 @@ }, { "commandType": "dropTipInPlace", - "key": "bfb11f03-bef0-4d98-a569-b21249c1f447", + "key": "28dc2329-937d-4d2c-8fc3-eecf3f321041", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e" } }, { "commandType": "pickUpTip", - "key": "7dec52bf-9c68-42ca-838b-1ebb9c4f325f", + "key": "5ad18635-8559-4904-8db4-4e2b19546238", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "c6f4ec70-92a5-11e9-ac62-1b173f839d9e:tiprack-10ul", @@ -5269,79 +5599,97 @@ }, { "commandType": "aspirate", - "key": "83e518c4-7a06-439f-b7f8-175feb33b528", + "key": "1227b40e-adda-4545-9724-5509ff790adf", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 5.5, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "F1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 8 } }, { "commandType": "dispense", - "key": "a79b3bc6-6e2c-4800-adf2-72f5b221e2d4", + "key": "b9c1000c-c52f-4b04-9790-9a2dec7dadd3", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 5.5, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "F1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 7 } }, { "commandType": "aspirate", - "key": "3fd83532-cf51-43d6-bd74-ca3fcd09f175", + "key": "0b5da711-8961-40d0-a294-b4d9eed6c77a", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 5.5, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "F1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 8 } }, { "commandType": "dispense", - "key": "48d023af-e120-4d61-8eb0-76a9433258a4", + "key": "12b3c883-f2b2-4651-816e-e38bb8cb5c85", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 5.5, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "F1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 7 } }, { "commandType": "aspirate", - "key": "54f4aba0-c8f0-463d-8bff-8e3311db6765", + "key": "b30463df-33e7-4038-97d6-298f7e9cef8e", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 5.5, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "F1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 8 } }, { "commandType": "dispense", - "key": "1589b195-68ec-47a4-baee-f27de214ef10", + "key": "b2c2c14c-6874-406a-b9d1-33bc02b7a74f", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 5.5, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "F1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 7 } }, { "commandType": "blowout", - "key": "0a4211db-4a8c-496a-9098-0a8547f4e39f", + "key": "98f8d095-46f4-4349-8c93-21eebfcf05d3", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -5352,7 +5700,7 @@ }, { "commandType": "touchTip", - "key": "0575a144-4887-4ccd-b64a-a1a18094a2f5", + "key": "d6985dc6-551c-4ceb-bcc9-c833301b1eac", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -5362,7 +5710,7 @@ }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "2551d68a-3a19-4283-84d9-fd285ee0f745", + "key": "cdf5e0f0-0598-4e4d-98e8-70a57ff83a4a", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -5372,12 +5720,12 @@ }, { "commandType": "dropTipInPlace", - "key": "981b6c74-860e-4c14-bb74-25c66d110508", + "key": "1c0dee1c-97fa-4f33-bb36-9b3b7a2ef73e", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e" } }, { "commandType": "waitForDuration", - "key": "a45e4cd0-d4b1-4042-9295-396f0e6b92df", + "key": "d306df0a-3ad2-48ac-9ac2-1151895982e0", "params": { "seconds": 3723, "message": "Delay plz" } } ], diff --git a/protocol-designer/fixtures/protocol/8/mix_8_0_0.json b/protocol-designer/fixtures/protocol/8/mix_8_0_0.json index 0cf5bc6679f..efa4b0ac6d6 100644 --- a/protocol-designer/fixtures/protocol/8/mix_8_0_0.json +++ b/protocol-designer/fixtures/protocol/8/mix_8_0_0.json @@ -6,7 +6,7 @@ "author": "", "description": "A test for 5.0.0 -> 5.1.0 migration", "created": 1600714068238, - "lastModified": 1709303322125, + "lastModified": 1711742569351, "category": null, "subcategory": null, "tags": [] @@ -15,7 +15,7 @@ "name": "opentrons/protocol-designer", "version": "8.1.0", "data": { - "_internalAppBuildDate": "Fri, 01 Mar 2024 14:22:27 GMT", + "_internalAppBuildDate": "Fri, 29 Mar 2024 20:00:04 GMT", "defaultValues": { "aspirate_mmFromBottom": 1, "dispense_mmFromBottom": 0.5, @@ -75,6 +75,8 @@ "dropTip_location": "5ba7047d-d3e2-4845-9eaa-1974af796ead:trashBin", "nozzles": null, "tipRack": "f1c677c0-fc3a-11ea-8809-e959e7d61d96:opentrons/opentrons_96_tiprack_10ul/1", + "mix_x_position": 0, + "mix_y_position": 0, "id": "fc4dc7c0-fc3a-11ea-8809-e959e7d61d96", "stepType": "mix", "stepName": "mix", @@ -2125,7 +2127,7 @@ "commandSchemaId": "opentronsCommandSchemaV8", "commands": [ { - "key": "87303141-a159-4390-ab9e-c737b5e29d2a", + "key": "3004b46c-2b41-4453-8ddc-1629ec3b5249", "commandType": "loadPipette", "params": { "pipetteName": "p20_single_gen2", @@ -2134,7 +2136,7 @@ } }, { - "key": "1dbb2e54-da06-4512-b02c-b3a4c2fc539f", + "key": "c318feee-5ec6-40a0-9ecc-554e67b30ce1", "commandType": "loadLabware", "params": { "displayName": "Opentrons OT-2 96 Tip Rack 10 µL", @@ -2146,7 +2148,7 @@ } }, { - "key": "7c5e3453-255c-4216-a5c3-7787fa4ef106", + "key": "3350dee6-aa60-4569-a801-0dfeb5baf8ed", "commandType": "loadLabware", "params": { "displayName": "Bio-Rad 96 Well Plate 200 µL PCR", @@ -2159,7 +2161,7 @@ }, { "commandType": "waitForDuration", - "key": "929f2a92-418b-411d-aa33-27db0788e1ff", + "key": "797e70f3-5310-48c2-ba06-12adb92a7b4e", "params": { "seconds": 3723, "message": "" } } ], diff --git a/protocol-designer/fixtures/protocol/8/ninetySixChannelFullAndColumn.json b/protocol-designer/fixtures/protocol/8/ninetySixChannelFullAndColumn.json index abc2d223176..07384926f57 100644 --- a/protocol-designer/fixtures/protocol/8/ninetySixChannelFullAndColumn.json +++ b/protocol-designer/fixtures/protocol/8/ninetySixChannelFullAndColumn.json @@ -6,7 +6,7 @@ "author": "", "description": "", "created": 1701805621086, - "lastModified": 1709303384383, + "lastModified": 1711742604736, "category": null, "subcategory": null, "tags": [] @@ -15,7 +15,7 @@ "name": "opentrons/protocol-designer", "version": "8.1.0", "data": { - "_internalAppBuildDate": "Fri, 01 Mar 2024 14:22:27 GMT", + "_internalAppBuildDate": "Fri, 29 Mar 2024 20:00:04 GMT", "defaultValues": { "aspirate_mmFromBottom": 1, "dispense_mmFromBottom": 0.5, @@ -93,6 +93,10 @@ "dispense_delay_mmFromBottom": null, "dropTip_location": "1e553651-9e4d-44b1-a31b-92459642bfd7:trashBin", "nozzles": "ALL", + "dispense_x_position": 0, + "dispense_y_position": 0, + "aspirate_x_position": 0, + "aspirate_y_position": 0, "id": "83a095fa-b649-4105-99d4-177f1a3f363a", "stepType": "moveLiquid", "stepName": "transfer", @@ -144,6 +148,10 @@ "dispense_delay_mmFromBottom": null, "dropTip_location": "1e553651-9e4d-44b1-a31b-92459642bfd7:trashBin", "nozzles": "COLUMN", + "dispense_x_position": 0, + "dispense_y_position": 0, + "aspirate_x_position": 0, + "aspirate_y_position": 0, "id": "f5ea3139-1585-4848-9d5f-832eb88c99ca", "stepType": "moveLiquid", "stepName": "transfer", @@ -2233,7 +2241,7 @@ "commandSchemaId": "opentronsCommandSchemaV8", "commands": [ { - "key": "e09dc6e2-c0e6-4b28-9460-865c48a3b03f", + "key": "7224d1a7-a7b3-4bb3-bc5c-65aa98565616", "commandType": "loadPipette", "params": { "pipetteName": "p1000_96", @@ -2242,7 +2250,7 @@ } }, { - "key": "3dc22b4a-9fa8-4c61-843d-b45a4054490e", + "key": "dcddeb3c-66d9-4868-9f9f-fbd47d754fc4", "commandType": "loadLabware", "params": { "displayName": "Opentrons Flex 96 Tip Rack Adapter", @@ -2254,7 +2262,7 @@ } }, { - "key": "0f3b11ad-a015-4ece-9267-0ca57c832bfd", + "key": "c206434e-aa1e-44ee-8667-29accd89941a", "commandType": "loadLabware", "params": { "displayName": "Opentrons Flex 96 Tip Rack 50 µL", @@ -2268,7 +2276,7 @@ } }, { - "key": "0194f4bc-e114-4048-af3f-e053db83a79e", + "key": "3cdba839-f0fa-4e50-8399-94338cced032", "commandType": "loadLabware", "params": { "displayName": "Bio-Rad 96 Well Plate 200 µL PCR", @@ -2280,7 +2288,7 @@ } }, { - "key": "c807c9aa-7300-40be-817f-6d2018cd9d95", + "key": "7f75bf03-3036-4847-afbf-4bbefdf6cee8", "commandType": "loadLabware", "params": { "displayName": "Opentrons Flex 96 Tip Rack 50 µL", @@ -2293,7 +2301,7 @@ }, { "commandType": "configureNozzleLayout", - "key": "131fd37b-29cb-41f8-8792-b3c210e2db36", + "key": "2326c781-0416-4319-b954-16929077b5e3", "params": { "pipetteId": "de7da440-95ec-43e8-8723-851321fbd6f9", "configurationParams": { "style": "ALL" } @@ -2301,7 +2309,7 @@ }, { "commandType": "pickUpTip", - "key": "d08a4b16-f17e-4146-adff-68d3235f3174", + "key": "86f7ac25-739d-4a38-8bf4-4730a8e6cce7", "params": { "pipetteId": "de7da440-95ec-43e8-8723-851321fbd6f9", "labwareId": "75aa666f-98d8-4af9-908e-963ced428580:opentrons/opentrons_flex_96_tiprack_50ul/1", @@ -2310,19 +2318,22 @@ }, { "commandType": "aspirate", - "key": "79c1655a-54de-4c5d-8b74-3d866244b229", + "key": "0113e27d-0949-4305-8f0b-5467753dfac3", "params": { "pipetteId": "de7da440-95ec-43e8-8723-851321fbd6f9", "volume": 10, "labwareId": "fe1942b1-1b75-4d3a-9c12-d23004958a12:opentrons/biorad_96_wellplate_200ul_pcr/2", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "moveToAddressableArea", - "key": "e95fefc8-1738-4e24-89ab-e8b27fbde04b", + "key": "79c134c0-5042-4243-8a81-95ad54594ab3", "params": { "pipetteId": "de7da440-95ec-43e8-8723-851321fbd6f9", "addressableAreaName": "movableTrashA3", @@ -2331,7 +2342,7 @@ }, { "commandType": "dispenseInPlace", - "key": "432061e5-a407-43cc-b703-25882875ae58", + "key": "2ce5b534-62b3-4415-bdd6-747fb57545be", "params": { "pipetteId": "de7da440-95ec-43e8-8723-851321fbd6f9", "volume": 10, @@ -2340,7 +2351,7 @@ }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "8e2ba800-c7af-451a-b730-0ef9115b970f", + "key": "7212407e-0bd1-4ef5-a8c7-4c6f95cee357", "params": { "pipetteId": "de7da440-95ec-43e8-8723-851321fbd6f9", "addressableAreaName": "movableTrashA3", @@ -2350,12 +2361,12 @@ }, { "commandType": "dropTipInPlace", - "key": "0cced503-95fa-49fb-8540-2d528819f20d", + "key": "55286f40-e2c1-44f6-a3f3-032bfbf89f3d", "params": { "pipetteId": "de7da440-95ec-43e8-8723-851321fbd6f9" } }, { "commandType": "configureNozzleLayout", - "key": "48a2d952-d9ad-4ed7-9021-31c97c43b175", + "key": "47ab8f5c-a2dc-40e0-a6db-3c2ff6c48778", "params": { "pipetteId": "de7da440-95ec-43e8-8723-851321fbd6f9", "configurationParams": { "primaryNozzle": "A12", "style": "COLUMN" } @@ -2363,7 +2374,7 @@ }, { "commandType": "pickUpTip", - "key": "474ddf94-384e-4c01-acbd-50e43c005c7c", + "key": "c6f563fd-4f3f-4bd8-833e-3519c4fb0026", "params": { "pipetteId": "de7da440-95ec-43e8-8723-851321fbd6f9", "labwareId": "9bd16b50-4ae9-4cfd-8583-3378087e6a6c:opentrons/opentrons_flex_96_tiprack_50ul/1", @@ -2372,19 +2383,22 @@ }, { "commandType": "aspirate", - "key": "1e082d08-89b8-4e5f-b80f-e9190280fad7", + "key": "ee919504-5c21-40c5-9205-00e8aee06718", "params": { "pipetteId": "de7da440-95ec-43e8-8723-851321fbd6f9", "volume": 10, "labwareId": "fe1942b1-1b75-4d3a-9c12-d23004958a12:opentrons/biorad_96_wellplate_200ul_pcr/2", "wellName": "A7", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "moveToAddressableArea", - "key": "42daf0a1-9c17-4c9a-b8e6-90e68e166d1a", + "key": "6c1dbdec-0d3a-4693-810b-b28984382fce", "params": { "pipetteId": "de7da440-95ec-43e8-8723-851321fbd6f9", "addressableAreaName": "movableTrashA3", @@ -2393,7 +2407,7 @@ }, { "commandType": "dispenseInPlace", - "key": "6e36d0e4-e975-4cf6-8dd4-24d74f9d60f7", + "key": "d7ad2bf5-3033-4168-adf4-082306dc5467", "params": { "pipetteId": "de7da440-95ec-43e8-8723-851321fbd6f9", "volume": 10, @@ -2402,7 +2416,7 @@ }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "918fec4b-1947-49c5-8fe1-af24fef2bf3f", + "key": "9ca4968e-0995-4354-95a1-37964599784f", "params": { "pipetteId": "de7da440-95ec-43e8-8723-851321fbd6f9", "addressableAreaName": "movableTrashA3", @@ -2412,7 +2426,7 @@ }, { "commandType": "dropTipInPlace", - "key": "7b5a5ab4-5dbd-4338-890f-38551bd58c4a", + "key": "548bbf90-da13-4487-a878-dd363b17d906", "params": { "pipetteId": "de7da440-95ec-43e8-8723-851321fbd6f9" } } ], diff --git a/protocol-designer/src/components/BatchEditForm/BatchEditMix.tsx b/protocol-designer/src/components/BatchEditForm/BatchEditMix.tsx index 062052ea9d6..76074bc8e3b 100644 --- a/protocol-designer/src/components/BatchEditForm/BatchEditMix.tsx +++ b/protocol-designer/src/components/BatchEditForm/BatchEditMix.tsx @@ -88,7 +88,10 @@ export const BatchEditMix = (props: BatchEditMixProps): JSX.Element => { tiprack={propsForFields.tipRack.value} /> { className={styles.small_field} > { /> {tipPositionFieldName && ( )} diff --git a/protocol-designer/src/components/StepEditForm/fields/TipPositionField/TipPositionAllViz.tsx b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/TipPositionAllViz.tsx new file mode 100644 index 00000000000..d1b219b04d8 --- /dev/null +++ b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/TipPositionAllViz.tsx @@ -0,0 +1,52 @@ +import * as React from 'react' +import round from 'lodash/round' + +import PIPETTE_TIP_IMAGE from '../../../../images/pipette_tip.svg' +import WELL_CROSS_SECTION_IMAGE from '../../../../images/well_cross_section.svg' + +import styles from './TipPositionInput.module.css' + +const WELL_HEIGHT_PIXELS = 145 +const WELL_WIDTH_PIXELS = 100 +const PIXEL_DECIMALS = 2 + +interface TipPositionAllVizProps { + mmFromBottom: number + xPosition: number + wellDepthMm: number + xWidthMm: number +} + +export function TipPositionAllViz(props: TipPositionAllVizProps): JSX.Element { + const { mmFromBottom, xPosition, wellDepthMm, xWidthMm } = props + const fractionOfWellHeight = mmFromBottom / wellDepthMm + const pixelsFromBottom = + Number(fractionOfWellHeight) * WELL_HEIGHT_PIXELS - WELL_HEIGHT_PIXELS + const roundedPixelsFromBottom = round(pixelsFromBottom, PIXEL_DECIMALS) + const bottomPx = wellDepthMm + ? roundedPixelsFromBottom + : mmFromBottom - WELL_HEIGHT_PIXELS + + const xPx = (WELL_WIDTH_PIXELS / xWidthMm) * xPosition + const roundedXPx = round(xPx, PIXEL_DECIMALS) + return ( +
+ + + {props.wellDepthMm !== null && ( + {props.wellDepthMm}mm + )} + +
+ ) +} diff --git a/protocol-designer/src/components/StepEditForm/fields/TipPositionField/TipPositionInput.module.css b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/TipPositionInput.module.css index 181c6ae6f0d..36818a42e4b 100644 --- a/protocol-designer/src/components/StepEditForm/fields/TipPositionField/TipPositionInput.module.css +++ b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/TipPositionInput.module.css @@ -11,7 +11,7 @@ display: flex; flex-direction: row; justify-content: space-between; - margin: 3rem 0 2rem; + margin: 1rem 0 2rem; } .position_from_bottom_input { @@ -65,3 +65,14 @@ position: relative; left: 9px; } + +.tip_position_icon { + height: 1.5rem; + width: 1.5rem; + cursor: pointer; + color: #24313f; /* black80 */ +} + +.tip_position_icon:hover { + background-color: #e6e6e6; +} diff --git a/protocol-designer/src/components/StepEditForm/fields/TipPositionField/TipPositionModal.tsx b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/TipPositionModal.tsx index b2417810488..0d79a39ae9a 100644 --- a/protocol-designer/src/components/StepEditForm/fields/TipPositionField/TipPositionModal.tsx +++ b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/TipPositionModal.tsx @@ -2,107 +2,84 @@ import * as React from 'react' import { createPortal } from 'react-dom' import cx from 'classnames' import { useTranslation } from 'react-i18next' -import round from 'lodash/round' import { AlertModal, + DIRECTION_COLUMN, Flex, - HandleKeypress, - Icon, InputField, - OutlineButton, RadioGroup, + SPACING, + StyledText, } from '@opentrons/components' import { getMainPagePortalEl } from '../../../portals/MainPageModalPortal' import modalStyles from '../../../modals/modal.module.css' import { getIsTouchTipField } from '../../../../form-types' -import { TipPositionZAxisViz } from './TipPositionZAxisViz' +import { TOO_MANY_DECIMALS } from './constants' +import { TipPositionAllViz } from './TipPositionAllViz' import styles from './TipPositionInput.module.css' import * as utils from './utils' -import type { StepFieldName } from '../../../../form-types' -const SMALL_STEP_MM = 1 -const LARGE_STEP_MM = 10 -const DECIMALS_ALLOWED = 1 +import type { StepFieldName } from '../../../../form-types' -interface Props { - closeModal: () => unknown - isIndeterminate?: boolean - mmFromBottom: number | null +type Offset = 'x' | 'y' | 'z' +interface PositionSpec { name: StepFieldName - updateValue: (val: number | null | undefined) => unknown - wellDepthMm: number + value: number | null + updateValue: (val?: number | null) => void } +export type PositionSpecs = Record -const roundValue = (value: number | string | null): number => { - return round(Number(value), DECIMALS_ALLOWED) -} - -const TOO_MANY_DECIMALS: 'TOO_MANY_DECIMALS' = 'TOO_MANY_DECIMALS' -const OUT_OF_BOUNDS: 'OUT_OF_BOUNDS' = 'OUT_OF_BOUNDS' -type Error = typeof TOO_MANY_DECIMALS | typeof OUT_OF_BOUNDS - -const getErrorText = (args: { - errors: Error[] - maxMmFromBottom: number - minMmFromBottom: number - isPristine: boolean - t: any -}): string | null => { - const { errors, minMmFromBottom, maxMmFromBottom, isPristine, t } = args - - if (errors.includes(TOO_MANY_DECIMALS)) { - return t('tip_position.errors.TOO_MANY_DECIMALS') - } else if (!isPristine && errors.includes(OUT_OF_BOUNDS)) { - return t('tip_position.errors.OUT_OF_BOUNDS', { - minMmFromBottom, - maxMmFromBottom, - }) - } else { - return null - } +interface TipPositionModalProps { + closeModal: () => void + specs: PositionSpecs + wellDepthMm: number + wellXWidthMm: number + wellYWidthMm: number + isIndeterminate?: boolean } -const getErrors = (args: { - isDefault: boolean - value: string | null - maxMmFromBottom: number - minMmFromBottom: number -}): Error[] => { - const { isDefault, value, maxMmFromBottom, minMmFromBottom } = args - const errors: Error[] = [] - if (isDefault) return errors +export const TipPositionModal = ( + props: TipPositionModalProps +): JSX.Element | null => { + const { + isIndeterminate, + specs, + wellDepthMm, + wellXWidthMm, + wellYWidthMm, + closeModal, + } = props + const zSpec = specs.z + const ySpec = specs.y + const xSpec = specs.x - const v = Number(value) - if (value === null || Number.isNaN(v)) { - // blank or otherwise invalid should show this error as a fallback - return [OUT_OF_BOUNDS] - } - const correctDecimals = round(v, DECIMALS_ALLOWED) === v - const outOfBounds = v > maxMmFromBottom || v < minMmFromBottom + const { t } = useTranslation(['modal', 'button']) - if (!correctDecimals) { - errors.push(TOO_MANY_DECIMALS) + if (zSpec == null || xSpec == null || ySpec == null) { + console.error('expected to find specs for the zPosition but could not') } - if (outOfBounds) { - errors.push(OUT_OF_BOUNDS) - } - return errors -} -export const TipPositionModal = (props: Props): JSX.Element => { - const { isIndeterminate, name, wellDepthMm } = props - const { t } = useTranslation(['modal', 'button']) const defaultMmFromBottom = utils.getDefaultMmFromBottom({ - name, + name: zSpec.name, wellDepthMm, }) - const [value, setValue] = React.useState( - props.mmFromBottom === null ? null : String(props.mmFromBottom) + const [zValue, setZValue] = React.useState( + zSpec?.value == null ? null : String(zSpec?.value) + ) + const [yValue, setYValue] = React.useState( + ySpec?.value == null ? null : String(ySpec?.value) ) + const [xValue, setXValue] = React.useState( + xSpec?.value == null ? null : String(xSpec?.value) + ) + const [isDefault, setIsDefault] = React.useState( - !isIndeterminate && props.mmFromBottom === null + !isIndeterminate && + zSpec.value === null && + ySpec.value === 0 && + xSpec.value === 0 ) // in this modal, pristinity hides the OUT_OF_BOUNDS error only. const [isPristine, setPristine] = React.useState(true) @@ -111,54 +88,78 @@ export const TipPositionModal = (props: Props): JSX.Element => { maxMmFromBottom: number minMmFromBottom: number } => { - if (getIsTouchTipField(name)) { + if (getIsTouchTipField(zSpec?.name ?? '')) { return { - maxMmFromBottom: roundValue(wellDepthMm), - minMmFromBottom: roundValue(wellDepthMm / 2), + maxMmFromBottom: utils.roundValue(wellDepthMm), + minMmFromBottom: utils.roundValue(wellDepthMm / 2), } } return { - maxMmFromBottom: roundValue(wellDepthMm * 2), + maxMmFromBottom: utils.roundValue(wellDepthMm * 2), minMmFromBottom: 0, } } + const { maxMmFromBottom, minMmFromBottom } = getMinMaxMmFromBottom() - const errors = getErrors({ - isDefault, - minMmFromBottom, - maxMmFromBottom, - value, - }) - const hasErrors = errors.length > 0 + const { minValue: yMinWidth, maxValue: yMaxWidth } = utils.getMinMaxWidth( + wellYWidthMm + ) + const { minValue: xMinWidth, maxValue: xMaxWidth } = utils.getMinMaxWidth( + wellXWidthMm + ) + + const createErrors = ( + value: string | null, + min: number, + max: number + ): utils.Error[] => { + return utils.getErrors({ isDefault, minMm: min, maxMm: max, value }) + } + const zErrors = createErrors(zValue, minMmFromBottom, maxMmFromBottom) + const xErrors = createErrors(xValue, xMinWidth, xMaxWidth) + const yErrors = createErrors(yValue, yMinWidth, yMaxWidth) + + const hasErrors = + zErrors.length > 0 || xErrors.length > 0 || yErrors.length > 0 const hasVisibleErrors = isPristine - ? errors.includes(TOO_MANY_DECIMALS) + ? zErrors.includes(TOO_MANY_DECIMALS) || + xErrors.includes(TOO_MANY_DECIMALS) || + yErrors.includes(TOO_MANY_DECIMALS) : hasErrors - const errorText = getErrorText({ - errors, - maxMmFromBottom, - minMmFromBottom, - isPristine, - t, - }) + + const createErrorText = ( + errors: utils.Error[], + min: number, + max: number + ): string | null => { + return utils.getErrorText({ errors, minMm: min, maxMm: max, isPristine, t }) + } + + const zErrorText = createErrorText(zErrors, minMmFromBottom, maxMmFromBottom) + const xErrorText = createErrorText(xErrors, xMinWidth, xMaxWidth) + const yErrorText = createErrorText(yErrors, yMinWidth, yMaxWidth) const handleDone = (): void => { setPristine(false) - if (!hasErrors) { if (isDefault) { - props.updateValue(null) + zSpec?.updateValue(null) + xSpec?.updateValue(0) + ySpec?.updateValue(0) } else { - props.updateValue(value === null ? null : Number(value)) + zSpec?.updateValue(zValue === null ? null : Number(zValue)) + xSpec?.updateValue(xValue === null ? null : Number(xValue)) + ySpec?.updateValue(yValue === null ? null : Number(yValue)) } - props.closeModal() + closeModal() } } const handleCancel = (): void => { - props.closeModal() + closeModal() } - const handleChange = (newValueRaw: string | number): void => { + const handleZChange = (newValueRaw: string | number): void => { // if string, strip non-number characters from string and cast to number const newValue = typeof newValueRaw === 'string' @@ -166,147 +167,177 @@ export const TipPositionModal = (props: Props): JSX.Element => { : String(newValueRaw) if (newValue === '.') { - setValue('0.') + setZValue('0.') } else { - setValue(Number(newValue) >= 0 ? newValue : '0') + setZValue(Number(newValue) >= 0 ? newValue : '0') } } - const handleInputFieldChange = ( + const handleZInputFieldChange = ( e: React.ChangeEvent ): void => { - handleChange(e.currentTarget.value) + handleZChange(e.currentTarget.value) } - const handleIncrementDecrement = (delta: number): void => { - const prevValue = value === null ? defaultMmFromBottom : Number(value) - setIsDefault(false) - handleChange(roundValue(prevValue + delta)) + const handleXChange = (newValueRaw: string | number): void => { + // if string, strip non-number characters from string and cast to number + const newValue = + typeof newValueRaw === 'string' + ? newValueRaw.replace(/[^-.0-9]/g, '') + : String(newValueRaw) + + if (newValue === '.') { + setXValue('0.') + } else { + setXValue(newValue) + } } - const makeHandleIncrement = (step: number): (() => void) => () => { - handleIncrementDecrement(step) + const handleXInputFieldChange = ( + e: React.ChangeEvent + ): void => { + handleXChange(e.currentTarget.value) } - const makeHandleDecrement = (step: number): (() => void) => () => { - handleIncrementDecrement(step * -1) + const handleYChange = (newValueRaw: string | number): void => { + // if string, strip non-number characters from string and cast to number + const newValue = + typeof newValueRaw === 'string' + ? newValueRaw.replace(/[^-.0-9]/g, '') + : String(newValueRaw) + + if (newValue === '.') { + setYValue('0.') + } else { + setYValue(newValue) + } } - const TipPositionInputField = !isDefault && ( - - ) + const handleYInputFieldChange = ( + e: React.ChangeEvent + ): void => { + handleYChange(e.currentTarget.value) + } + + const TipPositionInputField = !isDefault ? ( + + + + {t('tip_position.field_titles.x_position')} + + + + + + {t('tip_position.field_titles.y_position')} + + + + + + {t('tip_position.field_titles.z_position')} + + + + + ) : null // Mix Form's asp/disp tip position field has different default value text - const isMixAspDispField = name === 'mix_mmFromBottom' + const isMixAspDispField = zSpec?.name === 'mix_mmFromBottom' return createPortal( - - -
-

{t('tip_position.title')}

-

{t(`tip_position.body.${name}`)}

-
-
- -
- ) => { - setIsDefault(e.currentTarget.value === 'default') - }} - options={[ - { - name: isMixAspDispField - ? `Aspirate 1mm, Dispense 0.5mm from the bottom (default)` - : `${defaultMmFromBottom} mm from the bottom (default)`, - value: 'default', - }, - { - name: 'Custom', - value: 'custom', - }, - ]} - name="TipPositionOptions" - /> - {TipPositionInputField} -
- -
- {!isDefault && ( -
- - - - - - -
- )} - -
+
+

{t('tip_position.title')}

+

{t(`tip_position.body.${zSpec?.name}`)}

+
+
+ + + ) => { + setIsDefault(e.currentTarget.value === 'default') + }} + options={[ + { + name: isMixAspDispField + ? t('tip_position.radio_button.mix') + : t('tip_position.radio_button.default', { + defaultMmFromBottom, + }), + value: 'default', + }, + { + name: t('tip_position.radio_button.custom'), + value: 'custom', + }, + ]} + name="TipPositionOptions" + /> + {TipPositionInputField} -
- - , + +
+ +
+
+
+
, getMainPagePortalEl() ) } diff --git a/protocol-designer/src/components/StepEditForm/fields/TipPositionField/ZTipPositionModal.tsx b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/ZTipPositionModal.tsx new file mode 100644 index 00000000000..d9437ec820b --- /dev/null +++ b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/ZTipPositionModal.tsx @@ -0,0 +1,260 @@ +import * as React from 'react' +import { createPortal } from 'react-dom' +import cx from 'classnames' +import { useTranslation } from 'react-i18next' +import { + AlertModal, + Flex, + HandleKeypress, + Icon, + InputField, + OutlineButton, + RadioGroup, +} from '@opentrons/components' +import { getMainPagePortalEl } from '../../../portals/MainPageModalPortal' +import { getIsTouchTipField } from '../../../../form-types' +import { TipPositionZAxisViz } from './TipPositionZAxisViz' +import * as utils from './utils' +import { LARGE_STEP_MM, SMALL_STEP_MM, TOO_MANY_DECIMALS } from './constants' + +import type { StepFieldName } from '../../../../form-types' + +import modalStyles from '../../../modals/modal.module.css' +import styles from './TipPositionInput.module.css' + +interface ZTipPositionModalProps { + closeModal: () => void + mmFromBottom: number | null + name: StepFieldName + updateValue: (val?: number | null) => unknown + wellDepthMm: number + isIndeterminate?: boolean +} + +export function ZTipPositionModal(props: ZTipPositionModalProps): JSX.Element { + const { + isIndeterminate, + name, + wellDepthMm, + mmFromBottom, + closeModal, + updateValue, + } = props + const { t } = useTranslation(['modal', 'button']) + const defaultMmFromBottom = utils.getDefaultMmFromBottom({ + name, + wellDepthMm, + }) + + const [value, setValue] = React.useState( + mmFromBottom === null ? null : String(mmFromBottom) + ) + const [isDefault, setIsDefault] = React.useState( + !isIndeterminate && mmFromBottom === null + ) + // in this modal, pristinity hides the OUT_OF_BOUNDS error only. + const [isPristine, setPristine] = React.useState(true) + + const getMinMaxMmFromBottom = (): { + maxMmFromBottom: number + minMmFromBottom: number + } => { + if (getIsTouchTipField(name)) { + return { + maxMmFromBottom: utils.roundValue(wellDepthMm), + minMmFromBottom: utils.roundValue(wellDepthMm / 2), + } + } + return { + maxMmFromBottom: utils.roundValue(wellDepthMm * 2), + minMmFromBottom: 0, + } + } + const { maxMmFromBottom, minMmFromBottom } = getMinMaxMmFromBottom() + const errors = utils.getErrors({ + isDefault, + minMm: minMmFromBottom, + maxMm: maxMmFromBottom, + value, + }) + const hasErrors = errors.length > 0 + const hasVisibleErrors = isPristine + ? errors.includes(TOO_MANY_DECIMALS) + : hasErrors + const errorText = utils.getErrorText({ + errors, + minMm: maxMmFromBottom, + maxMm: minMmFromBottom, + isPristine, + t, + }) + + const handleDone = (): void => { + setPristine(false) + + if (!hasErrors) { + if (isDefault) { + updateValue(null) + } else { + updateValue(value === null ? null : Number(value)) + } + closeModal() + } + } + + const handleCancel = (): void => { + closeModal() + } + + const handleChange = (newValueRaw: string | number): void => { + // if string, strip non-number characters from string and cast to number + const newValue = + typeof newValueRaw === 'string' + ? newValueRaw.replace(/[^.0-9]/, '') + : String(newValueRaw) + + if (newValue === '.') { + setValue('0.') + } else { + setValue(Number(newValue) >= 0 ? newValue : '0') + } + } + + const handleInputFieldChange = ( + e: React.ChangeEvent + ): void => { + handleChange(e.currentTarget.value) + } + + const handleIncrementDecrement = (delta: number): void => { + const prevValue = value === null ? defaultMmFromBottom : Number(value) + setIsDefault(false) + handleChange(utils.roundValue(prevValue + delta)) + } + + const makeHandleIncrement = (step: number): (() => void) => () => { + handleIncrementDecrement(step) + } + + const makeHandleDecrement = (step: number): (() => void) => () => { + handleIncrementDecrement(step * -1) + } + + const TipPositionInputField = !isDefault && ( + + ) + + return createPortal( + + +
+

{t('tip_position.title')}

+

{t(`tip_position.body.${name}`)}

+
+
+ +
+ ) => { + setIsDefault(e.currentTarget.value === 'default') + }} + options={[ + { + name: t('tip_position.radio_button.default', { + defaultMmFromBottom, + }), + value: 'default', + }, + { + name: t('tip_position.radio_button.custom'), + value: 'custom', + }, + ]} + name="TipPositionOptions" + /> + {TipPositionInputField} +
+ +
+ {!isDefault ? ( +
+ + + + + + +
+ ) : null} + +
+
+
+
+
, + getMainPagePortalEl() + ) +} diff --git a/protocol-designer/src/components/StepEditForm/fields/TipPositionField/__tests__/TipPositionField.test.tsx b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/__tests__/TipPositionField.test.tsx new file mode 100644 index 00000000000..36e1d07a0f4 --- /dev/null +++ b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/__tests__/TipPositionField.test.tsx @@ -0,0 +1,113 @@ +import * as React from 'react' +import { fireEvent, screen } from '@testing-library/react' +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { fixture96Plate } from '@opentrons/shared-data' +import { renderWithProviders } from '../../../../../__testing-utils__' +import { i18n } from '../../../../../localization' +import { getLabwareEntities } from '../../../../../step-forms/selectors' +import { ZTipPositionModal } from '../ZTipPositionModal' +import { TipPositionModal } from '../TipPositionModal' +import { TipPositionField } from '../index' +import type { LabwareDefinition2 } from '@opentrons/shared-data' + +vi.mock('../../../../../step-forms/selectors') +vi.mock('../ZTipPositionModal') +vi.mock('../TipPositionModal') +const render = (props: React.ComponentProps) => { + return renderWithProviders(, { + i18nInstance: i18n, + })[0] +} +const mockDelay = 'aspirate_delay_mmFromBottom' +const mockAspirate = 'aspirate_mmFromBottom' +const mockLabwareId = 'mockId' +describe('TipPositionField', () => { + let props: React.ComponentProps + + beforeEach(() => { + props = { + zField: mockDelay, + labwareId: mockLabwareId, + propsForFields: { + [mockDelay]: { + name: mockDelay, + value: null, + updateValue: vi.fn(), + tooltipContent: 'mock content', + isIndeterminate: false, + disabled: false, + } as any, + }, + } + vi.mocked(TipPositionModal).mockReturnValue( +
mock TipPositionModal
+ ) + vi.mocked(ZTipPositionModal).mockReturnValue( +
mock ZTipPositionModal
+ ) + vi.mocked(getLabwareEntities).mockReturnValue({ + [mockLabwareId]: { + id: mockLabwareId, + labwareDefURI: 'mock uri', + def: fixture96Plate as LabwareDefinition2, + }, + }) + }) + it('renders the input field and header when x and y fields are not provided', () => { + render(props) + screen.getByText('mm') + fireEvent.click(screen.getByRole('textbox', { name: '' })) + expect(screen.getByRole('textbox', { name: '' })).not.toBeDisabled() + screen.getByText('mock ZTipPositionModal') + }) + it('renders the input field but it is disabled', () => { + props = { + ...props, + propsForFields: { + [mockDelay]: { + name: mockDelay, + value: null, + updateValue: vi.fn(), + tooltipContent: 'mock content', + isIndeterminate: false, + disabled: true, + } as any, + }, + } + render(props) + expect(screen.getByRole('textbox', { name: '' })).toBeDisabled() + }) + it('renders the icon when x,y, and z fields are provided', () => { + const mockX = 'aspirate_x_position' + const mockY = 'aspirate_y_position' + props = { + zField: mockAspirate, + xField: mockX, + yField: mockY, + labwareId: mockLabwareId, + propsForFields: { + [mockAspirate]: { + name: mockAspirate, + value: null, + updateValue: vi.fn(), + tooltipContent: 'mock content', + isIndeterminate: false, + disabled: false, + } as any, + [mockX]: { + name: mockX, + value: null, + updateValue: vi.fn(), + } as any, + [mockY]: { + name: mockY, + value: null, + updateValue: vi.fn(), + } as any, + }, + } + render(props) + fireEvent.click(screen.getByTestId('TipPositionIcon_aspirate_mmFromBottom')) + screen.getByText('mock TipPositionModal') + }) +}) diff --git a/protocol-designer/src/components/StepEditForm/fields/TipPositionField/__tests__/TipPositionModal.test.tsx b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/__tests__/TipPositionModal.test.tsx new file mode 100644 index 00000000000..5fccf40a480 --- /dev/null +++ b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/__tests__/TipPositionModal.test.tsx @@ -0,0 +1,124 @@ +import * as React from 'react' +import { fireEvent, screen } from '@testing-library/react' +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { renderWithProviders } from '../../../../../__testing-utils__' +import { i18n } from '../../../../../localization' +import { TipPositionModal } from '../TipPositionModal' +import { TipPositionAllViz } from '../TipPositionAllViz' + +vi.mock('../TipPositionAllViz') +const render = (props: React.ComponentProps) => { + return renderWithProviders(, { + i18nInstance: i18n, + })[0] +} + +const mockUpdateZSpec = vi.fn() +const mockUpdateXSpec = vi.fn() +const mockUpdateYSpec = vi.fn() + +describe('TipPositionModal', () => { + let props: React.ComponentProps + + beforeEach(() => { + props = { + closeModal: vi.fn(), + wellDepthMm: 50, + wellXWidthMm: 10.3, + wellYWidthMm: 10.5, + isIndeterminate: false, + specs: { + z: { + name: 'aspirate_mmFromBottom', + value: null, + updateValue: mockUpdateZSpec, + }, + y: { + name: 'aspirate_y_position', + value: 0, + updateValue: mockUpdateXSpec, + }, + x: { + name: 'aspirate_x_position', + value: 0, + updateValue: mockUpdateYSpec, + }, + }, + } + vi.mocked(TipPositionAllViz).mockReturnValue(
mock TipPositionViz
) + }) + it('renders the modal text and radio button text', () => { + render(props) + screen.getByText('Tip Positioning') + screen.getByText('Change from where in the well the robot aspirates') + screen.getByRole('radio', { name: '1 mm from the bottom center (default)' }) + screen.getByRole('radio', { name: 'Custom' }) + fireEvent.click(screen.getByText('cancel')) + expect(props.closeModal).toHaveBeenCalled() + fireEvent.click(screen.getByText('done')) + expect(props.closeModal).toHaveBeenCalled() + expect(mockUpdateXSpec).toHaveBeenCalled() + expect(mockUpdateYSpec).toHaveBeenCalled() + expect(mockUpdateZSpec).toHaveBeenCalled() + }) + it('renders the custom options, captions, and visual', () => { + render(props) + fireEvent.click(screen.getByRole('radio', { name: 'Custom' })) + expect(screen.getAllByRole('textbox', { name: '' })).toHaveLength(3) + screen.getByText('X position') + screen.getByText('between -5.15 and 5.15') + screen.getByText('Y position') + screen.getByText('between -5.25 and 5.25') + screen.getByText('Z position') + screen.getByText('between 0 and 100') + screen.getByText('mock TipPositionViz') + }) + it('renders a custom input field and clicks on it, calling the mock updates', () => { + render(props) + fireEvent.click(screen.getByRole('radio', { name: 'Custom' })) + const xInputField = screen.getAllByRole('textbox', { name: '' })[0] + fireEvent.change(xInputField, { target: { value: 3 } }) + const yInputField = screen.getAllByRole('textbox', { name: '' })[1] + fireEvent.change(yInputField, { target: { value: -2 } }) + const zInputField = screen.getAllByRole('textbox', { name: '' })[2] + fireEvent.change(zInputField, { target: { value: 10 } }) + fireEvent.click(screen.getByText('done')) + expect(props.closeModal).toHaveBeenCalled() + expect(mockUpdateXSpec).toHaveBeenCalled() + expect(mockUpdateYSpec).toHaveBeenCalled() + expect(mockUpdateZSpec).toHaveBeenCalled() + }) + it('renders custom input fields and displays error texts', () => { + props = { + ...props, + specs: { + z: { + name: 'aspirate_mmFromBottom', + value: 101, + updateValue: mockUpdateZSpec, + }, + y: { + name: 'aspirate_y_position', + value: -500, + updateValue: mockUpdateXSpec, + }, + x: { + name: 'aspirate_x_position', + value: 10.7, + updateValue: mockUpdateYSpec, + }, + }, + } + render(props) + fireEvent.click(screen.getByText('done')) + // display out of bounds error + screen.getByText('accepted range is 0 to 100') + screen.getByText('accepted range is -5.25 to 5.25') + screen.getByText('accepted range is -5.15 to 5.15') + const xInputField = screen.getAllByRole('textbox', { name: '' })[0] + fireEvent.change(xInputField, { target: { value: 3.55555 } }) + fireEvent.click(screen.getByText('done')) + // display too many decimals error + screen.getByText('a max of 1 decimal place is allowed') + }) +}) diff --git a/protocol-designer/src/components/StepEditForm/fields/TipPositionField/constants.ts b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/constants.ts new file mode 100644 index 00000000000..c790cb449cc --- /dev/null +++ b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/constants.ts @@ -0,0 +1,4 @@ +export const DECIMALS_ALLOWED = 1 +export const SMALL_STEP_MM = 1 +export const LARGE_STEP_MM = 10 +export const TOO_MANY_DECIMALS: 'TOO_MANY_DECIMALS' = 'TOO_MANY_DECIMALS' diff --git a/protocol-designer/src/components/StepEditForm/fields/TipPositionField/index.tsx b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/index.tsx index ccaa80e13d5..91ececa71c8 100644 --- a/protocol-designer/src/components/StepEditForm/fields/TipPositionField/index.tsx +++ b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/index.tsx @@ -2,13 +2,16 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' import { useSelector } from 'react-redux' import { + COLORS, + Flex, FormGroup, + Icon, InputField, Tooltip, useHoverTooltip, UseHoverTooltipTargetProps, } from '@opentrons/components' -import { getWellsDepth } from '@opentrons/shared-data' +import { getWellsDepth, getWellDimension } from '@opentrons/shared-data' import { getIsTouchTipField, getIsDelayPositionField, @@ -16,28 +19,40 @@ import { import { selectors as stepFormSelectors } from '../../../../step-forms' import { TipPositionModal } from './TipPositionModal' import { getDefaultMmFromBottom } from './utils' +import { ZTipPositionModal } from './ZTipPositionModal' +import type { + TipXOffsetFields, + TipYOffsetFields, + TipZOffsetFields, +} from '../../../../form-types' +import type { FieldPropsByName } from '../../types' +import type { PositionSpecs } from './TipPositionModal' + import stepFormStyles from '../../StepEditForm.module.css' import styles from './TipPositionInput.module.css' -import type { FieldProps } from '../../types' -interface TipPositionFieldProps extends FieldProps { +interface TipPositionFieldProps { + propsForFields: FieldPropsByName + zField: TipZOffsetFields + xField?: TipXOffsetFields + yField?: TipYOffsetFields labwareId?: string | null - className?: string } export function TipPositionField(props: TipPositionFieldProps): JSX.Element { + const { labwareId, propsForFields, zField, xField, yField } = props const { - disabled, - name, + name: zName, + value: rawZValue, + updateValue: zUpdateValue, tooltipContent, - updateValue, isIndeterminate, - labwareId, - value: rawValue, - } = props + disabled, + } = propsForFields[zField] + const { t } = useTranslation('application') const [targetProps, tooltipProps] = useHoverTooltip() - const [isModalOpen, setModalOpen] = React.useState(false) + const [isModalOpen, setModalOpen] = React.useState(false) const labwareEntities = useSelector(stepFormSelectors.getLabwareEntities) const labwareDef = labwareId != null && labwareEntities[labwareId] != null @@ -45,68 +60,137 @@ export function TipPositionField(props: TipPositionFieldProps): JSX.Element { : null let wellDepthMm = 0 + let wellXWidthMm = 0 + let wellYWidthMm = 0 + if (labwareDef != null) { - // NOTE: only taking depth of first well in labware def, UI not currently equipped for multiple depths + // NOTE: only taking depth of first well in labware def, UI not currently equipped for multiple depths/widths const firstWell = labwareDef.wells.A1 if (firstWell) { wellDepthMm = getWellsDepth(labwareDef, ['A1']) + wellXWidthMm = getWellDimension(labwareDef, ['A1'], 'x') + wellYWidthMm = getWellDimension(labwareDef, ['A1'], 'y') } } - if (wellDepthMm === 0 && labwareId != null && labwareDef != null) { + if ( + (wellDepthMm === 0 || wellXWidthMm === 0 || wellYWidthMm === 0) && + labwareId != null && + labwareDef != null + ) { console.error( - `expected to find the well depth mm with labwareId ${labwareId} but could not` + `expected to find all well dimensions mm with labwareId ${labwareId} but could not` ) } - const handleOpen = (): void => { - if (wellDepthMm) { + const handleOpen = (has3Specs: boolean): void => { + if (has3Specs && wellDepthMm && wellXWidthMm && wellYWidthMm) { + setModalOpen(true) + } + if (!has3Specs && wellDepthMm) { setModalOpen(true) } } const handleClose = (): void => { setModalOpen(false) } - const isTouchTipField = getIsTouchTipField(name) - const isDelayPositionField = getIsDelayPositionField(name) - let value: string | number = '0' - const mmFromBottom = typeof rawValue === 'number' ? rawValue : null + const isTouchTipField = getIsTouchTipField(zName) + const isDelayPositionField = getIsDelayPositionField(zName) + let zValue: string | number = '0' + const mmFromBottom = typeof rawZValue === 'number' ? rawZValue : null if (wellDepthMm !== null) { // show default value for field in parens if no mmFromBottom value is selected - value = - mmFromBottom !== null - ? mmFromBottom - : getDefaultMmFromBottom({ name, wellDepthMm }) + zValue = + mmFromBottom ?? getDefaultMmFromBottom({ name: zName, wellDepthMm }) } + + let modal = ( + + ) + if (yField != null && xField != null) { + const { + name: xName, + value: rawXValue, + updateValue: xUpdateValue, + } = propsForFields[xField] + const { + name: yName, + value: rawYValue, + updateValue: yUpdateValue, + } = propsForFields[yField] + + const specs: PositionSpecs = { + z: { + name: zName, + value: mmFromBottom, + updateValue: zUpdateValue, + }, + x: { + name: xName, + value: rawXValue != null ? Number(rawXValue) : null, + updateValue: xUpdateValue, + }, + y: { + name: yName, + value: rawYValue != null ? Number(rawYValue) : null, + updateValue: yUpdateValue, + }, + } + + modal = ( + + ) + } + return ( <> {tooltipContent} - {isModalOpen && ( - - )} + {isModalOpen ? modal : null} - + {yField != null && xField != null ? ( + handleOpen(true) : () => {}} + id={`TipPositionIcon_${zName}`} + data-testid={`TipPositionIcon_${zName}`} + width="5rem" + > + + + ) : ( + handleOpen(false)} + value={String(zValue)} + isIndeterminate={isIndeterminate} + units={t('units.millimeter')} + id={`TipPositionField_${zName}`} + /> + )} ) diff --git a/protocol-designer/src/components/StepEditForm/fields/TipPositionField/utils.ts b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/utils.ts index c4d4590c5dc..96ed4729d49 100644 --- a/protocol-designer/src/components/StepEditForm/fields/TipPositionField/utils.ts +++ b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/utils.ts @@ -1,9 +1,13 @@ +import round from 'lodash/round' +import { getIsTouchTipField } from '../../../../form-types' import { DEFAULT_MM_FROM_BOTTOM_ASPIRATE, DEFAULT_MM_FROM_BOTTOM_DISPENSE, DEFAULT_MM_TOUCH_TIP_OFFSET_FROM_TOP, } from '../../../../constants' -import { StepFieldName, getIsTouchTipField } from '../../../../form-types' +import { DECIMALS_ALLOWED, TOO_MANY_DECIMALS } from './constants' +import type { StepFieldName } from '../../../../form-types' + // TODO: Ian + Brian 2019-02-13 this should switch on stepType, not use field // name to infer step type! // @@ -41,3 +45,70 @@ export function getDefaultMmFromBottom(args: { return DEFAULT_MM_TOUCH_TIP_OFFSET_FROM_TOP + wellDepthMm } } + +export const roundValue = (value: number | string | null): number => { + return value === null ? 0 : round(Number(value), DECIMALS_ALLOWED) +} + +const OUT_OF_BOUNDS: 'OUT_OF_BOUNDS' = 'OUT_OF_BOUNDS' +export type Error = typeof TOO_MANY_DECIMALS | typeof OUT_OF_BOUNDS + +export const getErrorText = (args: { + errors: Error[] + maxMm: number + minMm: number + isPristine: boolean + t: any +}): string | null => { + const { errors, minMm, maxMm, isPristine, t } = args + + if (errors.includes(TOO_MANY_DECIMALS)) { + return t('tip_position.errors.TOO_MANY_DECIMALS') + } else if (!isPristine && errors.includes(OUT_OF_BOUNDS)) { + return t('tip_position.errors.OUT_OF_BOUNDS', { + minMm, + maxMm, + }) + } else { + return null + } +} + +export const getErrors = (args: { + isDefault: boolean + value: string | null + maxMm: number + minMm: number +}): Error[] => { + const { isDefault, value: rawValue, maxMm, minMm } = args + const errors: Error[] = [] + if (isDefault) return errors + + const value = Number(rawValue) + if (rawValue === null || Number.isNaN(value)) { + // blank or otherwise invalid should show this error as a fallback + return [OUT_OF_BOUNDS] + } + const incorrectDecimals = round(value, DECIMALS_ALLOWED) !== value + const outOfBounds = value > maxMm || value < minMm + + if (incorrectDecimals) { + errors.push(TOO_MANY_DECIMALS) + } + if (outOfBounds) { + errors.push(OUT_OF_BOUNDS) + } + return errors +} + +interface MinMaxValues { + minValue: number + maxValue: number +} + +export const getMinMaxWidth = (width: number): MinMaxValues => { + return { + minValue: -width * 0.5, + maxValue: width * 0.5, + } +} diff --git a/protocol-designer/src/components/StepEditForm/forms/MixForm.tsx b/protocol-designer/src/components/StepEditForm/forms/MixForm.tsx index 87cfdbcd49b..7b5f8fb9503 100644 --- a/protocol-designer/src/components/StepEditForm/forms/MixForm.tsx +++ b/protocol-designer/src/components/StepEditForm/forms/MixForm.tsx @@ -117,7 +117,10 @@ export const MixForm = (props: StepFormProps): JSX.Element => { tiprack={propsForFields.tipRack.value} /> { label={t('form:step_edit_form.field.touchTip.label')} > { tiprack={propsForFields.tipRack.value} /> { className={styles.small_field} > form.stepType === 'moveLiquid' || form.stepType === 'mix' ) - const pipettingSavedStepsWithTipRack = pipettingSavedSteps.reduce( + const pipettingSavedStepsWithAdditionalFields = pipettingSavedSteps.reduce( (acc, item) => { const tipRackUri = tiprackAssignments[item.pipette] const tiprackLoadName = @@ -67,8 +67,16 @@ export const migrateFile = ( const tiprackIds = loadLabwareCommands .filter(command => command.params.loadName === tiprackLoadName) .map(command => command.params.labwareId) - - acc[item.id] = { ...item, tipRack: tiprackIds[0] } + const xyKeys = + item.stepType === 'mix' + ? { mix_x_position: 0, mix_y_position: 0 } + : { + aspirate_x_position: 0, + aspirate_y_position: 0, + dispense_x_position: 0, + dispense_y_position: 0, + } + acc[item.id] = { ...item, tipRack: tiprackIds[0], ...xyKeys } return acc }, {} @@ -82,7 +90,7 @@ export const migrateFile = ( ...designerApplication.data, savedStepForms: { ...designerApplication.data.savedStepForms, - ...pipettingSavedStepsWithTipRack, + ...pipettingSavedStepsWithAdditionalFields, }, pipetteTiprackAssignments: newTiprackAssignments, }, diff --git a/protocol-designer/src/localization/en/modal.json b/protocol-designer/src/localization/en/modal.json index edceb80718f..03e92e5ea55 100644 --- a/protocol-designer/src/localization/en/modal.json +++ b/protocol-designer/src/localization/en/modal.json @@ -61,6 +61,12 @@ }, "tip_position": { "title": "Tip Positioning", + "caption": "between {{min}} and {{max}}", + "radio_button": { + "default": "{{defaultMmFromBottom}} mm from the bottom center (default)", + "mix": "Aspirate 1mm, Dispense 0.5mm from the bottom center (default)", + "custom": "Custom" + }, "body": { "aspirate_mmFromBottom": "Change from where in the well the robot aspirates", "dispense_mmFromBottom": "Change from where in the well the robot dispenses", @@ -71,9 +77,14 @@ "aspirate_delay_mmFromBottom": "Change from where in the well the robot delays after aspirating", "dispense_delay_mmFromBottom": "Change from where in the well the robot delays after dispensing" }, + "field_titles": { + "z_position": "Z position", + "x_position": "X position", + "y_position": "Y position" + }, "errors": { "TOO_MANY_DECIMALS": "a max of 1 decimal place is allowed", - "OUT_OF_BOUNDS": "accepted range is {{minMmFromBottom}} to {{maxMmFromBottom}}" + "OUT_OF_BOUNDS": "accepted range is {{minMm}} to {{maxMm}}" }, "field_label": "Distance from bottom of well" }, diff --git a/protocol-designer/src/localization/en/tooltip.json b/protocol-designer/src/localization/en/tooltip.json index 59d2f32d1c9..7aa0031b76e 100644 --- a/protocol-designer/src/localization/en/tooltip.json +++ b/protocol-designer/src/localization/en/tooltip.json @@ -26,7 +26,7 @@ "aspirate_delay_mmFromBottom": "Distance from the bottom of the well", "aspirate_flowRate": "The speed at which the pipette aspirates", "aspirate_mix_checkbox": "Pipette up and down before aspirating", - "aspirate_mmFromBottom": "Distance from the bottom of the well", + "aspirate_mmFromBottom": "Adjust tip position for aspirate", "aspirate_touchTip_checkbox": "Touch tip to each side of well after aspirating", "aspirate_touchTip_mmFromBottom": "Distance from the bottom of the well", "blowout_checkbox": "Where to dispose of remaining volume in tip", @@ -37,12 +37,12 @@ "dispense_delay_mmFromBottom": "Distance from the bottom of the well", "dispense_flowRate": "The speed at which the pipette dispenses", "dispense_mix_checkbox": "Pipette up and down after dispensing", - "dispense_mmFromBottom": "Distance from the bottom of the well", + "dispense_mmFromBottom": "Adjust tip position for dispense", "dispense_touchTip_checkbox": "Touch tip to each side of well after dispensing", "dispense_touchTip_mmFromBottom": "Distance from the bottom of the well", "disposalVolume_checkbox": "Aspirate extra volume that is disposed of after a multi-dispense is complete. We recommend a disposal volume of at least the pipette's minimum.", "heaterShakerSetTimer": "Once this counter has elapsed, the module will deactivate the heater and shaker", - "mix_mmFromBottom": "Distance from the bottom of the well", + "mix_mmFromBottom": "Adjust tip position", "mix_touchTip_checkbox": "Touch tip to each side of the well after mixing", "mix_touchTip_mmFromBottom": "Distance from the bottom of the well", "preWetTip": "Pre-wet pipette tip by aspirating and dispensing 2/3 of the tip's max volume", @@ -67,7 +67,9 @@ "aspirate_touchTip_checkbox": "Touch tip is not supported", "blowout_checkbox": "Redundant with disposal volume", "dispense_mix_checkbox": "Unable to mix in a waste chute or trash bin", + "aspirate_mmFromBottom": "Tip position adjustment is not supported", "dispense_mmFromBottom": "Tip position adjustment is not supported", + "mix_mmFromBottom": "Tip position adjustment is not supported", "dispense_touchTip_checkbox": "Touch tip is not supported" } }, diff --git a/protocol-designer/src/step-forms/test/createPresavedStepForm.test.ts b/protocol-designer/src/step-forms/test/createPresavedStepForm.test.ts index 526c4c784b1..c440c25a0e5 100644 --- a/protocol-designer/src/step-forms/test/createPresavedStepForm.test.ts +++ b/protocol-designer/src/step-forms/test/createPresavedStepForm.test.ts @@ -187,6 +187,10 @@ describe('createPresavedStepForm', () => { stepDetails: '', stepName: 'transfer', volume: null, + aspirate_x_position: 0, + aspirate_y_position: 0, + dispense_x_position: 0, + dispense_y_position: 0, }) }) describe('mix step', () => { @@ -210,6 +214,8 @@ describe('createPresavedStepForm', () => { mix_wellOrder_first: 't2b', mix_wellOrder_second: 'l2r', blowout_checkbox: false, + mix_x_position: 0, + mix_y_position: 0, blowout_location: null, changeTip: 'always', stepDetails: '', diff --git a/protocol-designer/src/steplist/formLevel/getDefaultsForStepType.ts b/protocol-designer/src/steplist/formLevel/getDefaultsForStepType.ts index 66656441dd1..25442fac9af 100644 --- a/protocol-designer/src/steplist/formLevel/getDefaultsForStepType.ts +++ b/protocol-designer/src/steplist/formLevel/getDefaultsForStepType.ts @@ -37,6 +37,8 @@ export function getDefaultsForStepType( dropTip_location: null, nozzles: null, tipRack: null, + mix_x_position: 0, + mix_y_position: 0, } case 'moveLiquid': @@ -86,6 +88,10 @@ export function getDefaultsForStepType( dispense_delay_mmFromBottom: null, dropTip_location: null, nozzles: null, + dispense_x_position: 0, + dispense_y_position: 0, + aspirate_x_position: 0, + aspirate_y_position: 0, } case 'moveLabware': diff --git a/protocol-designer/src/steplist/formLevel/stepFormToArgs/mixFormToArgs.ts b/protocol-designer/src/steplist/formLevel/stepFormToArgs/mixFormToArgs.ts index 741355f95a0..d9d4936b71e 100644 --- a/protocol-designer/src/steplist/formLevel/stepFormToArgs/mixFormToArgs.ts +++ b/protocol-designer/src/steplist/formLevel/stepFormToArgs/mixFormToArgs.ts @@ -15,7 +15,14 @@ type MixStepArgs = MixArgs export const mixFormToArgs = ( hydratedFormData: HydratedMixFormDataLegacy ): MixStepArgs => { - const { labware, pipette, dropTip_location, nozzles } = hydratedFormData + const { + labware, + pipette, + dropTip_location, + nozzles, + mix_x_position, + mix_y_position, + } = hydratedFormData const matchingTipLiquidSpecs = getMatchingTipLiquidSpecs( pipette, hydratedFormData.volume, @@ -105,5 +112,9 @@ export const mixFormToArgs = ( dispenseDelaySeconds, dropTipLocation: dropTip_location, nozzles, + aspirateXOffset: mix_x_position ?? 0, + dispenseXOffset: mix_x_position ?? 0, + aspirateYOffset: mix_y_position ?? 0, + dispenseYOffset: mix_y_position ?? 0, } } diff --git a/protocol-designer/src/steplist/formLevel/stepFormToArgs/moveLiquidFormToArgs.ts b/protocol-designer/src/steplist/formLevel/stepFormToArgs/moveLiquidFormToArgs.ts index 7d330f54dbf..4b3023fdad3 100644 --- a/protocol-designer/src/steplist/formLevel/stepFormToArgs/moveLiquidFormToArgs.ts +++ b/protocol-designer/src/steplist/formLevel/stepFormToArgs/moveLiquidFormToArgs.ts @@ -78,6 +78,10 @@ export const moveLiquidFormToArgs = ( path, tipRack, nozzles, + aspirate_x_position, + dispense_x_position, + aspirate_y_position, + dispense_y_position, } = fields let sourceWells = getOrderedWells( fields.aspirate_wells, @@ -211,6 +215,10 @@ export const moveLiquidFormToArgs = ( name: hydratedFormData.stepName, dropTipLocation, nozzles, + aspirateXOffset: aspirate_x_position ?? 0, + aspirateYOffset: aspirate_y_position ?? 0, + dispenseXOffset: dispense_x_position ?? 0, + dispenseYOffset: dispense_y_position ?? 0, } console.assert( sourceWellsUnordered.length > 0, diff --git a/protocol-designer/src/steplist/formLevel/test/getDefaultsForStepType.test.ts b/protocol-designer/src/steplist/formLevel/test/getDefaultsForStepType.test.ts index 84803e31a74..cf0b72b84b0 100644 --- a/protocol-designer/src/steplist/formLevel/test/getDefaultsForStepType.test.ts +++ b/protocol-designer/src/steplist/formLevel/test/getDefaultsForStepType.test.ts @@ -59,13 +59,16 @@ describe('getDefaultsForStepType', () => { aspirate_delay_checkbox: false, aspirate_delay_mmFromBottom: null, aspirate_delay_seconds: `${DEFAULT_DELAY_SECONDS}`, - + aspirate_x_position: 0, + aspirate_y_position: 0, dispense_airGap_checkbox: false, dispense_airGap_volume: null, dispense_delay_checkbox: false, dispense_delay_seconds: `${DEFAULT_DELAY_SECONDS}`, dispense_delay_mmFromBottom: null, tipRack: null, + dispense_x_position: 0, + dispense_y_position: 0, }) }) }) @@ -94,6 +97,8 @@ describe('getDefaultsForStepType', () => { aspirate_flowRate: null, dispense_flowRate: null, tipRack: null, + mix_x_position: 0, + mix_y_position: 0, }) }) }) diff --git a/protocol-designer/src/timelineMiddleware/__tests__/generateRobotStateTimeline.test.ts b/protocol-designer/src/timelineMiddleware/__tests__/generateRobotStateTimeline.test.ts index ede96f0be52..1717dc838cb 100644 --- a/protocol-designer/src/timelineMiddleware/__tests__/generateRobotStateTimeline.test.ts +++ b/protocol-designer/src/timelineMiddleware/__tests__/generateRobotStateTimeline.test.ts @@ -50,6 +50,10 @@ describe('generateRobotStateTimeline', () => { description: null, nozzles: null, tipRack: 'tiprack1Id', + aspirateXOffset: 0, + aspirateYOffset: 0, + dispenseXOffset: 0, + dispenseYOffset: 0, }, }, b: { @@ -86,6 +90,10 @@ describe('generateRobotStateTimeline', () => { description: null, nozzles: null, tipRack: 'tiprack1Id', + aspirateXOffset: 0, + aspirateYOffset: 0, + dispenseXOffset: 0, + dispenseYOffset: 0, }, }, c: { @@ -114,6 +122,10 @@ describe('generateRobotStateTimeline', () => { dispenseDelaySeconds: null, nozzles: null, tipRack: 'tiprack1Id', + aspirateXOffset: 0, + aspirateYOffset: 0, + dispenseXOffset: 0, + dispenseYOffset: 0, }, }, } diff --git a/protocol-designer/src/ui/steps/test/selectors.test.ts b/protocol-designer/src/ui/steps/test/selectors.test.ts index 5cf64a59160..7cfa25c5e22 100644 --- a/protocol-designer/src/ui/steps/test/selectors.test.ts +++ b/protocol-designer/src/ui/steps/test/selectors.test.ts @@ -418,10 +418,23 @@ describe('_getSavedMultiSelectFieldValues', () => { isIndeterminate: false, value: undefined, }, + aspirate_labware: { value: 'aspirate_labware_id', isIndeterminate: false, }, + aspirate_x_position: { + isIndeterminate: false, + }, + aspirate_y_position: { + isIndeterminate: false, + }, + dispense_x_position: { + isIndeterminate: false, + }, + dispense_y_position: { + isIndeterminate: false, + }, aspirate_wells: { isIndeterminate: true, }, @@ -669,6 +682,18 @@ describe('_getSavedMultiSelectFieldValues', () => { path: { isIndeterminate: true, }, + aspirate_x_position: { + isIndeterminate: false, + }, + aspirate_y_position: { + isIndeterminate: false, + }, + dispense_x_position: { + isIndeterminate: false, + }, + dispense_y_position: { + isIndeterminate: false, + }, preWetTip: { isIndeterminate: true, }, @@ -850,6 +875,12 @@ describe('_getSavedMultiSelectFieldValues', () => { mix_touchTip_checkbox: { value: false, isIndeterminate: false }, mix_touchTip_mmFromBottom: { value: null, isIndeterminate: false }, nozzles: { value: undefined, isIndeterminate: false }, + mix_x_position: { + isIndeterminate: false, + }, + mix_y_position: { + isIndeterminate: false, + }, dropTip_location: { value: 'fixedTrash', isIndeterminate: false, @@ -920,6 +951,12 @@ describe('_getSavedMultiSelectFieldValues', () => { mix_touchTip_checkbox: { isIndeterminate: true }, mix_touchTip_mmFromBottom: { isIndeterminate: true }, nozzles: { isIndeterminate: true }, + mix_x_position: { + isIndeterminate: false, + }, + mix_y_position: { + isIndeterminate: false, + }, dropTip_location: { value: 'fixedTrash', isIndeterminate: false, diff --git a/shared-data/js/helpers/index.ts b/shared-data/js/helpers/index.ts index a65a83085de..b996606f6e8 100644 --- a/shared-data/js/helpers/index.ts +++ b/shared-data/js/helpers/index.ts @@ -202,6 +202,23 @@ export const getWellsDepth = ( return offsets[0] } +export const getWellDimension = ( + labwareDef: LabwareDefinition2, + wells: string[], + position: 'x' | 'y' +): number => { + const offsets = wells.map(well => { + const labwareWell = labwareDef.wells[well] + const shape = labwareWell.shape + if (shape === 'circular') { + return labwareWell.diameter + } else { + return position === 'x' ? labwareWell.xDimension : labwareWell.yDimension + } + }) + return offsets[0] +} + export const getSlotHasMatingSurfaceUnitVector = ( deckDef: DeckDefinition, addressableAreaName: string diff --git a/step-generation/src/__tests__/aspirate.test.ts b/step-generation/src/__tests__/aspirate.test.ts index 7731f5e389e..d937fcda7a4 100644 --- a/step-generation/src/__tests__/aspirate.test.ts +++ b/step-generation/src/__tests__/aspirate.test.ts @@ -67,6 +67,8 @@ describe('aspirate', () => { well: 'A1', } as AspDispAirgapParams), tipRack: 'tiprack1Id', + xOffset: 0, + yOffset: 0, } const result = aspirate(params, invariantContext, robotStateWithTip) expect(getSuccessResult(result).commands).toEqual([ @@ -82,6 +84,8 @@ describe('aspirate', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 5, }, }, @@ -106,6 +110,8 @@ describe('aspirate', () => { well: 'A1', } as AspDispAirgapParams), tipRack: 'tiprack1Id', + xOffset: 0, + yOffset: 0, }, invariantContext, robotStateWithTip @@ -133,6 +139,8 @@ describe('aspirate', () => { well: 'A1', } as AspDispAirgapParams), tipRack: 'tipRack', + xOffset: 0, + yOffset: 0, }, invariantContext, robotStateWithTip @@ -153,6 +161,8 @@ describe('aspirate', () => { well: 'A1', } as AspDispAirgapParams), tipRack: 'tipRack', + xOffset: 0, + yOffset: 0, }, invariantContext, robotStateWithTip @@ -170,6 +180,8 @@ describe('aspirate', () => { well: 'A1', } as AspDispAirgapParams), tipRack: 'tipRack', + xOffset: 0, + yOffset: 0, }, invariantContext, initialRobotState @@ -190,6 +202,8 @@ describe('aspirate', () => { well: 'A1', } as AspDispAirgapParams), tipRack: 'tipRack', + xOffset: 0, + yOffset: 0, }, invariantContext, robotStateWithTip @@ -214,6 +228,8 @@ describe('aspirate', () => { well: 'A1', } as AspDispAirgapParams), tipRack: 'tipRack', + xOffset: 0, + yOffset: 0, }, invariantContext, initialRobotState @@ -246,6 +262,8 @@ describe('aspirate', () => { well: 'A1', } as AspDispAirgapParams), tipRack: 'tipRack', + xOffset: 0, + yOffset: 0, }, invariantContext, robotStateWithTip @@ -278,6 +296,8 @@ describe('aspirate', () => { well: 'A1', } as AspDispAirgapParams), tipRack: 'tipRack', + xOffset: 0, + yOffset: 0, }, invariantContext, robotStateWithTip @@ -316,6 +336,8 @@ describe('aspirate', () => { well: 'A1', } as AspDispAirgapParams), tipRack: 'tipRack', + xOffset: 0, + yOffset: 0, }, invariantContext, robotStateWithTip @@ -348,6 +370,8 @@ describe('aspirate', () => { well: 'A1', } as AspDispAirgapParams), tipRack: 'tipRack', + xOffset: 0, + yOffset: 0, }, invariantContext, robotStateWithTip @@ -386,6 +410,8 @@ describe('aspirate', () => { well: 'A1', } as AspDispAirgapParams), tipRack: 'tipRack', + xOffset: 0, + yOffset: 0, }, invariantContext, robotStateWithTip @@ -414,6 +440,8 @@ describe('aspirate', () => { well: 'A1', } as AspDispAirgapParams), tipRack: 'tipRack', + xOffset: 0, + yOffset: 0, }, invariantContext, robotStateWithTip @@ -441,6 +469,8 @@ describe('aspirate', () => { well: 'A1', } as AspDispAirgapParams), tipRack: 'tipRack', + xOffset: 0, + yOffset: 0, }, invariantContext, robotStateWithTip @@ -468,6 +498,8 @@ describe('aspirate', () => { well: 'A1', } as AspDispAirgapParams), tipRack: 'tipRack', + xOffset: 0, + yOffset: 0, }, invariantContext, robotStateWithTip @@ -497,6 +529,8 @@ describe('aspirate', () => { well: 'A1', } as AspDispAirgapParams), tipRack: 'tipRack', + xOffset: 0, + yOffset: 0, }, invariantContext, robotStateWithTip diff --git a/step-generation/src/__tests__/consolidate.test.ts b/step-generation/src/__tests__/consolidate.test.ts index e43e31c4463..db0303605af 100644 --- a/step-generation/src/__tests__/consolidate.test.ts +++ b/step-generation/src/__tests__/consolidate.test.ts @@ -33,6 +33,8 @@ const airGapHelper = makeAirGapHelper({ origin: 'bottom', offset: { z: 11.54, + x: 0, + y: 0, }, }, }) @@ -98,6 +100,10 @@ beforeEach(() => { blowoutLocation: null, dropTipLocation: FIXED_TRASH_ID, tipRack: 'tiprack1Id', + aspirateXOffset: 0, + dispenseXOffset: 0, + aspirateYOffset: 0, + dispenseYOffset: 0, } }) @@ -259,6 +265,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -274,6 +282,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -307,6 +317,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -330,6 +342,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -363,6 +377,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -373,6 +389,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -383,6 +401,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -399,6 +419,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -409,6 +431,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -419,6 +443,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -454,6 +480,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: DISPENSE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -467,6 +495,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: DISPENSE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -501,6 +531,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: DISPENSE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -520,6 +552,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: DISPENSE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -553,6 +587,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: DISPENSE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -566,6 +602,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: DISPENSE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -599,6 +637,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -616,6 +656,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -655,6 +697,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -675,6 +719,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -715,6 +761,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -734,6 +782,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -1056,6 +1106,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.1, }, }, @@ -1080,6 +1132,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.1, }, }, @@ -1105,6 +1159,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.1, }, }, @@ -1163,6 +1219,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 11.54, }, }, @@ -1188,6 +1246,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.1, }, }, @@ -1246,6 +1306,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 11.54, }, }, @@ -1271,6 +1333,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.2, }, }, @@ -1313,6 +1377,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.2, }, }, @@ -1337,6 +1403,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.2, }, }, @@ -1385,6 +1453,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.1, }, }, @@ -1409,6 +1479,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.1, }, }, @@ -1434,6 +1506,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.1, }, }, @@ -1492,6 +1566,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 11.54, }, }, @@ -1517,6 +1593,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.2, }, }, @@ -1559,6 +1637,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.2, }, }, @@ -1583,6 +1663,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.2, }, }, @@ -1627,6 +1709,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 11.54, }, }, @@ -1697,6 +1781,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.1, }, }, @@ -1721,6 +1807,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.1, }, }, @@ -1746,6 +1834,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.1, }, }, @@ -1804,6 +1894,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 11.54, }, }, @@ -1830,6 +1922,8 @@ describe('consolidate single-channel', () => { origin: 'bottom', offset: { z: 3.1, + x: 0, + y: 0, }, }, flowRate: 2.1, @@ -1887,6 +1981,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 11.54, }, }, @@ -1912,6 +2008,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.2, }, }, @@ -1954,6 +2052,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.2, }, }, @@ -1978,6 +2078,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.2, }, }, @@ -2041,6 +2143,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.1, }, }, @@ -2065,6 +2169,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.1, }, }, @@ -2090,6 +2196,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.1, }, }, @@ -2148,6 +2256,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 11.54, }, }, @@ -2173,6 +2283,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.2, }, }, @@ -2215,6 +2327,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.2, }, }, @@ -2239,6 +2353,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.2, }, }, @@ -2298,6 +2414,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 11.54, }, }, @@ -2365,6 +2483,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.1, }, }, @@ -2389,6 +2509,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.1, }, }, @@ -2414,6 +2536,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.1, }, }, @@ -2472,6 +2596,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 11.54, }, }, @@ -2497,6 +2623,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.1, }, }, @@ -2555,6 +2683,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 11.54, }, }, @@ -2580,6 +2710,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.2, }, }, @@ -2622,6 +2754,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.2, }, }, @@ -2646,6 +2780,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.2, }, }, @@ -2705,6 +2841,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 11.54, }, }, @@ -2744,6 +2882,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.1, }, }, @@ -2769,6 +2909,8 @@ describe('consolidate single-channel', () => { origin: 'bottom', offset: { z: 3.1, + x: 0, + y: 0, }, }, flowRate: 2.2, @@ -2793,6 +2935,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.1, }, }, @@ -2851,6 +2995,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 11.54, }, }, @@ -2876,6 +3022,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.2, }, }, @@ -2918,6 +3066,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.2, }, }, @@ -2942,6 +3092,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.2, }, }, @@ -3000,6 +3152,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 11.54, }, }, @@ -3058,6 +3212,10 @@ describe('consolidate multi-channel', () => { volume: 140, tipRack: 'tiprack1Id', changeTip: 'once', + aspirateXOffset: 0, + dispenseXOffset: 0, + aspirateYOffset: 0, + dispenseYOffset: 0, } as ConsolidateArgs const result = consolidate(data, invariantContext, initialRobotState) const res = getSuccessResult(result) diff --git a/step-generation/src/__tests__/dispense.test.ts b/step-generation/src/__tests__/dispense.test.ts index 18e51c9b7a7..1ef07707d80 100644 --- a/step-generation/src/__tests__/dispense.test.ts +++ b/step-generation/src/__tests__/dispense.test.ts @@ -20,12 +20,11 @@ import { DEFAULT_PIPETTE, SOURCE_LABWARE, } from '../fixtures' -import { dispense } from '../commandCreators/atomic/dispense' -import { InvariantContext, RobotState } from '../types' -import type { - AspDispAirgapParams as V3AspDispAirgapParams, - DispenseParams, -} from '@opentrons/shared-data/protocol/types/schemaV3' +import { + ExtendedDispenseParams, + dispense, +} from '../commandCreators/atomic/dispense' +import type { InvariantContext, RobotState } from '../types' vi.mock('../utils/thermocyclerPipetteCollision') vi.mock('../utils/heaterShakerCollision') @@ -46,7 +45,7 @@ describe('dispense', () => { vi.resetAllMocks() }) describe('tip tracking & commands:', () => { - let params: V3AspDispAirgapParams + let params: ExtendedDispenseParams beforeEach(() => { params = { pipette: DEFAULT_PIPETTE, @@ -55,6 +54,8 @@ describe('dispense', () => { well: 'A1', offsetFromBottomMm: 5, flowRate: 6, + xOffset: 0, + yOffset: 0, } }) it('dispense normally (with tip)', () => { @@ -71,6 +72,8 @@ describe('dispense', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 5, }, }, @@ -99,7 +102,9 @@ describe('dispense', () => { volume: 50, labware: SOURCE_LABWARE, well: 'A1', - } as DispenseParams, + xOffset: 0, + yOffset: 0, + }, invariantContext, initialRobotState ) diff --git a/step-generation/src/__tests__/distribute.test.ts b/step-generation/src/__tests__/distribute.test.ts index 2db91df01d2..3e8fa31f749 100644 --- a/step-generation/src/__tests__/distribute.test.ts +++ b/step-generation/src/__tests__/distribute.test.ts @@ -36,6 +36,8 @@ const airGapHelper = makeAirGapHelper({ wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 11.54, }, }, @@ -44,6 +46,8 @@ const dispenseAirGapHelper = makeDispenseAirGapHelper({ wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 11.54, }, }, @@ -84,6 +88,10 @@ beforeEach(() => { aspirateAirGapVolume: null, touchTipAfterDispense: false, dropTipLocation: FIXED_TRASH_ID, + aspirateXOffset: 0, + dispenseXOffset: 0, + aspirateYOffset: 0, + dispenseYOffset: 0, } blowoutSingleToTrash = blowoutInPlaceHelper() blowoutSingleToSourceA1 = blowoutHelper(SOURCE_LABWARE, { @@ -274,6 +282,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -309,6 +319,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -320,6 +332,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -553,6 +567,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -565,6 +581,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -690,6 +708,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -701,6 +721,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -781,6 +803,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -793,6 +817,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -879,6 +905,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, diff --git a/step-generation/src/__tests__/mix.test.ts b/step-generation/src/__tests__/mix.test.ts index c2392a94c98..cc2115c42da 100644 --- a/step-generation/src/__tests__/mix.test.ts +++ b/step-generation/src/__tests__/mix.test.ts @@ -51,6 +51,10 @@ beforeEach(() => { aspirateDelaySeconds: null, dispenseDelaySeconds: null, dropTipLocation: FIXED_TRASH_ID, + aspirateXOffset: 0, + dispenseXOffset: 0, + aspirateYOffset: 0, + dispenseYOffset: 0, } invariantContext = makeContext() diff --git a/step-generation/src/__tests__/transfer.test.ts b/step-generation/src/__tests__/transfer.test.ts index 49319bfe2ea..f0c9b9fce7e 100644 --- a/step-generation/src/__tests__/transfer.test.ts +++ b/step-generation/src/__tests__/transfer.test.ts @@ -37,6 +37,8 @@ const airGapHelper = makeAirGapHelper({ wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 11.54, }, }, @@ -45,6 +47,8 @@ const dispenseAirGapHelper = makeDispenseAirGapHelper({ wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 11.54, }, }, @@ -78,6 +82,10 @@ beforeEach(() => { mixInDestination: null, blowoutLocation: null, dropTipLocation: FIXED_TRASH_ID, + aspirateXOffset: 0, + dispenseXOffset: 0, + aspirateYOffset: 0, + dispenseYOffset: 0, } invariantContext = makeContext() @@ -561,6 +569,8 @@ describe('advanced options', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -594,6 +604,8 @@ describe('advanced options', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -628,6 +640,8 @@ describe('advanced options', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -704,6 +718,8 @@ describe('advanced options', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -715,6 +731,8 @@ describe('advanced options', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -754,6 +772,8 @@ describe('advanced options', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -766,6 +786,8 @@ describe('advanced options', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -928,6 +950,8 @@ describe('advanced options', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: DISPENSE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -939,6 +963,8 @@ describe('advanced options', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: DISPENSE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -977,6 +1003,8 @@ describe('advanced options', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: DISPENSE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -986,6 +1014,8 @@ describe('advanced options', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: DISPENSE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -997,6 +1027,8 @@ describe('advanced options', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: DISPENSE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -1097,6 +1129,8 @@ describe('advanced options', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.1, }, }, @@ -1122,6 +1156,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 3.1, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -1146,6 +1182,8 @@ describe('advanced options', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.1, }, }, @@ -1171,6 +1209,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 3.1, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -1195,6 +1235,8 @@ describe('advanced options', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.1, }, }, @@ -1254,6 +1296,8 @@ describe('advanced options', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 11.54, }, }, @@ -1281,6 +1325,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 11.54, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -1303,8 +1349,11 @@ describe('advanced options', () => { wellName: 'B1', wellLocation: { origin: 'bottom', + offset: { z: DISPENSE_OFFSET_FROM_BOTTOM_MM, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -1346,6 +1395,8 @@ describe('advanced options', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.2, }, }, @@ -1371,6 +1422,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: DISPENSE_OFFSET_FROM_BOTTOM_MM, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -1432,6 +1485,8 @@ describe('advanced options', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.1, }, }, @@ -1457,6 +1512,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 3.1, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -1481,6 +1538,8 @@ describe('advanced options', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.1, }, }, @@ -1540,6 +1599,8 @@ describe('advanced options', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 11.54, }, }, @@ -1567,6 +1628,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 11.54, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -1591,6 +1654,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 3.2, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -1632,6 +1697,8 @@ describe('advanced options', () => { wellLocation: { origin: 'bottom', offset: { + y: 0, + x: 0, z: 3.2, }, }, @@ -1657,6 +1724,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 3.2, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -1716,6 +1785,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 11.54, + y: 0, + x: 0, }, }, flowRate: 2.1, @@ -1756,6 +1827,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 3.1, + y: 0, + x: 0, }, }, flowRate: 2.1, @@ -1780,6 +1853,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 3.1, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -1805,6 +1880,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 3.1, + y: 0, + x: 0, }, }, flowRate: 2.1, @@ -1829,6 +1906,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 3.1, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -1854,6 +1933,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 3.1, + y: 0, + x: 0, }, }, flowRate: 2.1, @@ -1913,6 +1994,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 11.54, + y: 0, + x: 0, }, }, flowRate: 2.1, @@ -1939,6 +2022,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 11.54, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -1963,6 +2048,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: DISPENSE_OFFSET_FROM_BOTTOM_MM, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -2005,6 +2092,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: DISPENSE_OFFSET_FROM_BOTTOM_MM, + y: 0, + x: 0, }, }, flowRate: 2.1, @@ -2029,6 +2118,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: DISPENSE_OFFSET_FROM_BOTTOM_MM, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -2091,6 +2182,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 3.1, + y: 0, + x: 0, }, }, flowRate: 2.1, @@ -2115,6 +2208,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 3.1, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -2140,6 +2235,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 3.1, + y: 0, + x: 0, }, }, flowRate: 2.1, @@ -2197,6 +2294,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 11.54, + y: 0, + x: 0, }, }, pipetteId: 'p300SingleId', @@ -2222,6 +2321,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 11.54, + y: 0, + x: 0, }, }, pipetteId: 'p300SingleId', @@ -2248,6 +2349,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: DISPENSE_OFFSET_FROM_BOTTOM_MM, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -2290,6 +2393,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: DISPENSE_OFFSET_FROM_BOTTOM_MM, + y: 0, + x: 0, }, }, flowRate: 2.1, @@ -2314,6 +2419,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: DISPENSE_OFFSET_FROM_BOTTOM_MM, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -2374,6 +2481,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 11.54, + y: 0, + x: 0, }, }, }, @@ -2442,6 +2551,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 3.1, + y: 0, + x: 0, }, }, flowRate: 2.1, @@ -2466,6 +2577,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 3.1, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -2491,6 +2604,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 3.1, + y: 0, + x: 0, }, }, flowRate: 2.1, @@ -2515,6 +2630,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 3.1, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -2540,6 +2657,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 3.1, + y: 0, + x: 0, }, }, flowRate: 2.1, @@ -2599,6 +2718,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 11.54, + y: 0, + x: 0, }, }, flowRate: 2.1, @@ -2625,6 +2746,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 11.54, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -2649,6 +2772,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: DISPENSE_OFFSET_FROM_BOTTOM_MM, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -2691,6 +2816,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: DISPENSE_OFFSET_FROM_BOTTOM_MM, + y: 0, + x: 0, }, }, flowRate: 2.1, @@ -2715,6 +2842,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: DISPENSE_OFFSET_FROM_BOTTOM_MM, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -2777,6 +2906,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 3.1, + y: 0, + x: 0, }, }, flowRate: 2.1, @@ -2801,6 +2932,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 3.1, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -2826,6 +2959,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 3.1, + y: 0, + x: 0, }, }, flowRate: 2.1, @@ -2885,6 +3020,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 11.54, + y: 0, + x: 0, }, }, flowRate: 2.1, @@ -2911,6 +3048,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 11.54, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -2935,6 +3074,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: DISPENSE_OFFSET_FROM_BOTTOM_MM, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -2977,6 +3118,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: DISPENSE_OFFSET_FROM_BOTTOM_MM, + y: 0, + x: 0, }, }, flowRate: 2.1, @@ -3001,6 +3144,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: DISPENSE_OFFSET_FROM_BOTTOM_MM, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -3061,6 +3206,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 11.54, + y: 0, + x: 0, }, }, }, @@ -3127,6 +3274,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 3.1, + y: 0, + x: 0, }, }, flowRate: 2.1, @@ -3151,6 +3300,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 3.1, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -3176,6 +3327,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 3.1, + y: 0, + x: 0, }, }, flowRate: 2.1, @@ -3200,6 +3353,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 3.1, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -3225,6 +3380,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 3.1, + y: 0, + x: 0, }, }, flowRate: 2.1, @@ -3284,6 +3441,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 11.54, + y: 0, + x: 0, }, }, flowRate: 2.1, @@ -3310,6 +3469,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 11.54, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -3334,6 +3495,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: DISPENSE_OFFSET_FROM_BOTTOM_MM, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -3376,6 +3539,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: DISPENSE_OFFSET_FROM_BOTTOM_MM, + y: 0, + x: 0, }, }, flowRate: 2.1, @@ -3399,6 +3564,8 @@ describe('advanced options', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: DISPENSE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -3459,6 +3626,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 11.54, + y: 0, + x: 0, }, }, volume: 3, @@ -3511,6 +3680,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 3.1, + y: 0, + x: 0, }, }, flowRate: 2.1, @@ -3535,6 +3706,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 3.1, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -3560,6 +3733,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 3.1, + y: 0, + x: 0, }, }, flowRate: 2.1, @@ -3619,6 +3794,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 11.54, + y: 0, + x: 0, }, }, flowRate: 2.1, @@ -3644,6 +3821,8 @@ describe('advanced options', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 11.54, }, }, @@ -3669,6 +3848,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: DISPENSE_OFFSET_FROM_BOTTOM_MM, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -3711,6 +3892,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: DISPENSE_OFFSET_FROM_BOTTOM_MM, + y: 0, + x: 0, }, }, flowRate: 2.1, @@ -3735,6 +3918,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: DISPENSE_OFFSET_FROM_BOTTOM_MM, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -3795,6 +3980,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 11.54, + y: 0, + x: 0, }, }, }, diff --git a/step-generation/src/commandCreators/atomic/aspirate.ts b/step-generation/src/commandCreators/atomic/aspirate.ts index fb360c4cebf..d7226da3387 100644 --- a/step-generation/src/commandCreators/atomic/aspirate.ts +++ b/step-generation/src/commandCreators/atomic/aspirate.ts @@ -18,6 +18,8 @@ import type { AspirateParams } from '@opentrons/shared-data/protocol/types/schem import type { CommandCreator, CommandCreatorError } from '../../types' export interface ExtendedAspirateParams extends AspirateParams { + xOffset: number + yOffset: number tipRack: string } /** Aspirate with given args. Requires tip. */ @@ -35,6 +37,8 @@ export const aspirate: CommandCreator = ( flowRate, isAirGap, tipRack, + xOffset, + yOffset, } = args const actionName = 'aspirate' const errors: CommandCreatorError[] = [] @@ -208,6 +212,8 @@ export const aspirate: CommandCreator = ( origin: 'bottom', offset: { z: offsetFromBottomMm, + x: xOffset, + y: yOffset, }, }, flowRate, diff --git a/step-generation/src/commandCreators/atomic/dispense.ts b/step-generation/src/commandCreators/atomic/dispense.ts index 58c7019fe75..2bec571bd6e 100644 --- a/step-generation/src/commandCreators/atomic/dispense.ts +++ b/step-generation/src/commandCreators/atomic/dispense.ts @@ -16,8 +16,12 @@ import type { CreateCommand } from '@opentrons/shared-data' import type { DispenseParams } from '@opentrons/shared-data/protocol/types/schemaV3' import type { CommandCreator, CommandCreatorError } from '../../types' +export interface ExtendedDispenseParams extends DispenseParams { + xOffset: number + yOffset: number +} /** Dispense with given args. Requires tip. */ -export const dispense: CommandCreator = ( +export const dispense: CommandCreator = ( args, invariantContext, prevRobotState @@ -30,6 +34,8 @@ export const dispense: CommandCreator = ( offsetFromBottomMm, flowRate, isAirGap, + xOffset, + yOffset, } = args const actionName = 'dispense' const errors: CommandCreatorError[] = [] @@ -172,6 +178,8 @@ export const dispense: CommandCreator = ( origin: 'bottom', offset: { z: offsetFromBottomMm, + x: xOffset, + y: yOffset, }, }, flowRate, diff --git a/step-generation/src/commandCreators/compound/consolidate.ts b/step-generation/src/commandCreators/compound/consolidate.ts index 6507f9227f2..b37f2ede1b0 100644 --- a/step-generation/src/commandCreators/compound/consolidate.ts +++ b/step-generation/src/commandCreators/compound/consolidate.ts @@ -152,6 +152,10 @@ export const consolidate: CommandCreator = ( mixFirstAspirate, mixInDestination, dropTipLocation, + aspirateXOffset, + aspirateYOffset, + dispenseXOffset, + dispenseYOffset, } = args const aspirateAirGapVolume = args.aspirateAirGapVolume || 0 const maxWellsPerChunk = Math.floor( @@ -220,6 +224,8 @@ export const consolidate: CommandCreator = ( offsetFromBottomMm: airGapOffsetSourceWell, isAirGap: true, tipRack: args.tipRack, + xOffset: 0, + yOffset: 0, }), ...(aspirateDelay != null ? [ @@ -277,6 +283,8 @@ export const consolidate: CommandCreator = ( flowRate: aspirateFlowRateUlSec, offsetFromBottomMm: aspirateOffsetFromBottomMm, tipRack: args.tipRack, + xOffset: aspirateXOffset, + yOffset: aspirateYOffset, }), ...delayAfterAspirateCommands, ...touchTipAfterAspirateCommand, @@ -326,6 +334,10 @@ export const consolidate: CommandCreator = ( aspirateDelaySeconds: aspirateDelay?.seconds, dispenseDelaySeconds: dispenseDelay?.seconds, tipRack: args.tipRack, + aspirateXOffset, + aspirateYOffset, + dispenseXOffset, + dispenseYOffset, }) : [] const preWetTipCommands = args.preWetTip // Pre-wet tip is equivalent to a single mix, with volume equal to the consolidate volume. @@ -342,6 +354,10 @@ export const consolidate: CommandCreator = ( aspirateDelaySeconds: aspirateDelay?.seconds, dispenseDelaySeconds: dispenseDelay?.seconds, tipRack: args.tipRack, + aspirateXOffset, + aspirateYOffset, + dispenseXOffset, + dispenseYOffset, }) : [] // can not mix in a waste chute @@ -360,6 +376,10 @@ export const consolidate: CommandCreator = ( aspirateDelaySeconds: aspirateDelay?.seconds, dispenseDelaySeconds: dispenseDelay?.seconds, tipRack: args.tipRack, + aspirateXOffset, + aspirateYOffset, + dispenseXOffset, + dispenseYOffset, }) : [] @@ -385,6 +405,8 @@ export const consolidate: CommandCreator = ( well: destinationWell ?? undefined, flowRate: dispenseFlowRateUlSec, offsetFromBottomMm: dispenseOffsetFromBottomMm, + xOffset: dispenseXOffset, + yOffset: dispenseYOffset, }), ] diff --git a/step-generation/src/commandCreators/compound/distribute.ts b/step-generation/src/commandCreators/compound/distribute.ts index 9662a07d959..520ce06aeb4 100644 --- a/step-generation/src/commandCreators/compound/distribute.ts +++ b/step-generation/src/commandCreators/compound/distribute.ts @@ -147,6 +147,10 @@ export const distribute: CommandCreator = ( dispenseFlowRateUlSec, dispenseOffsetFromBottomMm, blowoutLocation, + aspirateXOffset, + aspirateYOffset, + dispenseXOffset, + dispenseYOffset, } = args const aspirateAirGapVolume = args.aspirateAirGapVolume || 0 const dispenseAirGapVolume = args.dispenseAirGapVolume || 0 @@ -211,6 +215,8 @@ export const distribute: CommandCreator = ( flowRate: aspirateFlowRateUlSec, offsetFromBottomMm: airGapOffsetSourceWell, isAirGap: true, + xOffset: 0, + yOffset: 0, tipRack: args.tipRack, }), ...(aspirateDelay != null @@ -232,6 +238,8 @@ export const distribute: CommandCreator = ( flowRate: dispenseFlowRateUlSec, offsetFromBottomMm: airGapOffsetDestWell, isAirGap: true, + xOffset: 0, + yOffset: 0, }), ...(dispenseDelay != null ? [ @@ -290,6 +298,8 @@ export const distribute: CommandCreator = ( well: destWell, flowRate: dispenseFlowRateUlSec, offsetFromBottomMm: dispenseOffsetFromBottomMm, + xOffset: dispenseXOffset, + yOffset: dispenseYOffset, }), ...delayAfterDispenseCommands, ...touchTipAfterDispenseCommand, @@ -337,6 +347,8 @@ export const distribute: CommandCreator = ( offsetFromBottomMm: airGapOffsetDestWell, isAirGap: true, tipRack: args.tipRack, + xOffset: 0, + yOffset: 0, }), ...(aspirateDelay != null ? [ @@ -439,6 +451,10 @@ export const distribute: CommandCreator = ( aspirateDelaySeconds: aspirateDelay?.seconds, dispenseDelaySeconds: dispenseDelay?.seconds, tipRack: args.tipRack, + aspirateXOffset, + aspirateYOffset, + dispenseXOffset, + dispenseYOffset, }) : [] @@ -478,6 +494,8 @@ export const distribute: CommandCreator = ( flowRate: aspirateFlowRateUlSec, offsetFromBottomMm: aspirateOffsetFromBottomMm, tipRack: args.tipRack, + xOffset: aspirateXOffset, + yOffset: aspirateYOffset, }), ...delayAfterAspirateCommands, ...touchTipAfterAspirateCommand, diff --git a/step-generation/src/commandCreators/compound/mix.ts b/step-generation/src/commandCreators/compound/mix.ts index 4a918da5a0d..284529c7c1f 100644 --- a/step-generation/src/commandCreators/compound/mix.ts +++ b/step-generation/src/commandCreators/compound/mix.ts @@ -35,6 +35,10 @@ export function mixUtil(args: { aspirateFlowRateUlSec: number dispenseFlowRateUlSec: number tipRack: string + aspirateXOffset: number + dispenseXOffset: number + aspirateYOffset: number + dispenseYOffset: number aspirateDelaySeconds?: number | null | undefined dispenseDelaySeconds?: number | null | undefined }): CurriedCommandCreator[] { @@ -51,6 +55,10 @@ export function mixUtil(args: { aspirateDelaySeconds, dispenseDelaySeconds, tipRack, + aspirateXOffset, + aspirateYOffset, + dispenseXOffset, + dispenseYOffset, } = args const getDelayCommand = (seconds?: number | null): CurriedCommandCreator[] => @@ -76,6 +84,8 @@ export function mixUtil(args: { offsetFromBottomMm: aspirateOffsetFromBottomMm, flowRate: aspirateFlowRateUlSec, tipRack, + xOffset: aspirateXOffset, + yOffset: aspirateYOffset, }), ...getDelayCommand(aspirateDelaySeconds), curryCommandCreator(dispense, { @@ -85,6 +95,8 @@ export function mixUtil(args: { well, offsetFromBottomMm: dispenseOffsetFromBottomMm, flowRate: dispenseFlowRateUlSec, + xOffset: dispenseXOffset, + yOffset: dispenseYOffset, }), ...getDelayCommand(dispenseDelaySeconds), ], @@ -123,6 +135,10 @@ export const mix: CommandCreator = ( blowoutOffsetFromTopMm, dropTipLocation, tipRack, + aspirateXOffset, + aspirateYOffset, + dispenseXOffset, + dispenseYOffset, } = data const is96Channel = @@ -257,6 +273,10 @@ export const mix: CommandCreator = ( aspirateDelaySeconds, dispenseDelaySeconds, tipRack, + aspirateXOffset, + aspirateYOffset, + dispenseXOffset, + dispenseYOffset, }) return [ ...configureNozzleLayoutCommand, diff --git a/step-generation/src/commandCreators/compound/transfer.ts b/step-generation/src/commandCreators/compound/transfer.ts index d7f4ec5e181..2d16c8064bf 100644 --- a/step-generation/src/commandCreators/compound/transfer.ts +++ b/step-generation/src/commandCreators/compound/transfer.ts @@ -205,6 +205,10 @@ export const transfer: CommandCreator = ( dispenseFlowRateUlSec, dispenseOffsetFromBottomMm, tipRack, + aspirateXOffset, + aspirateYOffset, + dispenseXOffset, + dispenseYOffset, } = args const aspirateAirGapVolume = args.aspirateAirGapVolume || 0 const dispenseAirGapVolume = args.dispenseAirGapVolume || 0 @@ -329,6 +333,10 @@ export const transfer: CommandCreator = ( aspirateDelaySeconds: aspirateDelay?.seconds, dispenseDelaySeconds: dispenseDelay?.seconds, tipRack, + aspirateXOffset, + aspirateYOffset, + dispenseXOffset, + dispenseYOffset, }) : [] const mixBeforeAspirateCommands = @@ -346,6 +354,10 @@ export const transfer: CommandCreator = ( aspirateDelaySeconds: aspirateDelay?.seconds, dispenseDelaySeconds: dispenseDelay?.seconds, tipRack, + aspirateXOffset, + aspirateYOffset, + dispenseXOffset, + dispenseYOffset, }) : [] const delayAfterAspirateCommands = @@ -410,6 +422,10 @@ export const transfer: CommandCreator = ( aspirateDelaySeconds: aspirateDelay?.seconds, dispenseDelaySeconds: dispenseDelay?.seconds, tipRack, + aspirateXOffset, + aspirateYOffset, + dispenseXOffset, + dispenseYOffset, }) : [] @@ -425,6 +441,8 @@ export const transfer: CommandCreator = ( offsetFromBottomMm: airGapOffsetSourceWell, isAirGap: true, tipRack, + xOffset: 0, + yOffset: 0, }), ...(aspirateDelay != null ? [ @@ -445,6 +463,8 @@ export const transfer: CommandCreator = ( flowRate: dispenseFlowRateUlSec, offsetFromBottomMm: airGapOffsetDestWell, isAirGap: true, + xOffset: 0, + yOffset: 0, }), ...(dispenseDelay != null ? [ @@ -486,6 +506,8 @@ export const transfer: CommandCreator = ( flowRate: aspirateFlowRateUlSec, offsetFromBottomMm: aspirateOffsetFromBottomMm, tipRack, + xOffset: aspirateXOffset, + yOffset: aspirateYOffset, }), ] const dispenseCommand = [ @@ -496,6 +518,8 @@ export const transfer: CommandCreator = ( well: destinationWell ?? undefined, flowRate: dispenseFlowRateUlSec, offsetFromBottomMm: dispenseOffsetFromBottomMm, + xOffset: dispenseXOffset, + yOffset: dispenseYOffset, }), ] diff --git a/step-generation/src/fixtures/commandFixtures.ts b/step-generation/src/fixtures/commandFixtures.ts index 2c38a361ee7..3d1ee394574 100644 --- a/step-generation/src/fixtures/commandFixtures.ts +++ b/step-generation/src/fixtures/commandFixtures.ts @@ -129,6 +129,8 @@ export const makeAspirateHelper: MakeAspDispHelper = bakedP wellLocation: { origin: 'bottom', offset: { + y: 0, + x: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -199,6 +201,8 @@ const _defaultDispenseParams = { wellLocation: { origin: 'bottom' as const, offset: { + y: 0, + x: 0, z: DISPENSE_OFFSET_FROM_BOTTOM_MM, }, }, diff --git a/step-generation/src/types.ts b/step-generation/src/types.ts index 98e1e8ec90c..6cef80c43ed 100644 --- a/step-generation/src/types.ts +++ b/step-generation/src/types.ts @@ -192,6 +192,10 @@ export type SharedTransferLikeArgs = CommonArgs & { aspirateFlowRateUlSec: number /** offset from bottom of well in mm */ aspirateOffsetFromBottomMm: number + /** x offset mm */ + aspirateXOffset: number + /** y offset mm */ + aspirateYOffset: number // ===== DISPENSE SETTINGS ===== /** Air gap after dispense */ @@ -206,6 +210,10 @@ export type SharedTransferLikeArgs = CommonArgs & { dispenseFlowRateUlSec: number /** offset from bottom of well in mm */ dispenseOffsetFromBottomMm: number + /** x offset mm */ + dispenseXOffset: number + /** y offset mm */ + dispenseYOffset: number } export type ConsolidateArgs = SharedTransferLikeArgs & { @@ -286,6 +294,12 @@ export type MixArgs = CommonArgs & { /** offset from bottom of well in mm */ aspirateOffsetFromBottomMm: number dispenseOffsetFromBottomMm: number + /** x offset */ + aspirateXOffset: number + dispenseXOffset: number + /** y offset */ + aspirateYOffset: number + dispenseYOffset: number /** flow rates in uL/sec */ aspirateFlowRateUlSec: number dispenseFlowRateUlSec: number diff --git a/step-generation/src/utils/misc.ts b/step-generation/src/utils/misc.ts index c9f36587213..58bf2e9f782 100644 --- a/step-generation/src/utils/misc.ts +++ b/step-generation/src/utils/misc.ts @@ -479,6 +479,8 @@ interface DispenseLocationHelperArgs { pipetteId: string volume: number flowRate: number + xOffset: number + yOffset: number offsetFromBottomMm?: number well?: string } @@ -494,6 +496,8 @@ export const dispenseLocationHelper: CommandCreator flowRate, offsetFromBottomMm, well, + xOffset, + yOffset, } = args const trashOrLabware = getTrashOrLabware( @@ -516,6 +520,8 @@ export const dispenseLocationHelper: CommandCreator well, flowRate, offsetFromBottomMm, + xOffset, + yOffset, }), ] } else if (trashOrLabware === 'wasteChute') { @@ -660,6 +666,8 @@ export const airGapHelper: CommandCreator = ( offsetFromBottomMm, isAirGap: true, tipRack, + xOffset: 0, + yOffset: 0, }), ] // when aspirating out of multi wells for consolidate @@ -674,6 +682,9 @@ export const airGapHelper: CommandCreator = ( offsetFromBottomMm, isAirGap: true, tipRack, + // NOTE: airgap aspirates happen at default x/y offset + xOffset: 0, + yOffset: 0, }), ] } From 6bf0579bfd65d1408a34d3441e9128a20307bb80 Mon Sep 17 00:00:00 2001 From: Jeremy Leon Date: Wed, 3 Apr 2024 15:18:54 -0400 Subject: [PATCH 031/194] feat(robot-server): update run creation endpoint to accept runtime parameter values (#14776) Adds an optional argument `runTimeParameterValues` to the request body of the POST /runs endpoint to start a run with new runtime parameter values. --- .../protocols/parameters/validation.py | 19 +++++++++++-------- .../protocols/parameters/test_validation.py | 5 ++++- .../robot_server/runs/engine_store.py | 12 +++++++++--- .../robot_server/runs/router/base_router.py | 4 ++++ .../robot_server/runs/run_data_manager.py | 6 ++++++ robot-server/robot_server/runs/run_models.py | 5 +++++ .../tests/runs/router/test_base_router.py | 9 ++++++++- .../tests/runs/test_run_data_manager.py | 10 +++++++++- 8 files changed, 56 insertions(+), 14 deletions(-) diff --git a/api/src/opentrons/protocols/parameters/validation.py b/api/src/opentrons/protocols/parameters/validation.py index cbb2464ebd0..6e5c3b78a9f 100644 --- a/api/src/opentrons/protocols/parameters/validation.py +++ b/api/src/opentrons/protocols/parameters/validation.py @@ -61,14 +61,17 @@ def ensure_value_type( This does not guarantee that the value will be the correct type for the given parameter, only that any data coming in is in the format that we expect. For now, the only transformation it is doing is converting integers represented - as floating points to integers. If something is labelled as an int but is not actually an integer, that will be - caught when it is attempted to be set as the parameter value and will raise the appropriate error there. + as floating points to integers, and bools represented as 1.0/0.0 to True/False. + + If something is labelled as a type but does not get converted here, that will be caught when it is attempted to be + set as the parameter value and will raise the appropriate error there. """ - validated_value: AllowedTypes - if isinstance(value, float) and parameter_type is int and value.is_integer(): - validated_value = int(value) - else: - validated_value = value + validated_value: AllowedTypes = value + if isinstance(value, float): + if parameter_type is bool and (value == 0 or value == 1): + validated_value = bool(value) + elif parameter_type is int and value.is_integer(): + validated_value = int(value) return validated_value @@ -163,7 +166,7 @@ def validate_type(value: ParamType, parameter_type: type) -> None: """Validate parameter value is the correct type.""" if not isinstance(value, parameter_type): raise ParameterValueError( - f"Parameter value has type {type(value)} must match type {parameter_type}." + f"Parameter value {value} has type {type(value)}, must match type {parameter_type}." ) diff --git a/api/tests/opentrons/protocols/parameters/test_validation.py b/api/tests/opentrons/protocols/parameters/test_validation.py index 988e203a822..f515da885ed 100644 --- a/api/tests/opentrons/protocols/parameters/test_validation.py +++ b/api/tests/opentrons/protocols/parameters/test_validation.py @@ -137,13 +137,16 @@ def test_validate_options_raises_name_error() -> None: (2.0, float, 2.0), (2.2, float, 2.2), ("3.0", str, "3.0"), + (0.0, bool, False), + (1, bool, True), + (3.0, bool, 3.0), (True, bool, True), ], ) def test_ensure_value_type( value: Union[float, bool, str], param_type: type, result: AllowedTypes ) -> None: - """It should ensure the correct type is there, converting floats to ints.""" + """It should ensure that if applicable, the value is coerced into the expected type""" assert result == subject.ensure_value_type(value, param_type) diff --git a/robot-server/robot_server/runs/engine_store.py b/robot-server/robot_server/runs/engine_store.py index 673ff5549f3..8a35c20d92f 100644 --- a/robot-server/robot_server/runs/engine_store.py +++ b/robot-server/robot_server/runs/engine_store.py @@ -32,7 +32,10 @@ ) from robot_server.protocols.protocol_store import ProtocolResource -from opentrons.protocol_engine.types import DeckConfigurationType +from opentrons.protocol_engine.types import ( + DeckConfigurationType, + RunTimeParamValuesType, +) class EngineConflictError(RuntimeError): @@ -154,14 +157,17 @@ async def create( deck_configuration: DeckConfigurationType, notify_publishers: Callable[[], None], protocol: Optional[ProtocolResource], + run_time_param_values: Optional[RunTimeParamValuesType] = None, ) -> StateSummary: """Create and store a ProtocolRunner and ProtocolEngine for a given Run. Args: run_id: The run resource the engine is assigned to. labware_offsets: Labware offsets to create the engine with. - protocol: The protocol to load the runner with, if any. + deck_configuration: A mapping of fixtures to cutout fixtures the deck will be loaded with. notify_publishers: Utilized by the engine to notify publishers of state changes. + protocol: The protocol to load the runner with, if any. + run_time_param_values: Any runtime parameter values to set. Returns: The initial equipment and status summary of the engine. @@ -217,7 +223,7 @@ async def create( # was uploaded before we added stricter validation, and that # doesn't conform to the new rules. python_parse_mode=PythonParseMode.ALLOW_LEGACY_METADATA_AND_REQUIREMENTS, - run_time_param_values=None, + run_time_param_values=run_time_param_values, ) elif isinstance(runner, JsonRunner): assert ( diff --git a/robot-server/robot_server/runs/router/base_router.py b/robot-server/robot_server/runs/router/base_router.py index e1e62fdf0d4..728966823fb 100644 --- a/robot-server/robot_server/runs/router/base_router.py +++ b/robot-server/robot_server/runs/router/base_router.py @@ -162,6 +162,9 @@ async def create_run( """ protocol_id = request_body.data.protocolId if request_body is not None else None offsets = request_body.data.labwareOffsets if request_body is not None else [] + rtp_values = ( + request_body.data.runTimeParameterValues if request_body is not None else None + ) protocol_resource = None deck_configuration = await deck_configuration_store.get_deck_configuration() @@ -185,6 +188,7 @@ async def create_run( created_at=created_at, labware_offsets=offsets, deck_configuration=deck_configuration, + run_time_param_values=rtp_values, protocol=protocol_resource, notify_publishers=notify_publishers, ) diff --git a/robot-server/robot_server/runs/run_data_manager.py b/robot-server/robot_server/runs/run_data_manager.py index f0fc28dca37..5c57a14ecda 100644 --- a/robot-server/robot_server/runs/run_data_manager.py +++ b/robot-server/robot_server/runs/run_data_manager.py @@ -12,6 +12,7 @@ CurrentCommand, Command, ) +from opentrons.protocol_engine.types import RunTimeParamValuesType from robot_server.protocols.protocol_store import ProtocolResource from robot_server.service.task_runner import TaskRunner @@ -142,6 +143,7 @@ async def create( created_at: datetime, labware_offsets: List[LabwareOffsetCreate], deck_configuration: DeckConfigurationType, + run_time_param_values: Optional[RunTimeParamValuesType], notify_publishers: Callable[[], None], protocol: Optional[ProtocolResource], ) -> Union[Run, BadRun]: @@ -151,7 +153,10 @@ async def create( run_id: Identifier to assign the new run. created_at: Creation datetime. labware_offsets: Labware offsets to initialize the engine with. + deck_configuration: A mapping of fixtures to cutout fixtures the deck will be loaded with. notify_publishers: Utilized by the engine to notify publishers of state changes. + run_time_param_values: Any runtime parameter values to set. + protocol: The protocol to load the runner with, if any. Returns: The run resource. @@ -173,6 +178,7 @@ async def create( labware_offsets=labware_offsets, deck_configuration=deck_configuration, protocol=protocol, + run_time_param_values=run_time_param_values, notify_publishers=notify_publishers, ) run_resource = self._run_store.insert( diff --git a/robot-server/robot_server/runs/run_models.py b/robot-server/robot_server/runs/run_models.py index e05cd25330c..7da6e0b0a5d 100644 --- a/robot-server/robot_server/runs/run_models.py +++ b/robot-server/robot_server/runs/run_models.py @@ -18,6 +18,7 @@ Liquid, CommandNote, ) +from opentrons.protocol_engine.types import RunTimeParamValuesType from opentrons_shared_data.errors import GeneralError from robot_server.service.json_api import ResourceModel from robot_server.errors.error_responses import ErrorDetails @@ -212,6 +213,10 @@ class RunCreate(BaseModel): default_factory=list, description="Labware offsets to apply as labware are loaded.", ) + runTimeParameterValues: Optional[RunTimeParamValuesType] = Field( + None, + description="Key-value pairs of run-time parameters defined in a protocol.", + ) class RunUpdate(BaseModel): diff --git a/robot-server/tests/runs/router/test_base_router.py b/robot-server/tests/runs/router/test_base_router.py index 5c772e14be7..5763935cc39 100644 --- a/robot-server/tests/runs/router/test_base_router.py +++ b/robot-server/tests/runs/router/test_base_router.py @@ -92,6 +92,7 @@ async def test_create_run( labware_offsets=[labware_offset_create], deck_configuration=[], protocol=None, + run_time_param_values=None, notify_publishers=mock_notify_publishers, ) ).then_return(expected_response) @@ -169,12 +170,17 @@ async def test_create_protocol_run( labware_offsets=[], deck_configuration=[], protocol=protocol_resource, + run_time_param_values={"foo": "bar"}, notify_publishers=mock_notify_publishers, ) ).then_return(expected_response) result = await create_run( - request_body=RequestModel(data=RunCreate(protocolId="protocol-id")), + request_body=RequestModel( + data=RunCreate( + protocolId="protocol-id", runTimeParameterValues={"foo": "bar"} + ) + ), protocol_store=mock_protocol_store, run_data_manager=mock_run_data_manager, run_id=run_id, @@ -232,6 +238,7 @@ async def test_create_run_conflict( labware_offsets=[], deck_configuration=[], protocol=None, + run_time_param_values=None, notify_publishers=mock_notify_publishers, ) ).then_raise(EngineConflictError("oh no")) diff --git a/robot-server/tests/runs/test_run_data_manager.py b/robot-server/tests/runs/test_run_data_manager.py index bac302e3065..ba4ceec8799 100644 --- a/robot-server/tests/runs/test_run_data_manager.py +++ b/robot-server/tests/runs/test_run_data_manager.py @@ -143,6 +143,7 @@ async def test_create( labware_offsets=[], protocol=None, deck_configuration=[], + run_time_param_values=None, notify_publishers=mock_notify_publishers, ) ).then_return(engine_state_summary) @@ -160,6 +161,7 @@ async def test_create( labware_offsets=[], protocol=None, deck_configuration=[], + run_time_param_values=None, notify_publishers=mock_notify_publishers, ) @@ -187,7 +189,7 @@ async def test_create_with_options( engine_state_summary: StateSummary, run_resource: RunResource, ) -> None: - """It should handle creation with a protocol and labware offsets.""" + """It should handle creation with a protocol, labware offsets and parameters.""" run_id = "hello world" created_at = datetime(year=2021, month=1, day=1) @@ -210,6 +212,7 @@ async def test_create_with_options( labware_offsets=[labware_offset], protocol=protocol, deck_configuration=[], + run_time_param_values={"foo": "bar"}, notify_publishers=mock_notify_publishers, ) ).then_return(engine_state_summary) @@ -228,6 +231,7 @@ async def test_create_with_options( labware_offsets=[labware_offset], protocol=protocol, deck_configuration=[], + run_time_param_values={"foo": "bar"}, notify_publishers=mock_notify_publishers, ) @@ -263,6 +267,7 @@ async def test_create_engine_error( labware_offsets=[], protocol=None, deck_configuration=[], + run_time_param_values=None, notify_publishers=mock_notify_publishers, ) ).then_raise(EngineConflictError("oh no")) @@ -274,6 +279,7 @@ async def test_create_engine_error( labware_offsets=[], protocol=None, deck_configuration=[], + run_time_param_values=None, notify_publishers=mock_notify_publishers, ) @@ -651,6 +657,7 @@ async def test_create_archives_existing( labware_offsets=[], protocol=None, deck_configuration=[], + run_time_param_values=None, notify_publishers=mock_notify_publishers, ) ).then_return(engine_state_summary) @@ -669,6 +676,7 @@ async def test_create_archives_existing( labware_offsets=[], protocol=None, deck_configuration=[], + run_time_param_values=None, notify_publishers=mock_notify_publishers, ) From 0f7d1ff49cf0f026cc693947e096445e29744840 Mon Sep 17 00:00:00 2001 From: Nick Diehl <47604184+ncdiehl11@users.noreply.github.com> Date: Wed, 3 Apr 2024 15:46:24 -0400 Subject: [PATCH 032/194] chore(app, api-client): remove mock RTP datafrom ProtocolDetails (#14791) closes AUTH-266 --- .../__fixtures__/simpleAnalysisFile.json | 54 ++++++- .../index.tsx | 58 +------ app/src/organisms/ProtocolDetails/index.tsx | 8 +- app/src/pages/Protocols/hooks/index.ts | 145 +----------------- 4 files changed, 60 insertions(+), 205 deletions(-) diff --git a/api-client/src/protocols/__fixtures__/simpleAnalysisFile.json b/api-client/src/protocols/__fixtures__/simpleAnalysisFile.json index e6f0a5bba3b..74faa60fcb6 100644 --- a/api-client/src/protocols/__fixtures__/simpleAnalysisFile.json +++ b/api-client/src/protocols/__fixtures__/simpleAnalysisFile.json @@ -3937,5 +3937,57 @@ "displayColor": "#b925ff" } ], - "runTimeParameters": [] + "runTimeParameters": [ + { + "type": "int", + "displayName": "number of samples", + "variableName": "num_samples", + "description": "How many samples do you want to run?", + "value": 96, + "min": 1, + "max": 96, + "default": 96 + }, + { + "type": "float", + "displayName": "samples volume", + "variableName": "vol_sample", + "description": "What sample volume are you using?", + "value": 10.0, + "min": 1, + "max": 20.0, + "default": 10.0 + }, + { + "displayName": "Additional mix for reagent 2?", + "variableName": "extra_mix", + "description": "When on, we do an extra mix for reagent 2.", + "type": "bool", + "default": false, + "value": false + }, + { + "displayName": "Number of PCR Cycles", + "variableName": "real_mode", + "description": "Cycle map", + "type": "int", + "unit": "cycles", + "default": 15, + "value": 15, + "choices": [ + { + "displayName": "1 & 10ng (15 cycles)", + "value": 15 + }, + { + "displayName": "100ng (15 cycles)", + "value": 15 + }, + { + "displayName": "1ug (10 cycles)", + "value": 10 + } + ] + } + ] } diff --git a/app/src/organisms/ChooseRobotToRunProtocolSlideout/index.tsx b/app/src/organisms/ChooseRobotToRunProtocolSlideout/index.tsx index 09b7f4e0fe8..39cf498b0e5 100644 --- a/app/src/organisms/ChooseRobotToRunProtocolSlideout/index.tsx +++ b/app/src/organisms/ChooseRobotToRunProtocolSlideout/index.tsx @@ -62,62 +62,8 @@ export function ChooseRobotToRunProtocolSlideoutComponent( ) // TODO: (nd: 3/20/24) remove stubs and pull parameters from analysis - // const runTimeParameters = - // storedProtocolData.mostRecentAnalysis?.runTimeParameters ?? [] - const mockRunTimeParameters: RunTimeParameter[] = [ - { - displayName: 'Dry Run', - value: false, - variableName: 'DRYRUN', - description: 'Is this a dry or wet run? Wet is true, dry is false', - type: 'bool', - default: false, - }, - { - value: 4, - displayName: 'Columns of Samples', - variableName: 'COLUMNS', - description: 'How many columns do you want?', - type: 'int', - min: 1, - max: 14, - default: 4, - }, - { - value: 6.5, - displayName: 'EtoH Volume', - variableName: 'ETOH_VOLUME', - description: '70% ethanol volume', - type: 'float', - suffix: 'mL', - min: 1.5, - max: 10.0, - default: 6.5, - }, - { - value: 'none', - displayName: 'Default Module Offsets', - variableName: 'DEFAULT_OFFSETS', - description: 'default module offsets for temp, H-S, and none', - type: 'str', - choices: [ - { - displayName: 'No offsets', - value: 'none', - }, - { - displayName: 'temp offset', - value: '1', - }, - { - displayName: 'heater-shaker offset', - value: '2', - }, - ], - default: 'none', - }, - ] - const runTimeParameters: RunTimeParameter[] = mockRunTimeParameters + const runTimeParameters = + storedProtocolData.mostRecentAnalysis?.runTimeParameters ?? [] const [ runTimeParametersOverrides, setRunTimeParametersOverrides, diff --git a/app/src/organisms/ProtocolDetails/index.tsx b/app/src/organisms/ProtocolDetails/index.tsx index 02d897c3b4e..90add9f023a 100644 --- a/app/src/organisms/ProtocolDetails/index.tsx +++ b/app/src/organisms/ProtocolDetails/index.tsx @@ -73,7 +73,6 @@ import { ProtocolLabwareDetails } from './ProtocolLabwareDetails' import { ProtocolLiquidsDetails } from './ProtocolLiquidsDetails' import { RobotConfigurationDetails } from './RobotConfigurationDetails' import { ProtocolParameters } from './ProtocolParameters' -import { useRunTimeParameters } from '../../pages/Protocols/hooks' import type { JsonConfig, PythonConfig } from '@opentrons/shared-data' import type { StoredProtocolData } from '../../redux/protocol-storage' @@ -201,9 +200,12 @@ export function ProtocolDetails( const { t, i18n } = useTranslation(['protocol_details', 'shared']) const enableProtocolStats = useFeatureFlag('protocolStats') const enableRunTimeParameters = useFeatureFlag('enableRunTimeParameters') + const runTimeParameters = mostRecentAnalysis?.runTimeParameters ?? [] + const hasRunTimeParameters = + enableRunTimeParameters && runTimeParameters.length > 0 const [currentTab, setCurrentTab] = React.useState< 'robot_config' | 'labware' | 'liquids' | 'stats' | 'parameters' - >('robot_config') + >(hasRunTimeParameters ? 'parameters' : 'robot_config') const [ showChooseRobotToRunProtocolSlideout, setShowChooseRobotToRunProtocolSlideout, @@ -218,8 +220,6 @@ export function ProtocolDetails( getIsProtocolAnalysisInProgress(state, protocolKey) ) - const runTimeParameters = useRunTimeParameters(protocolKey) - const analysisStatus = getAnalysisStatus(isAnalyzing, mostRecentAnalysis) if (analysisStatus === 'stale') { diff --git a/app/src/pages/Protocols/hooks/index.ts b/app/src/pages/Protocols/hooks/index.ts index c873ff35a9f..964103dc5c5 100644 --- a/app/src/pages/Protocols/hooks/index.ts +++ b/app/src/pages/Protocols/hooks/index.ts @@ -200,150 +200,7 @@ export const useRunTimeParameters = ( { enabled: protocolData != null } ) - const mockData: RunTimeParameter[] = [ - { - value: false, - displayName: 'Dry Run', - variableName: 'DRYRUN', - description: 'Is this a dry or wet run? Wet is true, dry is false', - type: 'bool', - default: false, - }, - { - value: true, - displayName: 'Use Gripper', - variableName: 'USE_GRIPPER', - description: 'For using the gripper.', - type: 'bool', - default: true, - }, - { - value: true, - displayName: 'Trash Tips', - variableName: 'TIP_TRASH', - description: - 'to throw tip into the trash or to not throw tip into the trash', - type: 'bool', - default: true, - }, - { - value: true, - displayName: 'Deactivate Temperatures', - variableName: 'DEACTIVATE_TEMP', - description: 'deactivate temperature on the module', - type: 'bool', - default: true, - }, - { - value: 4, - displayName: 'Columns of Samples', - variableName: 'COLUMNS', - description: 'How many columns do you want?', - type: 'int', - min: 1, - max: 14, - default: 4, - }, - { - value: 6, - displayName: 'PCR Cycles', - variableName: 'PCR_CYCLES', - description: 'number of PCR cycles on a thermocycler', - type: 'int', - min: 1, - max: 10, - default: 6, - }, - { - value: 6.5, - displayName: 'EtoH Volume', - variableName: 'ETOH_VOLUME', - description: '70% ethanol volume', - type: 'float', - suffix: 'mL', - min: 1.5, - max: 10.0, - default: 6.5, - }, - { - value: 'none', - displayName: 'Default Module Offsets', - variableName: 'DEFAULT_OFFSETS', - description: 'default module offsets for temp, H-S, and none', - type: 'str', - choices: [ - { - displayName: 'No offsets', - value: 'none', - }, - { - displayName: 'temp offset', - value: '1', - }, - { - displayName: 'heater-shaker offset', - value: '2', - }, - ], - default: 'none', - }, - { - value: 'left', - displayName: 'pipette mount', - variableName: 'mont', - description: 'pipette mount', - type: 'str', - choices: [ - { - displayName: 'Left', - value: 'left', - }, - { - displayName: 'Right', - value: 'right', - }, - ], - default: 'left', - }, - { - value: 'flex', - displayName: 'short test case', - variableName: 'short 2 options', - description: 'this play 2 short options', - type: 'str', - choices: [ - { - displayName: 'OT-2', - value: 'ot2', - }, - { - displayName: 'Flex', - value: 'flex', - }, - ], - default: 'flex', - }, - { - value: 'flex', - displayName: 'long test case', - variableName: 'long 2 options', - description: 'this play 2 long options', - type: 'str', - choices: [ - { - displayName: 'I am kind of long text version', - value: 'ot2', - }, - { - displayName: 'I am kind of long text version. Today is 3/15', - value: 'flex', - }, - ], - default: 'flex', - }, - ] - // TODO(jr, 3/14/24): remove the mockData - return analysis?.runTimeParameters ?? mockData + return analysis?.runTimeParameters ?? [] } /** From 3d34031d01ccab67b42f839ef1b898c814756068 Mon Sep 17 00:00:00 2001 From: Sanniti Pimpley Date: Wed, 3 Apr 2024 17:55:07 -0400 Subject: [PATCH 033/194] fix(robot-server): maintain correct order of protocol analyses (#14762) Closes AUTH-229 # Overview Updates the `/protocols` endpoints to always maintain the order of list of analyses as most-recently-started-analysis last, making sure to verify if a new analysis needs to be triggered because of new run-time-parameter values for a previously uploaded protocol. # Risk assessment Medium. Does database update and fixes the analysis order that was broken by #14688 --------- Co-authored-by: Max Marrone --- api/src/opentrons/protocol_engine/types.py | 1 + .../persistence/_migrations/v3_to_v4.py | 52 +++ .../persistence/persistence_directory.py | 7 +- .../persistence/tables/__init__.py | 2 +- .../persistence/tables/schema_4.py | 130 +++++++ .../robot_server/protocols/analysis_models.py | 9 +- .../robot_server/protocols/analysis_store.py | 103 ++++- .../protocols/completed_analysis_store.py | 82 +++- robot-server/robot_server/protocols/router.py | 64 +++- .../http_api/persistence/test_reset.py | 6 +- .../protocols/test_analyses.tavern.yaml | 23 -- ...lyses_with_run_time_parameters.tavern.yaml | 180 +++++++++ .../http_api/protocols/test_key.tavern.yaml | 2 + .../http_api/protocols/test_persistence.py | 4 +- ...basic_transfer_with_run_time_parameters.py | 57 +++ robot-server/tests/persistence/test_tables.py | 69 +++- .../tests/protocols/test_analysis_store.py | 203 +++++++++- .../test_completed_analysis_store.py | 51 ++- .../tests/protocols/test_protocols_router.py | 359 +++++++++++++++++- 19 files changed, 1331 insertions(+), 73 deletions(-) create mode 100644 robot-server/robot_server/persistence/_migrations/v3_to_v4.py create mode 100644 robot-server/robot_server/persistence/tables/schema_4.py create mode 100644 robot-server/tests/integration/http_api/protocols/test_analyses_with_run_time_parameters.tavern.yaml create mode 100644 robot-server/tests/integration/protocols/basic_transfer_with_run_time_parameters.py diff --git a/api/src/opentrons/protocol_engine/types.py b/api/src/opentrons/protocol_engine/types.py index 266dc6aa81f..3d833a65042 100644 --- a/api/src/opentrons/protocol_engine/types.py +++ b/api/src/opentrons/protocol_engine/types.py @@ -847,6 +847,7 @@ def from_hw_state(cls, state: HwTipStateType) -> "TipPresenceStatus": }[state] +# TODO (spp, 2024-04-02): move all RTP types to runner class RTPBase(BaseModel): """Parameters defined in a protocol.""" diff --git a/robot-server/robot_server/persistence/_migrations/v3_to_v4.py b/robot-server/robot_server/persistence/_migrations/v3_to_v4.py new file mode 100644 index 00000000000..8b4445aaec3 --- /dev/null +++ b/robot-server/robot_server/persistence/_migrations/v3_to_v4.py @@ -0,0 +1,52 @@ +"""Migrate the persistence directory from schema 3 to 4. + +Summary of changes from schema 3: + +- Adds a new "run_time_parameter_values_and_defaults" column to analysis table +""" + +from pathlib import Path +from contextlib import ExitStack +import shutil +from typing import Any + +import sqlalchemy + +from ..database import sql_engine_ctx +from ..tables import schema_4 +from .._folder_migrator import Migration + +_DB_FILE = "robot_server.db" + + +class Migration3to4(Migration): # noqa: D101 + def migrate(self, source_dir: Path, dest_dir: Path) -> None: + """Migrate the persistence directory from schema 3 to 4.""" + # Copy over all existing directories and files to new version + for item in source_dir.iterdir(): + if item.is_dir(): + shutil.copytree(src=item, dst=dest_dir / item.name) + else: + shutil.copy(src=item, dst=dest_dir / item.name) + dest_db_file = dest_dir / _DB_FILE + + # Append the new column to existing analyses in v4 database + with ExitStack() as exit_stack: + dest_engine = exit_stack.enter_context(sql_engine_ctx(dest_db_file)) + schema_4.metadata.create_all(dest_engine) + + def add_column( + engine: sqlalchemy.engine.Engine, + table_name: str, + column: Any, + ) -> None: + column_type = column.type.compile(engine.dialect) + engine.execute( + f"ALTER TABLE {table_name} ADD COLUMN {column.key} {column_type}" + ) + + add_column( + dest_engine, + schema_4.analysis_table.name, + schema_4.analysis_table.c.run_time_parameter_values_and_defaults, + ) diff --git a/robot-server/robot_server/persistence/persistence_directory.py b/robot-server/robot_server/persistence/persistence_directory.py index 666d5c7998f..b7982b38555 100644 --- a/robot-server/robot_server/persistence/persistence_directory.py +++ b/robot-server/robot_server/persistence/persistence_directory.py @@ -11,7 +11,7 @@ from anyio import Path as AsyncPath, to_thread from ._folder_migrator import MigrationOrchestrator -from ._migrations import up_to_3 +from ._migrations import up_to_3, v3_to_v4 _TEMP_PERSISTENCE_DIR_PREFIX: Final = "opentrons-robot-server-" @@ -48,7 +48,10 @@ async def prepare_active_subdirectory(prepared_root: Path) -> Path: """Return the active persistence subdirectory after preparing it, if necessary.""" migration_orchestrator = MigrationOrchestrator( root=prepared_root, - migrations=[up_to_3.MigrationUpTo3(subdirectory="3")], + migrations=[ + up_to_3.MigrationUpTo3(subdirectory="3"), + v3_to_v4.Migration3to4(subdirectory="4"), + ], temp_file_prefix="temp-", ) diff --git a/robot-server/robot_server/persistence/tables/__init__.py b/robot-server/robot_server/persistence/tables/__init__.py index 97262e73fab..0aaf869fb35 100644 --- a/robot-server/robot_server/persistence/tables/__init__.py +++ b/robot-server/robot_server/persistence/tables/__init__.py @@ -1,7 +1,7 @@ """SQL database schemas.""" # Re-export the latest schema. -from .schema_3 import ( +from .schema_4 import ( metadata, protocol_table, analysis_table, diff --git a/robot-server/robot_server/persistence/tables/schema_4.py b/robot-server/robot_server/persistence/tables/schema_4.py new file mode 100644 index 00000000000..47d29d3d8f3 --- /dev/null +++ b/robot-server/robot_server/persistence/tables/schema_4.py @@ -0,0 +1,130 @@ +"""v4 of our SQLite schema.""" + +import sqlalchemy + +from robot_server.persistence._utc_datetime import UTCDateTime + +metadata = sqlalchemy.MetaData() + +protocol_table = sqlalchemy.Table( + "protocol", + metadata, + sqlalchemy.Column( + "id", + sqlalchemy.String, + primary_key=True, + ), + sqlalchemy.Column( + "created_at", + UTCDateTime, + nullable=False, + ), + sqlalchemy.Column("protocol_key", sqlalchemy.String, nullable=True), +) + +analysis_table = sqlalchemy.Table( + "analysis", + metadata, + sqlalchemy.Column( + "id", + sqlalchemy.String, + primary_key=True, + ), + sqlalchemy.Column( + "protocol_id", + sqlalchemy.String, + sqlalchemy.ForeignKey("protocol.id"), + index=True, + nullable=False, + ), + sqlalchemy.Column( + "analyzer_version", + sqlalchemy.String, + nullable=False, + ), + sqlalchemy.Column( + "completed_analysis", + # Stores a JSON string. See CompletedAnalysisStore. + sqlalchemy.String, + nullable=False, + ), + # column added in schema v4 + sqlalchemy.Column( + "run_time_parameter_values_and_defaults", + sqlalchemy.String, + nullable=True, + ), +) + +run_table = sqlalchemy.Table( + "run", + metadata, + sqlalchemy.Column( + "id", + sqlalchemy.String, + primary_key=True, + ), + sqlalchemy.Column( + "created_at", + UTCDateTime, + nullable=False, + ), + sqlalchemy.Column( + "protocol_id", + sqlalchemy.String, + sqlalchemy.ForeignKey("protocol.id"), + nullable=True, + ), + # column added in schema v1 + sqlalchemy.Column( + "state_summary", + sqlalchemy.String, + nullable=True, + ), + # column added in schema v1 + sqlalchemy.Column("engine_status", sqlalchemy.String, nullable=True), + # column added in schema v1 + sqlalchemy.Column("_updated_at", UTCDateTime, nullable=True), +) + +action_table = sqlalchemy.Table( + "action", + metadata, + sqlalchemy.Column( + "id", + sqlalchemy.String, + primary_key=True, + ), + sqlalchemy.Column("created_at", UTCDateTime, nullable=False), + sqlalchemy.Column("action_type", sqlalchemy.String, nullable=False), + sqlalchemy.Column( + "run_id", + sqlalchemy.String, + sqlalchemy.ForeignKey("run.id"), + nullable=False, + ), +) + +run_command_table = sqlalchemy.Table( + "run_command", + metadata, + sqlalchemy.Column("row_id", sqlalchemy.Integer, primary_key=True), + sqlalchemy.Column( + "run_id", sqlalchemy.String, sqlalchemy.ForeignKey("run.id"), nullable=False + ), + sqlalchemy.Column("index_in_run", sqlalchemy.Integer, nullable=False), + sqlalchemy.Column("command_id", sqlalchemy.String, nullable=False), + sqlalchemy.Column("command", sqlalchemy.String, nullable=False), + sqlalchemy.Index( + "ix_run_run_id_command_id", # An arbitrary name for the index. + "run_id", + "command_id", + unique=True, + ), + sqlalchemy.Index( + "ix_run_run_id_index_in_run", # An arbitrary name for the index. + "run_id", + "index_in_run", + unique=True, + ), +) diff --git a/robot-server/robot_server/protocols/analysis_models.py b/robot-server/robot_server/protocols/analysis_models.py index 0a3c64c9db0..c5827e577da 100644 --- a/robot-server/robot_server/protocols/analysis_models.py +++ b/robot-server/robot_server/protocols/analysis_models.py @@ -5,7 +5,7 @@ from opentrons.protocol_engine.types import RunTimeParameter from opentrons_shared_data.robot.dev_types import RobotType from pydantic import BaseModel, Field -from typing import List, Optional, Union +from typing import List, Optional, Union, NamedTuple from typing_extensions import Literal from opentrons.protocol_engine import ( @@ -150,4 +150,11 @@ class CompletedAnalysis(BaseModel): ) +class RunTimeParameterAnalysisData(NamedTuple): + """Data from analysis of a run-time parameter.""" + + value: Union[float, bool, str] + default: Union[float, bool, str] + + ProtocolAnalysis = Union[PendingAnalysis, CompletedAnalysis] diff --git a/robot-server/robot_server/protocols/analysis_store.py b/robot-server/robot_server/protocols/analysis_store.py index d8ce780f98d..b0ea474ec07 100644 --- a/robot-server/robot_server/protocols/analysis_store.py +++ b/robot-server/robot_server/protocols/analysis_store.py @@ -19,6 +19,7 @@ LoadedModule, Liquid, ) +from opentrons.protocol_engine.types import RunTimeParamValuesType from .analysis_models import ( AnalysisSummary, @@ -27,6 +28,7 @@ CompletedAnalysis, AnalysisResult, AnalysisStatus, + RunTimeParameterAnalysisData, ) from .completed_analysis_store import CompletedAnalysisStore, CompletedAnalysisResource @@ -71,6 +73,14 @@ def __init__(self, analysis_id: str) -> None: super().__init__(f'Analysis "{analysis_id}" not found.') +class AnalysisIsPendingError(RuntimeError): + """Exception raised if a given analysis is still pending.""" + + def __init__(self, analysis_id: str) -> None: + """Initialize the error's message.""" + super().__init__(f'Analysis "{analysis_id}" is still pending.') + + # TODO(sf, 2023-05-05): Like for protocols and runs, there's an in-memory cache for # elements of this store. Unlike for protocols and runs, it isn't just an lru_cache # on the top-level store's access methods, because those access methods have to be @@ -93,10 +103,14 @@ class AnalysisStore: so they're only kept in-memory, and lost when the store instance is destroyed. """ - def __init__(self, sql_engine: sqlalchemy.engine.Engine) -> None: + def __init__( + self, + sql_engine: sqlalchemy.engine.Engine, + completed_store: Optional[CompletedAnalysisStore] = None, + ) -> None: """Initialize the `AnalysisStore`.""" self._pending_store = _PendingAnalysisStore() - self._completed_store = CompletedAnalysisStore( + self._completed_store = completed_store or CompletedAnalysisStore( sql_engine=sql_engine, memory_cache=MemoryCache(_CACHE_MAX_SIZE, str, CompletedAnalysisResource), current_analyzer_version=_CURRENT_ANALYZER_VERSION, @@ -180,6 +194,9 @@ async def update( protocol_id=protocol_id, analyzer_version=_CURRENT_ANALYZER_VERSION, completed_analysis=completed_analysis, + run_time_parameter_values_and_defaults=self._extract_run_time_param_values_and_defaults( + completed_analysis + ), ) await self._completed_store.add( completed_analysis_resource=completed_analysis_resource @@ -258,6 +275,88 @@ async def get_by_protocol(self, protocol_id: str) -> List[ProtocolAnalysis]: else: return completed_analyses + [pending_analysis] + @staticmethod + def _extract_run_time_param_values_and_defaults( + completed_analysis: CompletedAnalysis, + ) -> Dict[str, RunTimeParameterAnalysisData]: + """Extract the Run Time Parameters with current value and default value of each. + + We do this in order to save the RTP data separately, outside the analysis + in the database. This saves us from having to de-serialize the entire analysis + to read just the RTP values. + """ + rtp_list = completed_analysis.runTimeParameters + + rtp_values_and_defaults = {} + for param_spec in rtp_list: + rtp_values_and_defaults.update( + { + param_spec.variableName: RunTimeParameterAnalysisData( + value=param_spec.value, default=param_spec.default + ) + } + ) + return rtp_values_and_defaults + + async def matching_rtp_values_in_analysis( + self, analysis_summary: AnalysisSummary, new_rtp_values: RunTimeParamValuesType + ) -> bool: + """Return whether the last analysis of the given protocol used the mentioned RTP values. + + It is not sufficient to just check the values of provided parameters against the + corresponding parameter values in analysis because a previous request could have + composed of some extra parameters that are not in the current list. + + Similarly, it is not enough to only compare the current parameter values from + the client with the previous values from the client because a previous param + might have been assigned a default value by the client while the current request + doesn't include that param because it can rely on the API to assign the default + value to that param. + + So, we check that the Run Time Parameters in the previous analysis has params + with the values provided in the current request, and also verify that rest of the + parameters in the analysis use default values. + """ + if analysis_summary.status == AnalysisStatus.PENDING: + raise AnalysisIsPendingError(analysis_summary.id) + + rtp_values_and_defaults_in_last_analysis = ( + await self._completed_store.get_rtp_values_and_defaults_by_analysis_id( + analysis_summary.id + ) + ) + # We already make sure that the protocol has an analysis associated with before + # checking the RTP values so this assert should never raise. + # It is only added for type checking. + assert ( + rtp_values_and_defaults_in_last_analysis is not None + ), "This protocol has no analysis associated with it." + + if not set(new_rtp_values.keys()).issubset( + set(rtp_values_and_defaults_in_last_analysis.keys()) + ): + # Since the RTP keys in analysis represent all params defined in the protocol, + # if the client passes a parameter that's not present in the analysis, + # it means that the client is sending incorrect parameters. + # We will let this request trigger an analysis using the incorrect params + # and have the analysis raise an appropriate error instead of giving an + # error response to the protocols request. + # This makes the behavior of robot server consistent regardless of whether + # the client is sending a protocol for the first time or for the nth time. + return False + for ( + parameter, + prev_value_and_default, + ) in rtp_values_and_defaults_in_last_analysis.items(): + if ( + new_rtp_values.get(parameter, prev_value_and_default.default) + == prev_value_and_default.value + ): + continue + else: + return False + return True + class _PendingAnalysisStore: """An in-memory store of protocol analyses that are pending. diff --git a/robot-server/robot_server/protocols/completed_analysis_store.py b/robot-server/robot_server/protocols/completed_analysis_store.py index f4c696d0519..58017e4398a 100644 --- a/robot-server/robot_server/protocols/completed_analysis_store.py +++ b/robot-server/robot_server/protocols/completed_analysis_store.py @@ -2,18 +2,20 @@ from __future__ import annotations import asyncio +import json from typing import Dict, List, Optional from logging import getLogger from dataclasses import dataclass import sqlalchemy import anyio +from pydantic import parse_raw_as from robot_server.persistence.database import sqlite_rowid from robot_server.persistence.tables import analysis_table from robot_server.persistence.pydantic import json_to_pydantic, pydantic_to_json -from .analysis_models import CompletedAnalysis +from .analysis_models import CompletedAnalysis, RunTimeParameterAnalysisData from .analysis_memcache import MemoryCache @@ -31,6 +33,7 @@ class CompletedAnalysisResource: protocol_id: str analyzer_version: str completed_analysis: CompletedAnalysis + run_time_parameter_values_and_defaults: Dict[str, RunTimeParameterAnalysisData] async def to_sql_values(self) -> Dict[str, object]: """Return this data as a dict that can be passed to a SQLALchemy insert. @@ -46,18 +49,25 @@ async def to_sql_values(self) -> Dict[str, object]: def serialize_completed_analysis() -> str: return pydantic_to_json(self.completed_analysis) - serialized_json = await anyio.to_thread.run_sync( + def serialize_rtp_dict() -> str: + return json.dumps(self.run_time_parameter_values_and_defaults) + + serialized_analysis = await anyio.to_thread.run_sync( serialize_completed_analysis, # Cancellation may orphan the worker thread, # but that should be harmless in this case. cancellable=True, ) - + serialized_rtp_dict = await anyio.to_thread.run_sync( + serialize_rtp_dict, + cancellable=True, + ) return { "id": self.id, "protocol_id": self.protocol_id, "analyzer_version": self.analyzer_version, - "completed_analysis": serialized_json, + "completed_analysis": serialized_analysis, + "run_time_parameter_values_and_defaults": serialized_rtp_dict, } @classmethod @@ -94,12 +104,40 @@ def parse_completed_analysis() -> CompletedAnalysis: # but that should be harmless in this case. cancellable=True, ) - + rtp_values_and_defaults = await cls.get_run_time_parameter_values_and_defaults( + sql_row + ) return cls( id=id, protocol_id=protocol_id, analyzer_version=analyzer_version, completed_analysis=completed_analysis, + run_time_parameter_values_and_defaults=rtp_values_and_defaults, + ) + + @classmethod + async def get_run_time_parameter_values_and_defaults( + cls, sql_row: sqlalchemy.engine.Row + ) -> Dict[str, RunTimeParameterAnalysisData]: + """Get the run-time parameters used in the analysis with their values & defaults.""" + + def parse_rtp_dict() -> Dict[str, RunTimeParameterAnalysisData]: + rtp_contents = sql_row.run_time_parameter_values_and_defaults + return ( + parse_raw_as( + Dict[str, RunTimeParameterAnalysisData], + sql_row.run_time_parameter_values_and_defaults, + ) + if rtp_contents + else {} + ) + + # In most cases, this parsing should be quite quick but theoretically + # there could be an unexpectedly large number of run time params. + # So we delegate the parsing of this to a cancellable thread as well. + return await anyio.to_thread.run_sync( + parse_rtp_dict, + cancellable=True, ) @@ -185,6 +223,40 @@ async def get_by_id_as_document(self, analysis_id: str) -> Optional[str]: return document + async def get_rtp_values_and_defaults_by_analysis_id( + self, analysis_id: str + ) -> Optional[Dict[str, RunTimeParameterAnalysisData]]: + """Return the dictionary of run time parameter values & defaults used in the given analysis. + + If the analysis ID doesn't exist, return None. + These RTP values are not cached in memory by themselves since we don't anticipate + that fetching the values from the database to be a time-consuming operation. + """ + async with self._memcache_lock: + try: + analysis = self._memcache.get(analysis_id) + except KeyError: + pass + else: + return analysis.run_time_parameter_values_and_defaults + + statement = sqlalchemy.select(analysis_table).where( + analysis_table.c.id == analysis_id + ) + with self._sql_engine.begin() as transaction: + try: + result = transaction.execute(statement).one() + except sqlalchemy.exc.NoResultFound: + # Since we just no-op when fetching non-existent analysis, + # do the same for non-existent RTP data + return None + + rtp_values_and_defaults = await CompletedAnalysisResource.get_run_time_parameter_values_and_defaults( + result + ) + + return rtp_values_and_defaults + async def get_by_protocol( self, protocol_id: str ) -> List[CompletedAnalysisResource]: diff --git a/robot-server/robot_server/protocols/router.py b/robot-server/robot_server/protocols/router.py index fb72c938def..8ae9365de36 100644 --- a/robot-server/robot_server/protocols/router.py +++ b/robot-server/robot_server/protocols/router.py @@ -37,7 +37,7 @@ from .protocol_auto_deleter import ProtocolAutoDeleter from .protocol_models import Protocol, ProtocolFile, Metadata from .protocol_analyzer import ProtocolAnalyzer -from .analysis_store import AnalysisStore, AnalysisNotFoundError +from .analysis_store import AnalysisStore, AnalysisNotFoundError, AnalysisIsPendingError from .analysis_models import ProtocolAnalysis from .protocol_store import ( ProtocolStore, @@ -74,6 +74,13 @@ class AnalysisNotFound(ErrorDetails): title: str = "Protocol Analysis Not Found" +class LastAnalysisPending(ErrorDetails): + """An error returned when the most recent analysis of a protocol is still pending.""" + + id: Literal["LastAnalysisPending"] = "LastAnalysisPending" + title: str = "Last Analysis Still Pending." + + class ProtocolFilesInvalid(ErrorDetails): """An error returned when an uploaded protocol files are invalid.""" @@ -140,7 +147,9 @@ class ProtocolLinks(BaseModel): resource will be returned instead of creating duplicate ones. When a new protocol resource is created, an analysis is started for it. - See the `/protocols/{id}/analyses/` endpoints. + A new analysis is also started if the same protocol file is uploaded but with + a different set of run-time parameter values than the most recent request. + See the `/protocols/{id}/analyses/` endpoints for more details. """ ), status_code=status.HTTP_201_CREATED, @@ -150,9 +159,10 @@ class ProtocolLinks(BaseModel): status.HTTP_422_UNPROCESSABLE_ENTITY: { "model": ErrorBody[Union[ProtocolFilesInvalid, ProtocolRobotTypeMismatch]] }, + status.HTTP_503_SERVICE_UNAVAILABLE: {"model": ErrorBody[LastAnalysisPending]}, }, ) -async def create_protocol( +async def create_protocol( # noqa: C901 files: List[UploadFile] = File(...), # use Form because request is multipart/form-data # https://fastapi.tiangolo.com/tutorial/request-forms-and-files/ @@ -214,7 +224,6 @@ async def create_protocol( # TODO(mm, 2024-02-07): Investigate whether the filename can actually be None. assert file.filename is not None buffered_files = await file_reader_writer.read(files=files) # type: ignore[arg-type] - if isinstance(run_time_parameter_values, str): # We have to do this isinstance check because if `runTimeParameterValues` is # not specified in the request, then it gets assigned a Form(None) value @@ -223,29 +232,46 @@ async def create_protocol( # so we can validate the data contents and return a better error response. parsed_rtp = json.loads(run_time_parameter_values) else: - parsed_rtp = None + parsed_rtp = {} content_hash = await file_hasher.hash(buffered_files) cached_protocol_id = protocol_store.get_id_by_hash(content_hash) if cached_protocol_id is not None: - # Protocol exists in database resource = protocol_store.get(protocol_id=cached_protocol_id) - if parsed_rtp: - # This protocol exists in database but needs to be re-analyzed with the - # passed-in RTP overrides - task_runner.run( - protocol_analyzer.analyze, - protocol_resource=resource, - analysis_id=analysis_id, - run_time_param_values=parsed_rtp, - ) - analysis_store.add_pending( - protocol_id=cached_protocol_id, - analysis_id=analysis_id, - ) analyses = analysis_store.get_summaries_by_protocol( protocol_id=cached_protocol_id ) + + try: + if ( + # Unexpected situations, like powering off the robot after a protocol upload + # but before the analysis is complete, can leave the protocol resource + # without an associated analysis. + len(analyses) == 0 + or + # The most recent analysis was done using different RTP values + not await analysis_store.matching_rtp_values_in_analysis( + analysis_summary=analyses[-1], new_rtp_values=parsed_rtp + ) + ): + # This protocol exists in database but needs to be (re)analyzed + task_runner.run( + protocol_analyzer.analyze, + protocol_resource=resource, + analysis_id=analysis_id, + run_time_param_values=parsed_rtp, + ) + analyses.append( + analysis_store.add_pending( + protocol_id=cached_protocol_id, + analysis_id=analysis_id, + ) + ) + except AnalysisIsPendingError as error: + raise LastAnalysisPending(detail=str(error)).as_error( + status.HTTP_503_SERVICE_UNAVAILABLE + ) from error + data = Protocol.construct( id=cached_protocol_id, createdAt=resource.created_at, diff --git a/robot-server/tests/integration/http_api/persistence/test_reset.py b/robot-server/tests/integration/http_api/persistence/test_reset.py index c9973713802..394671bba64 100644 --- a/robot-server/tests/integration/http_api/persistence/test_reset.py +++ b/robot-server/tests/integration/http_api/persistence/test_reset.py @@ -40,9 +40,9 @@ async def _assert_reset_was_successful( all_files_and_directories = set(persistence_directory.glob("**/*")) expected_files_and_directories = { persistence_directory / "robot_server.db", - persistence_directory / "3", - persistence_directory / "3" / "protocols", - persistence_directory / "3" / "robot_server.db", + persistence_directory / "4", + persistence_directory / "4" / "protocols", + persistence_directory / "4" / "robot_server.db", } assert all_files_and_directories == expected_files_and_directories diff --git a/robot-server/tests/integration/http_api/protocols/test_analyses.tavern.yaml b/robot-server/tests/integration/http_api/protocols/test_analyses.tavern.yaml index 3634989ed3f..a756ea10e1b 100644 --- a/robot-server/tests/integration/http_api/protocols/test_analyses.tavern.yaml +++ b/robot-server/tests/integration/http_api/protocols/test_analyses.tavern.yaml @@ -84,26 +84,3 @@ stages: # We need to make sure we get the Content-Type right because FastAPI won't do it for us. Content-Type: application/json json: !force_format_include '{analysis_data}' - - - name: Check that uploading the same protocol with run-time parameter values triggers re-analysis - # This test must be executed after the analysis of the previous upload is completed. - request: - url: '{ot2_server_base_url}/protocols' - method: POST - data: - runTimeParameterValues: '{{"volume": 123, "dry_run": true, "pipette": "p10_single"}}' - files: - files: 'tests/integration/protocols/basic_transfer_standalone.py' - response: - strict: - - json:off - status_code: 200 - json: - data: - id: '{protocol_id}' - analyses: [] - analysisSummaries: - - id: '{analysis_id}' - status: completed - - id: !anystr - status: pending diff --git a/robot-server/tests/integration/http_api/protocols/test_analyses_with_run_time_parameters.tavern.yaml b/robot-server/tests/integration/http_api/protocols/test_analyses_with_run_time_parameters.tavern.yaml new file mode 100644 index 00000000000..3ad017a546d --- /dev/null +++ b/robot-server/tests/integration/http_api/protocols/test_analyses_with_run_time_parameters.tavern.yaml @@ -0,0 +1,180 @@ +test_name: Test the protocol analysis endpoints with run time parameters + +marks: + - usefixtures: + - ot2_server_base_url + +stages: + - name: Upload a protocol + request: + url: '{ot2_server_base_url}/protocols' + method: POST + files: + files: 'tests/integration/protocols/basic_transfer_with_run_time_parameters.py' + response: + save: + json: + protocol_id: data.id + analysis_id: data.analysisSummaries[0].id + strict: + - json:off + status_code: 201 + json: + data: + analyses: [] + analysisSummaries: + - id: !anystr + status: pending + + - name: Check that the analysis summary is present in /protocols/:id; retry until it says it's completed + max_retries: 5 + delay_after: 1 + request: + url: '{ot2_server_base_url}/protocols/{protocol_id}' + response: + status_code: 200 + json: + data: + analyses: [] + analysisSummaries: + - id: '{analysis_id}' + status: completed + id: !anything + protocolType: !anything + files: !anything + createdAt: !anything + robotType: !anything + metadata: !anything + links: !anything + + - name: Check that the analysis data is present in /protocols/:id/analyses/:id + request: + url: '{ot2_server_base_url}/protocols/{protocol_id}/analyses/{analysis_id}' + response: + strict: + - json:off + json: + data: + id: '{analysis_id}' + runTimeParameters: + - displayName: Sample count + variableName: sample_count + type: int + default: 6.0 + min: 1.0 + max: 12.0 + value: 6.0 + description: How many samples to process. + - displayName: Pipette volume + variableName: volume + type: float + default: 20.1 + choices: + - displayName: Low Volume + value: 10.23 + - displayName: Medium Volume + value: 20.1 + - displayName: High Volume + value: 50.5 + value: 20.1 + description: How many microliters to pipette of each sample. + - displayName: Dry Run + variableName: dry_run + type: bool + default: false + value: false + description: Skip aspirate and dispense steps. + - displayName: Pipette Name + variableName: pipette + type: str + choices: + - displayName: Single channel 50µL + value: flex_1channel_50 + - displayName: Eight Channel 50µL + value: flex_8channel_50 + default: flex_1channel_50 + value: flex_1channel_50 + description: What pipette to use during the protocol. + commands: + # Check for this command's presence as a smoke test that the analysis isn't empty. + - commandType: loadPipette + + - name: Check that uploading same protocol with new run time parameter values re-triggers analysis + # This test must be executed after the analysis of the previous upload is completed. + request: + url: '{ot2_server_base_url}/protocols' + method: POST + data: + runTimeParameterValues: '{{"sample_count": 10, "volume": 10.23, "dry_run": true}}' + files: + files: 'tests/integration/protocols/basic_transfer_with_run_time_parameters.py' + response: + save: + json: + analysis_id2: data.analysisSummaries[1].id + strict: + - json:off + status_code: 200 + json: + data: + id: '{protocol_id}' + analyses: [ ] + analysisSummaries: + - id: '{analysis_id}' + status: completed + - id: !anystr + status: pending + + - name: Check that the new analysis uses run time parameter values from client; retry until analysis is completed + max_retries: 5 + delay_after: 1 + request: + url: '{ot2_server_base_url}/protocols/{protocol_id}/analyses/{analysis_id2}' + response: + strict: + - json:off + json: + data: + id: '{analysis_id2}' + runTimeParameters: + - displayName: Sample count + variableName: sample_count + type: int + default: 6.0 + min: 1.0 + max: 12.0 + value: 10.0 + description: How many samples to process. + - displayName: Pipette volume + variableName: volume + type: float + default: 20.1 + choices: + - displayName: Low Volume + value: 10.23 + - displayName: Medium Volume + value: 20.1 + - displayName: High Volume + value: 50.5 + value: 10.23 + description: How many microliters to pipette of each sample. + - displayName: Dry Run + variableName: dry_run + type: bool + default: false + value: true + description: Skip aspirate and dispense steps. + - displayName: Pipette Name + variableName: pipette + type: str + choices: + - displayName: Single channel 50µL + value: flex_1channel_50 + - displayName: Eight Channel 50µL + value: flex_8channel_50 + default: flex_1channel_50 + value: flex_1channel_50 + description: What pipette to use during the protocol. + commands: + # Check for this command's presence as a smoke test that the analysis isn't empty. + - commandType: loadPipette \ No newline at end of file diff --git a/robot-server/tests/integration/http_api/protocols/test_key.tavern.yaml b/robot-server/tests/integration/http_api/protocols/test_key.tavern.yaml index 7d0f4361cb3..7729ee15fa5 100644 --- a/robot-server/tests/integration/http_api/protocols/test_key.tavern.yaml +++ b/robot-server/tests/integration/http_api/protocols/test_key.tavern.yaml @@ -169,6 +169,8 @@ stages: author: engineer@opentrons.com key: duplicate_key - name: Upload basic_transfer_standalone protocol with same key + # add a delay before starting to let previous analysis complete + delay_before: 2 request: url: '{ot2_server_base_url}/protocols' method: POST diff --git a/robot-server/tests/integration/http_api/protocols/test_persistence.py b/robot-server/tests/integration/http_api/protocols/test_persistence.py index a939f5f5fda..0480accb39c 100644 --- a/robot-server/tests/integration/http_api/protocols/test_persistence.py +++ b/robot-server/tests/integration/http_api/protocols/test_persistence.py @@ -120,10 +120,10 @@ async def test_protocol_labware_files_persist() -> None: assert restarted_protocol_detail == protocol_detail four_tuberack = Path( - f"{server.persistence_directory}/3/protocols/{protocol_id}/cpx_4_tuberack_100ul.json" + f"{server.persistence_directory}/4/protocols/{protocol_id}/cpx_4_tuberack_100ul.json" ) six_tuberack = Path( - f"{server.persistence_directory}/3/protocols/{protocol_id}/cpx_6_tuberack_100ul.json" + f"{server.persistence_directory}/4/protocols/{protocol_id}/cpx_6_tuberack_100ul.json" ) assert four_tuberack.is_file() assert six_tuberack.is_file() diff --git a/robot-server/tests/integration/protocols/basic_transfer_with_run_time_parameters.py b/robot-server/tests/integration/protocols/basic_transfer_with_run_time_parameters.py new file mode 100644 index 00000000000..7fe90c65d8c --- /dev/null +++ b/robot-server/tests/integration/protocols/basic_transfer_with_run_time_parameters.py @@ -0,0 +1,57 @@ +from opentrons.protocol_api import ProtocolContext, ParameterContext + +metadata = { + "apiLevel": "2.18", + "author": "engineer@opentrons.com", + "protocolName": "basic_transfer_standalone", +} + + +def add_parameters(parameters: ParameterContext): + parameters.add_int( + display_name="Sample count", + variable_name="sample_count", + default=6, + minimum=1, + maximum=12, + description="How many samples to process.", + ) + parameters.add_float( + display_name="Pipette volume", + variable_name="volume", + default=20.1, + choices=[ + {"display_name": "Low Volume", "value": 10.23}, + {"display_name": "Medium Volume", "value": 20.1}, + {"display_name": "High Volume", "value": 50.5}, + ], + description="How many microliters to pipette of each sample.", + unit="µL", # Unit is not wired up, and it doesn't raise errors either. + ) + parameters.add_bool( + display_name="Dry Run", + variable_name="dry_run", + default=False, + description="Skip aspirate and dispense steps.", + ) + parameters.add_str( + display_name="Pipette Name", + variable_name="pipette", + choices=[ + {"display_name": "Single channel 50µL", "value": "flex_1channel_50"}, + {"display_name": "Eight Channel 50µL", "value": "flex_8channel_50"}, + ], + default="flex_1channel_50", + description="What pipette to use during the protocol.", + ) + + +def run(protocol: ProtocolContext) -> None: + plate = protocol.load_labware("corning_96_wellplate_360ul_flat", 1) + tiprack_1 = protocol.load_labware("opentrons_96_tiprack_300ul", 2) + p300 = protocol.load_instrument("p300_single", "right", tip_racks=[tiprack_1]) + + p300.pick_up_tip() + p300.aspirate(100, plate["A1"]) + p300.dispense(100, plate["B1"]) + p300.return_tip() diff --git a/robot-server/tests/persistence/test_tables.py b/robot-server/tests/persistence/test_tables.py index ca0bca5c2d5..eaa2824ce75 100644 --- a/robot-server/tests/persistence/test_tables.py +++ b/robot-server/tests/persistence/test_tables.py @@ -10,6 +10,7 @@ metadata as latest_metadata, schema_3, schema_2, + schema_4, ) # The statements that we expect to emit when we create a fresh database. @@ -39,6 +40,7 @@ protocol_id VARCHAR NOT NULL, analyzer_version VARCHAR NOT NULL, completed_analysis VARCHAR NOT NULL, + run_time_parameter_values_and_defaults VARCHAR, PRIMARY KEY (id), FOREIGN KEY(protocol_id) REFERENCES protocol (id) ) @@ -87,8 +89,70 @@ """, ] +EXPECTED_STATEMENTS_V4 = EXPECTED_STATEMENTS_LATEST -EXPECTED_STATEMENTS_V3 = EXPECTED_STATEMENTS_LATEST +EXPECTED_STATEMENTS_V3 = [ + """ + CREATE TABLE protocol ( + id VARCHAR NOT NULL, + created_at DATETIME NOT NULL, + protocol_key VARCHAR, + PRIMARY KEY (id) + ) + """, + """ + CREATE TABLE analysis ( + id VARCHAR NOT NULL, + protocol_id VARCHAR NOT NULL, + analyzer_version VARCHAR NOT NULL, + completed_analysis VARCHAR NOT NULL, + PRIMARY KEY (id), + FOREIGN KEY(protocol_id) REFERENCES protocol (id) + ) + """, + """ + CREATE INDEX ix_analysis_protocol_id ON analysis (protocol_id) + """, + """ + CREATE TABLE run ( + id VARCHAR NOT NULL, + created_at DATETIME NOT NULL, + protocol_id VARCHAR, + state_summary VARCHAR, + engine_status VARCHAR, + _updated_at DATETIME, + PRIMARY KEY (id), + FOREIGN KEY(protocol_id) REFERENCES protocol (id) + ) + """, + """ + CREATE TABLE action ( + id VARCHAR NOT NULL, + created_at DATETIME NOT NULL, + action_type VARCHAR NOT NULL, + run_id VARCHAR NOT NULL, + PRIMARY KEY (id), + FOREIGN KEY(run_id) REFERENCES run (id) + ) + """, + """ + CREATE TABLE run_command ( + row_id INTEGER NOT NULL, + run_id VARCHAR NOT NULL, + index_in_run INTEGER NOT NULL, + command_id VARCHAR NOT NULL, + command VARCHAR NOT NULL, + PRIMARY KEY (row_id), + FOREIGN KEY(run_id) REFERENCES run (id) + ) + """, + """ + CREATE UNIQUE INDEX ix_run_run_id_command_id ON run_command (run_id, command_id) + """, + """ + CREATE UNIQUE INDEX ix_run_run_id_index_in_run ON run_command (run_id, index_in_run) + """, +] EXPECTED_STATEMENTS_V2 = [ @@ -165,6 +229,7 @@ def _normalize_statement(statement: str) -> str: ("metadata", "expected_statements"), [ (latest_metadata, EXPECTED_STATEMENTS_LATEST), + (schema_4.metadata, EXPECTED_STATEMENTS_V4), (schema_3.metadata, EXPECTED_STATEMENTS_V3), (schema_2.metadata, EXPECTED_STATEMENTS_V2), ], @@ -172,7 +237,7 @@ def _normalize_statement(statement: str) -> str: def test_creating_tables_emits_expected_statements( metadata: sqlalchemy.MetaData, expected_statements: List[str] ) -> None: - """Test that fresh databases are created with with the expected statements. + """Test that fresh databases are created with the expected statements. This is a snapshot test to help catch accidental changes to our SQL schema. diff --git a/robot-server/tests/protocols/test_analysis_store.py b/robot-server/tests/protocols/test_analysis_store.py index b9c2dcccdac..94d7f67f953 100644 --- a/robot-server/tests/protocols/test_analysis_store.py +++ b/robot-server/tests/protocols/test_analysis_store.py @@ -6,6 +6,8 @@ from typing import List, NamedTuple import pytest +from decoy import Decoy +from opentrons.protocol_engine.types import RunTimeParamValuesType from sqlalchemy.engine import Engine as SQLEngine @@ -28,10 +30,17 @@ AnalysisSummary, PendingAnalysis, CompletedAnalysis, + RunTimeParameterAnalysisData, ) from robot_server.protocols.analysis_store import ( AnalysisStore, AnalysisNotFoundError, + AnalysisIsPendingError, + _CURRENT_ANALYZER_VERSION, +) +from robot_server.protocols.completed_analysis_store import ( + CompletedAnalysisStore, + CompletedAnalysisResource, ) from robot_server.protocols.protocol_store import ( ProtocolStore, @@ -171,12 +180,20 @@ async def test_update_adds_details_and_completes_analysis( pipetteName=PipetteNameType.P300_SINGLE, mount=MountType.LEFT, ) - + run_time_param = pe_types.NumberParameter( + displayName="My parameter", + variableName="cool_param", + type="int", + min=1, + max=5, + value=2.0, + default=3.0, + ) subject.add_pending(protocol_id="protocol-id", analysis_id="analysis-id") await subject.update( analysis_id="analysis-id", robot_type="OT-2 Standard", - run_time_parameters=[], + run_time_parameters=[run_time_param], labware=[labware], pipettes=[pipette], # TODO(mm, 2022-10-21): Give the subject some commands, errors, and liquids here @@ -195,7 +212,7 @@ async def test_update_adds_details_and_completes_analysis( status=AnalysisStatus.COMPLETED, result=AnalysisResult.OK, robotType="OT-2 Standard", - runTimeParameters=[], + runTimeParameters=[run_time_param], labware=[labware], pipettes=[pipette], modules=[], @@ -209,7 +226,17 @@ async def test_update_adds_details_and_completes_analysis( "result": "ok", "status": "completed", "robotType": "OT-2 Standard", - "runTimeParameters": [], + "runTimeParameters": [ + { + "displayName": "My parameter", + "variableName": "cool_param", + "type": "int", + "min": 1, + "max": 5, + "value": 2.0, + "default": 3.0, + } + ], "labware": [ { "id": "labware-id", @@ -228,6 +255,76 @@ async def test_update_adds_details_and_completes_analysis( } +async def test_update_adds_rtp_values_and_defaults_to_completed_store( + decoy: Decoy, sql_engine: SQLEngine, protocol_store: ProtocolStore +) -> None: + """It should add RTP values and defaults to completed analysis store.""" + number_param = pe_types.NumberParameter( + displayName="My parameter", + variableName="cool_param", + type="int", + min=1, + max=5, + value=2.0, + default=3.0, + ) + string_param = pe_types.EnumParameter( + displayName="A choiced param", + variableName="cooler_param", + type="str", + choices=[ + pe_types.EnumChoice(displayName="FOOOO", value="foo"), + pe_types.EnumChoice(displayName="BARRR", value="bar"), + ], + value="baz", + default="blah", + ) + expected_completed_analysis_resource = CompletedAnalysisResource( + id="analysis-id", + protocol_id="protocol-id", + analyzer_version=_CURRENT_ANALYZER_VERSION, + completed_analysis=CompletedAnalysis( + id="analysis-id", + status=AnalysisStatus.COMPLETED, + result=AnalysisResult.OK, + robotType="OT-2 Standard", + runTimeParameters=[number_param, string_param], + labware=[], + pipettes=[], + modules=[], + commands=[], + errors=[], + liquids=[], + ), + run_time_parameter_values_and_defaults={ + "cool_param": RunTimeParameterAnalysisData(value=2.0, default=3.0), + "cooler_param": RunTimeParameterAnalysisData(value="baz", default="blah"), + }, + ) + + mock_completed_store = decoy.mock(cls=CompletedAnalysisStore) + subject = AnalysisStore(sql_engine=sql_engine, completed_store=mock_completed_store) + protocol_store.insert(make_dummy_protocol_resource(protocol_id="protocol-id")) + + subject.add_pending(protocol_id="protocol-id", analysis_id="analysis-id") + await subject.update( + analysis_id="analysis-id", + robot_type="OT-2 Standard", + run_time_parameters=[number_param, string_param], + labware=[], + pipettes=[], + modules=[], + commands=[], + errors=[], + liquids=[], + ) + decoy.verify( + await mock_completed_store.add( + completed_analysis_resource=expected_completed_analysis_resource + ) + ) + + class AnalysisResultSpec(NamedTuple): """Spec data for analysis result tests.""" @@ -291,3 +388,101 @@ async def test_update_infers_status_from_errors( analysis = (await subject.get_by_protocol("protocol-id"))[0] assert isinstance(analysis, CompletedAnalysis) assert analysis.result == expected_result + + +@pytest.mark.parametrize( + argnames=["rtp_values_from_client", "expected_match"], + argvalues=[ + ({"cool_param": 2.0, "cooler_param": "baz", "uncool_param": 5}, True), + ( + {"cool_param": 2, "cooler_param": "baz"}, + True, + ), + ( + {"cool_param": 2, "cooler_param": "buzzzzzzz"}, + False, + ), + ( + {"cool_param": 2.0, "cooler_param": "baz", "weird_param": 5}, + False, + ), + ({}, False), + ], +) +async def test_matching_rtp_values_in_analysis( + decoy: Decoy, + sql_engine: SQLEngine, + protocol_store: ProtocolStore, + rtp_values_from_client: RunTimeParamValuesType, + expected_match: bool, +) -> None: + """It should return whether the client's RTP values match with those in the last analysis of protocol.""" + mock_completed_store = decoy.mock(cls=CompletedAnalysisStore) + subject = AnalysisStore(sql_engine=sql_engine, completed_store=mock_completed_store) + protocol_store.insert(make_dummy_protocol_resource(protocol_id="protocol-id")) + + decoy.when( + await mock_completed_store.get_rtp_values_and_defaults_by_analysis_id( + "analysis-2" + ) + ).then_return( + { + "cool_param": RunTimeParameterAnalysisData(value=2.0, default=3.0), + "cooler_param": RunTimeParameterAnalysisData( + value="baz", default="very cool" + ), + "uncool_param": RunTimeParameterAnalysisData(value=5, default=5), + } + ) + assert ( + await subject.matching_rtp_values_in_analysis( + analysis_summary=AnalysisSummary( + id="analysis-2", status=AnalysisStatus.COMPLETED + ), + new_rtp_values=rtp_values_from_client, + ) + == expected_match + ) + + +async def test_matching_default_rtp_values_in_analysis_with_no_client_rtp_values( + decoy: Decoy, + sql_engine: SQLEngine, + protocol_store: ProtocolStore, +) -> None: + """It should return a match when client sends no RTP values and last analysis used all default values.""" + params_with_only_default_values = { + "cool_param": RunTimeParameterAnalysisData(value=2.0, default=2.0), + "cooler_param": RunTimeParameterAnalysisData( + value="very cool", default="very cool" + ), + "uncool_param": RunTimeParameterAnalysisData(value=True, default=True), + } + mock_completed_store = decoy.mock(cls=CompletedAnalysisStore) + subject = AnalysisStore(sql_engine=sql_engine, completed_store=mock_completed_store) + protocol_store.insert(make_dummy_protocol_resource(protocol_id="protocol-id")) + + decoy.when( + await mock_completed_store.get_rtp_values_and_defaults_by_analysis_id( + "analysis-2" + ) + ).then_return(params_with_only_default_values) + assert ( + await subject.matching_rtp_values_in_analysis( + analysis_summary=AnalysisSummary( + id="analysis-2", status=AnalysisStatus.COMPLETED + ), + new_rtp_values={}, + ) + is True + ) + + +async def test_matching_default_rtp_values_in_analysis_with_pending_analysis( + subject: AnalysisStore, protocol_store: ProtocolStore +) -> None: + """It should raise an error if analysis is pending.""" + with pytest.raises(AnalysisIsPendingError): + await subject.matching_rtp_values_in_analysis( + AnalysisSummary(id="analysis-id", status=AnalysisStatus.PENDING), {} + ) diff --git a/robot-server/tests/protocols/test_completed_analysis_store.py b/robot-server/tests/protocols/test_completed_analysis_store.py index 8339460cf66..f41594d0c5d 100644 --- a/robot-server/tests/protocols/test_completed_analysis_store.py +++ b/robot-server/tests/protocols/test_completed_analysis_store.py @@ -2,6 +2,7 @@ import json from datetime import datetime, timezone from pathlib import Path +from typing import Optional, Dict import pytest from sqlalchemy.engine import Engine @@ -20,6 +21,7 @@ CompletedAnalysis, AnalysisResult, AnalysisStatus, + RunTimeParameterAnalysisData, ) from robot_server.protocols.protocol_store import ( ProtocolStore, @@ -76,7 +78,9 @@ def make_dummy_protocol_resource(protocol_id: str) -> ProtocolResource: def _completed_analysis_resource( - analysis_id: str, protocol_id: str + analysis_id: str, + protocol_id: str, + rtp_values_and_defaults: Optional[Dict[str, RunTimeParameterAnalysisData]] = None, ) -> CompletedAnalysisResource: return CompletedAnalysisResource( analysis_id, @@ -93,6 +97,7 @@ def _completed_analysis_resource( errors=[], liquids=[], ), + run_time_parameter_values_and_defaults=rtp_values_and_defaults or {}, ) @@ -212,3 +217,47 @@ async def test_get_by_protocol( decoy.when(memcache.insert("analysis-id-1", resource_1)).then_return(None) resources = await subject.get_by_protocol("protocol-id-1") assert resources == [resource_1, resource_2] + + +async def test_get_rtp_values_and_defaults_by_analysis_id_prefers_memcache( + subject: CompletedAnalysisStore, + memcache: MemoryCache[str, CompletedAnalysisResource], + protocol_store: ProtocolStore, + decoy: Decoy, +) -> None: + """It should return RTP values and defaults dict from memcache.""" + resource = _completed_analysis_resource( + analysis_id="analysis-id", + protocol_id="protocol-id", + rtp_values_and_defaults={ + "abc": RunTimeParameterAnalysisData(value=123, default=234) + }, + ) + protocol_store.insert(make_dummy_protocol_resource("protocol-id")) + # When we retrieve a resource via its id we should see it query the cache, and it should + # return the identity-same resource + decoy.when(memcache.get("analysis-id")).then_return(resource) + result = await subject.get_rtp_values_and_defaults_by_analysis_id("analysis-id") + assert result == resource.run_time_parameter_values_and_defaults + + +async def test_get_rtp_values_and_defaults_by_analysis_from_db( + subject: CompletedAnalysisStore, + memcache: MemoryCache[str, CompletedAnalysisResource], + protocol_store: ProtocolStore, + decoy: Decoy, +) -> None: + """It should fetch the RTP values and defaults dict from database if not present in cache.""" + resource = _completed_analysis_resource( + analysis_id="analysis-id", + protocol_id="protocol-id", + rtp_values_and_defaults={ + "xyz": RunTimeParameterAnalysisData(value=123, default=234) + }, + ) + protocol_store.insert(make_dummy_protocol_resource("protocol-id")) + await subject.add(resource) + # Not in memcache + decoy.when(memcache.get("analysis-id")).then_raise(KeyError()) + result = await subject.get_rtp_values_and_defaults_by_analysis_id("analysis-id") + assert result == resource.run_time_parameter_values_and_defaults diff --git a/robot-server/tests/protocols/test_protocols_router.py b/robot-server/tests/protocols/test_protocols_router.py index dbdad50c3bd..ffb02d929b1 100644 --- a/robot-server/tests/protocols/test_protocols_router.py +++ b/robot-server/tests/protocols/test_protocols_router.py @@ -1,5 +1,6 @@ """Tests for the /protocols router.""" import io + import pytest from datetime import datetime from decoy import Decoy, matchers @@ -24,7 +25,11 @@ from robot_server.errors.error_responses import ApiError from robot_server.service.json_api import SimpleEmptyBody, MultiBodyMeta from robot_server.service.task_runner import TaskRunner -from robot_server.protocols.analysis_store import AnalysisStore, AnalysisNotFoundError +from robot_server.protocols.analysis_store import ( + AnalysisStore, + AnalysisNotFoundError, + AnalysisIsPendingError, +) from robot_server.protocols.protocol_analyzer import ProtocolAnalyzer from robot_server.protocols.protocol_auto_deleter import ProtocolAutoDeleter from robot_server.protocols.analysis_models import ( @@ -373,6 +378,11 @@ async def test_create_existing_protocol( decoy.when( analysis_store.get_summaries_by_protocol(protocol_id="the-og-proto-id") ).then_return([completed_analysis]) + decoy.when( + await analysis_store.matching_rtp_values_in_analysis( + analysis_summary=completed_analysis, new_rtp_values={} + ) + ).then_return(True) result = await create_protocol( files=[protocol_file], @@ -513,12 +523,12 @@ async def test_create_protocol( protocol_analyzer.analyze, analysis_id="analysis-id", protocol_resource=protocol_resource, - run_time_param_values=None, + run_time_param_values={}, ), ) -async def test_create_protocol_with_run_time_params( +async def test_create_new_protocol_with_run_time_params( decoy: Decoy, protocol_store: ProtocolStore, analysis_store: AnalysisStore, @@ -620,7 +630,240 @@ async def test_create_protocol_with_run_time_params( ) -async def test_create_existing_protocol_with_run_time_params( +async def test_create_existing_protocol_with_no_previous_analysis( + decoy: Decoy, + protocol_store: ProtocolStore, + analysis_store: AnalysisStore, + protocol_reader: ProtocolReader, + file_reader_writer: FileReaderWriter, + file_hasher: FileHasher, + protocol_analyzer: ProtocolAnalyzer, + task_runner: TaskRunner, + protocol_auto_deleter: ProtocolAutoDeleter, +) -> None: + """It should re-trigger analysis of the existing protocol resource.""" + protocol_directory = Path("/dev/null") + content = bytes("some_content", encoding="utf-8") + uploaded_file = io.BytesIO(content) + + protocol_file = UploadFile(filename="foo.json", file=uploaded_file) + buffered_file = BufferedFile(name="blah", contents=content, path=None) + + protocol_source = ProtocolSource( + directory=Path("/dev/null"), + main_file=Path("/dev/null/foo.json"), + files=[ + ProtocolSourceFile( + path=Path("/dev/null/foo.json"), + role=ProtocolFileRole.MAIN, + ) + ], + metadata={"this_is_fake_metadata": True}, + robot_type="OT-2 Standard", + config=JsonProtocolConfig(schema_version=123), + content_hash="a_b_c", + ) + + stored_protocol_resource = ProtocolResource( + protocol_id="protocol-id", + created_at=datetime(year=2020, month=1, day=1), + source=protocol_source, + protocol_key="dummy-key-222", + ) + pending_analysis = AnalysisSummary( + id="analysis-id", + status=AnalysisStatus.PENDING, + ) + decoy.when( + await file_reader_writer.read( + # TODO(mm, 2024-02-07): Recent FastAPI upgrades mean protocol_file.filename + # is typed as possibly None. Investigate whether that can actually happen in + # practice and whether we need to account for it. + files=[protocol_file] # type: ignore[list-item] + ) + ).then_return([buffered_file]) + + decoy.when(await file_hasher.hash(files=[buffered_file])).then_return("a_b_c") + decoy.when(protocol_store.get_id_by_hash("a_b_c")).then_return("the-og-proto-id") + decoy.when(protocol_store.get(protocol_id="the-og-proto-id")).then_return( + stored_protocol_resource + ) + decoy.when( + analysis_store.get_summaries_by_protocol(protocol_id="the-og-proto-id") + ).then_return([]) + decoy.when( + analysis_store.add_pending( + protocol_id="the-og-proto-id", analysis_id="analysis-id" + ) + ).then_return(pending_analysis) + + result = await create_protocol( + files=[protocol_file], + key="dummy-key-111", + run_time_parameter_values='{"vol": 123, "dry_run": true, "mount": "left"}', + protocol_directory=protocol_directory, + protocol_store=protocol_store, + analysis_store=analysis_store, + file_reader_writer=file_reader_writer, + protocol_reader=protocol_reader, + file_hasher=file_hasher, + protocol_analyzer=protocol_analyzer, + task_runner=task_runner, + protocol_auto_deleter=protocol_auto_deleter, + robot_type="OT-2 Standard", + protocol_id="protocol-id", + analysis_id="analysis-id", + created_at=datetime(year=2021, month=1, day=1), + ) + + assert result.content.data == Protocol( + id="the-og-proto-id", + createdAt=datetime(year=2020, month=1, day=1), + protocolType=ProtocolType.JSON, + metadata=Metadata(this_is_fake_metadata=True), # type: ignore[call-arg] + robotType="OT-2 Standard", + analysisSummaries=[pending_analysis], + files=[ProtocolFile(name="foo.json", role=ProtocolFileRole.MAIN)], + key="dummy-key-222", + ) + assert result.status_code == 200 + decoy.verify( + task_runner.run( + protocol_analyzer.analyze, + analysis_id="analysis-id", + protocol_resource=stored_protocol_resource, + run_time_param_values={"vol": 123, "dry_run": True, "mount": "left"}, + ), + analysis_store.add_pending( + protocol_id="the-og-proto-id", + analysis_id="analysis-id", + ), + ) + + +async def test_create_existing_protocol_with_different_run_time_params( + decoy: Decoy, + protocol_store: ProtocolStore, + analysis_store: AnalysisStore, + protocol_reader: ProtocolReader, + file_reader_writer: FileReaderWriter, + file_hasher: FileHasher, + protocol_analyzer: ProtocolAnalyzer, + task_runner: TaskRunner, + protocol_auto_deleter: ProtocolAutoDeleter, +) -> None: + """It should re-trigger analysis of the existing protocol resource.""" + protocol_directory = Path("/dev/null") + content = bytes("some_content", encoding="utf-8") + uploaded_file = io.BytesIO(content) + + protocol_file = UploadFile(filename="foo.json", file=uploaded_file) + buffered_file = BufferedFile(name="blah", contents=content, path=None) + + protocol_source = ProtocolSource( + directory=Path("/dev/null"), + main_file=Path("/dev/null/foo.json"), + files=[ + ProtocolSourceFile( + path=Path("/dev/null/foo.json"), + role=ProtocolFileRole.MAIN, + ) + ], + metadata={"this_is_fake_metadata": True}, + robot_type="OT-2 Standard", + config=JsonProtocolConfig(schema_version=123), + content_hash="a_b_c", + ) + + stored_protocol_resource = ProtocolResource( + protocol_id="protocol-id", + created_at=datetime(year=2020, month=1, day=1), + source=protocol_source, + protocol_key="dummy-key-222", + ) + + completed_summary = AnalysisSummary( + id="analysis-id", + status=AnalysisStatus.COMPLETED, + ) + + pending_summary = AnalysisSummary( + id="analysis-id", + status=AnalysisStatus.PENDING, + ) + decoy.when( + await file_reader_writer.read( + # TODO(mm, 2024-02-07): Recent FastAPI upgrades mean protocol_file.filename + # is typed as possibly None. Investigate whether that can actually happen in + # practice and whether we need to account for it. + files=[protocol_file] # type: ignore[list-item] + ) + ).then_return([buffered_file]) + + decoy.when(await file_hasher.hash(files=[buffered_file])).then_return("a_b_c") + decoy.when(protocol_store.get_id_by_hash("a_b_c")).then_return("the-og-proto-id") + decoy.when(protocol_store.get(protocol_id="the-og-proto-id")).then_return( + stored_protocol_resource + ) + decoy.when( + analysis_store.get_summaries_by_protocol(protocol_id="the-og-proto-id") + ).then_return([completed_summary]) + decoy.when( + await analysis_store.matching_rtp_values_in_analysis( + completed_summary, {"vol": 123, "dry_run": True, "mount": "left"} + ) + ).then_return(False) + decoy.when( + analysis_store.add_pending( + protocol_id="the-og-proto-id", analysis_id="analysis-id" + ) + ).then_return(pending_summary) + + result = await create_protocol( + files=[protocol_file], + key="dummy-key-111", + run_time_parameter_values='{"vol": 123, "dry_run": true, "mount": "left"}', + protocol_directory=protocol_directory, + protocol_store=protocol_store, + analysis_store=analysis_store, + file_reader_writer=file_reader_writer, + protocol_reader=protocol_reader, + file_hasher=file_hasher, + protocol_analyzer=protocol_analyzer, + task_runner=task_runner, + protocol_auto_deleter=protocol_auto_deleter, + robot_type="OT-2 Standard", + protocol_id="protocol-id", + analysis_id="analysis-id", + created_at=datetime(year=2021, month=1, day=1), + ) + + assert result.content.data == Protocol( + id="the-og-proto-id", + createdAt=datetime(year=2020, month=1, day=1), + protocolType=ProtocolType.JSON, + metadata=Metadata(this_is_fake_metadata=True), # type: ignore[call-arg] + robotType="OT-2 Standard", + analysisSummaries=[completed_summary, pending_summary], + files=[ProtocolFile(name="foo.json", role=ProtocolFileRole.MAIN)], + key="dummy-key-222", + ) + assert result.status_code == 200 + decoy.verify( + task_runner.run( + protocol_analyzer.analyze, + analysis_id="analysis-id", + protocol_resource=stored_protocol_resource, + run_time_param_values={"vol": 123, "dry_run": True, "mount": "left"}, + ), + analysis_store.add_pending( + protocol_id="the-og-proto-id", + analysis_id="analysis-id", + ), + ) + + +async def test_create_existing_protocol_with_same_run_time_params( decoy: Decoy, protocol_store: ProtocolStore, analysis_store: AnalysisStore, @@ -666,10 +909,6 @@ async def test_create_existing_protocol_with_run_time_params( id="analysis-id", status=AnalysisStatus.COMPLETED, ), - AnalysisSummary( - id="analysis-id", - status=AnalysisStatus.PENDING, - ), ] decoy.when( @@ -689,6 +928,11 @@ async def test_create_existing_protocol_with_run_time_params( decoy.when( analysis_store.get_summaries_by_protocol(protocol_id="the-og-proto-id") ).then_return(analysis_summaries) + decoy.when( + await analysis_store.matching_rtp_values_in_analysis( + analysis_summaries[-1], {"vol": 123, "dry_run": True, "mount": "left"} + ) + ).then_return(True) result = await create_protocol( files=[protocol_file], @@ -727,11 +971,110 @@ async def test_create_existing_protocol_with_run_time_params( protocol_resource=stored_protocol_resource, run_time_param_values={"vol": 123, "dry_run": True, "mount": "left"}, ), + times=0, + ) + decoy.verify( analysis_store.add_pending( protocol_id="the-og-proto-id", analysis_id="analysis-id", ), + times=0, + ) + + +async def test_create_existing_protocol_with_pending_analysis_raises( + decoy: Decoy, + protocol_store: ProtocolStore, + analysis_store: AnalysisStore, + protocol_reader: ProtocolReader, + file_reader_writer: FileReaderWriter, + file_hasher: FileHasher, + protocol_analyzer: ProtocolAnalyzer, + task_runner: TaskRunner, + protocol_auto_deleter: ProtocolAutoDeleter, +) -> None: + """It should raise an error if protocol has existing pending analysis.""" + protocol_directory = Path("/dev/null") + content = bytes("some_content", encoding="utf-8") + uploaded_file = io.BytesIO(content) + + protocol_file = UploadFile(filename="foo.json", file=uploaded_file) + buffered_file = BufferedFile(name="blah", contents=content, path=None) + + protocol_source = ProtocolSource( + directory=Path("/dev/null"), + main_file=Path("/dev/null/foo.json"), + files=[ + ProtocolSourceFile( + path=Path("/dev/null/foo.json"), + role=ProtocolFileRole.MAIN, + ) + ], + metadata={"this_is_fake_metadata": True}, + robot_type="OT-2 Standard", + config=JsonProtocolConfig(schema_version=123), + content_hash="a_b_c", + ) + + stored_protocol_resource = ProtocolResource( + protocol_id="protocol-id", + created_at=datetime(year=2020, month=1, day=1), + source=protocol_source, + protocol_key="dummy-key-222", + ) + + analysis_summaries = [ + AnalysisSummary( + id="analysis-id", + status=AnalysisStatus.PENDING, + ), + ] + + decoy.when( + await file_reader_writer.read( + # TODO(mm, 2024-02-07): Recent FastAPI upgrades mean protocol_file.filename + # is typed as possibly None. Investigate whether that can actually happen in + # practice and whether we need to account for it. + files=[protocol_file] # type: ignore[list-item] + ) + ).then_return([buffered_file]) + + decoy.when(await file_hasher.hash(files=[buffered_file])).then_return("a_b_c") + decoy.when(protocol_store.get_id_by_hash("a_b_c")).then_return("the-og-proto-id") + decoy.when(protocol_store.get(protocol_id="the-og-proto-id")).then_return( + stored_protocol_resource ) + decoy.when( + analysis_store.get_summaries_by_protocol(protocol_id="the-og-proto-id") + ).then_return(analysis_summaries) + decoy.when( + await analysis_store.matching_rtp_values_in_analysis( + analysis_summaries[-1], {"vol": 123, "dry_run": True, "mount": "left"} + ) + ).then_raise(AnalysisIsPendingError("a-id")) + + with pytest.raises(ApiError) as exc_info: + await create_protocol( + files=[protocol_file], + key="dummy-key-111", + run_time_parameter_values='{"vol": 123, "dry_run": true, "mount": "left"}', + protocol_directory=protocol_directory, + protocol_store=protocol_store, + analysis_store=analysis_store, + file_reader_writer=file_reader_writer, + protocol_reader=protocol_reader, + file_hasher=file_hasher, + protocol_analyzer=protocol_analyzer, + task_runner=task_runner, + protocol_auto_deleter=protocol_auto_deleter, + robot_type="OT-2 Standard", + protocol_id="protocol-id", + analysis_id="analysis-id", + created_at=datetime(year=2021, month=1, day=1), + ) + + assert exc_info.value.status_code == 503 + assert exc_info.value.content["errors"][0]["id"] == "LastAnalysisPending" async def test_create_protocol_not_readable( From 5c3f08b5dcae3fbd6407d077301811056784de3c Mon Sep 17 00:00:00 2001 From: koji Date: Wed, 3 Apr 2024 18:19:42 -0400 Subject: [PATCH 034/194] fix(app,components): fix module controls no module connected case (#14784) * fix(app,components): fix module controls no module connected case --- .../ProtocolRun/ProtocolRunModuleControls.tsx | 22 +++------ .../ProtocolRunRunTimeParameters.tsx | 6 +-- .../ProtocolRunRuntimeParameters.test.tsx | 12 ++--- .../ProtocolParameters/index.tsx | 6 +-- .../{NoParameters.tsx => InfoScreen.tsx} | 14 ++++-- .../__tests__/InfoScreen.test.tsx | 49 +++++++++++++++++++ .../__tests__/NoParameters.test.tsx | 32 ------------ .../src/molecules/{index.tsx => index.ts} | 2 +- 8 files changed, 81 insertions(+), 62 deletions(-) rename components/src/molecules/ParametersTable/{NoParameters.tsx => InfoScreen.tsx} (70%) create mode 100644 components/src/molecules/ParametersTable/__tests__/InfoScreen.test.tsx delete mode 100644 components/src/molecules/ParametersTable/__tests__/NoParameters.test.tsx rename components/src/molecules/{index.tsx => index.ts} (66%) diff --git a/app/src/organisms/Devices/ProtocolRun/ProtocolRunModuleControls.tsx b/app/src/organisms/Devices/ProtocolRun/ProtocolRunModuleControls.tsx index 690ae1b43d0..fa9aad2e7d1 100644 --- a/app/src/organisms/Devices/ProtocolRun/ProtocolRunModuleControls.tsx +++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunModuleControls.tsx @@ -1,13 +1,12 @@ import * as React from 'react' -import { useTranslation } from 'react-i18next' import { useInstrumentsQuery } from '@opentrons/react-api-client' import { COLORS, DIRECTION_COLUMN, Flex, JUSTIFY_CENTER, + InfoScreen, SPACING, - StyledText, } from '@opentrons/components' import { ModuleCard } from '../../ModuleCard' import { useModuleRenderInfoForProtocolById } from '../hooks' @@ -73,8 +72,6 @@ export const ProtocolRunModuleControls = ({ robotName, runId, }: ProtocolRunModuleControlsProps): JSX.Element => { - const { t } = useTranslation('protocol_details') - const { attachPipetteRequired, calibratePipetteRequired, @@ -97,18 +94,15 @@ export const ProtocolRunModuleControls = ({ const rightColumnModules = attachedModules?.slice(halfAttachedModulesSize) return attachedModules.length === 0 ? ( - - - {t('connect_modules_to_see_controls')} - - - ) : ( + + + ) : ( + {!hasParameter ? ( - + ) : ( <> diff --git a/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunRuntimeParameters.test.tsx b/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunRuntimeParameters.test.tsx index 36c71e6d363..f683986c26b 100644 --- a/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunRuntimeParameters.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunRuntimeParameters.test.tsx @@ -3,7 +3,7 @@ import { describe, it, vi, beforeEach, afterEach, expect } from 'vitest' import { screen } from '@testing-library/react' import { when } from 'vitest-when' -import { NoParameters } from '@opentrons/components' +import { InfoScreen } from '@opentrons/components' import { renderWithProviders } from '../../../../__testing-utils__' import { i18n } from '../../../../i18n' import { useMostRecentCompletedAnalysis } from '../../../LabwarePositionCheck/useMostRecentCompletedAnalysis' @@ -16,10 +16,10 @@ import type { } from '@opentrons/shared-data' vi.mock('@opentrons/components', async importOriginal => { - const actual = await importOriginal() + const actual = await importOriginal() return { ...actual, - NoParameters: vi.fn(), + InfoScreen: vi.fn(), } }) vi.mock('../../../LabwarePositionCheck/useMostRecentCompletedAnalysis') @@ -94,7 +94,7 @@ describe('ProtocolRunRuntimeParameters', () => { props = { runId: RUN_ID, } - vi.mocked(NoParameters).mockReturnValue(
mock NoParameter
) + vi.mocked(InfoScreen).mockReturnValue(
mock InfoScreen
) when(vi.mocked(useMostRecentCompletedAnalysis)) .calledWith(RUN_ID) .thenReturn({ @@ -151,7 +151,7 @@ describe('ProtocolRunRuntimeParameters', () => { screen.getByText('No offsets') }) - it('should render mock NoParameter component when RunTimeParameters are empty', () => { + it('should render mock InfoScreen component when RunTimeParameters are empty', () => { when(vi.mocked(useMostRecentCompletedAnalysis)) .calledWith(RUN_ID) .thenReturn({ @@ -160,7 +160,7 @@ describe('ProtocolRunRuntimeParameters', () => { render(props) screen.getByText('Parameters') expect(screen.queryByText('Default values')).not.toBeInTheDocument() - screen.getByText('mock NoParameter') + screen.getByText('mock InfoScreen') }) // ToDo Additional test will be implemented when chip component is added diff --git a/app/src/organisms/ProtocolDetails/ProtocolParameters/index.tsx b/app/src/organisms/ProtocolDetails/ProtocolParameters/index.tsx index 69be8a3a468..797e18b930d 100644 --- a/app/src/organisms/ProtocolDetails/ProtocolParameters/index.tsx +++ b/app/src/organisms/ProtocolDetails/ProtocolParameters/index.tsx @@ -4,11 +4,11 @@ import { useTranslation } from 'react-i18next' import { DIRECTION_COLUMN, Flex, + InfoScreen, + ParametersTable, SPACING, StyledText, TYPOGRAPHY, - ParametersTable, - NoParameters, } from '@opentrons/components' import { Banner } from '../../../atoms/Banner' @@ -48,7 +48,7 @@ export function ProtocolParameters({
) : ( - + )}
) diff --git a/components/src/molecules/ParametersTable/NoParameters.tsx b/components/src/molecules/ParametersTable/InfoScreen.tsx similarity index 70% rename from components/src/molecules/ParametersTable/NoParameters.tsx rename to components/src/molecules/ParametersTable/InfoScreen.tsx index 27f9566b8cd..b9798f828e3 100644 --- a/components/src/molecules/ParametersTable/NoParameters.tsx +++ b/components/src/molecules/ParametersTable/InfoScreen.tsx @@ -7,7 +7,15 @@ import { Icon } from '../../icons' import { Flex } from '../../primitives' import { ALIGN_CENTER, DIRECTION_COLUMN } from '../../styles' -export function NoParameters(): JSX.Element { +interface InfoScreenProps { + contentType: 'parameters' | 'moduleControls' +} + +export function InfoScreen({ contentType }: InfoScreenProps): JSX.Element { + const bodyText = + contentType === 'parameters' + ? 'No parameters specified in this protocol' + : 'Connect modules to see controls' return ( - No parameters specified in this protocol + {bodyText} ) diff --git a/components/src/molecules/ParametersTable/__tests__/InfoScreen.test.tsx b/components/src/molecules/ParametersTable/__tests__/InfoScreen.test.tsx new file mode 100644 index 00000000000..a6f3b78a358 --- /dev/null +++ b/components/src/molecules/ParametersTable/__tests__/InfoScreen.test.tsx @@ -0,0 +1,49 @@ +import * as React from 'react' +import { screen } from '@testing-library/react' +import { describe, it, expect, beforeEach } from 'vitest' + +import { renderWithProviders } from '../../../testing/utils' +import { BORDERS, COLORS } from '../../../helix-design-system' +import { InfoScreen } from '../InfoScreen' + +const render = (props: React.ComponentProps) => { + return renderWithProviders() +} + +describe('InfoScreen', () => { + let props: React.ComponentProps + + beforeEach(() => { + props = { + contentType: 'parameters', + } + }) + + it('should render text and icon with proper color - parameters', () => { + render(props) + screen.getByLabelText('alert') + screen.getByText('No parameters specified in this protocol') + }) + + it('should render text and icon with proper color - module controls', () => { + props = { + contentType: 'moduleControls', + } + render(props) + screen.getByLabelText('alert') + screen.getByText('Connect modules to see controls') + }) + + it('should have proper styles', () => { + render(props) + expect(screen.getByTestId('InfoScreen_parameters')).toHaveStyle( + `background-color: ${COLORS.grey30}` + ) + expect(screen.getByTestId('InfoScreen_parameters')).toHaveStyle( + `border-radius: ${BORDERS.borderRadius8}` + ) + expect(screen.getByLabelText('alert')).toHaveStyle( + `color: ${COLORS.grey60}` + ) + }) +}) diff --git a/components/src/molecules/ParametersTable/__tests__/NoParameters.test.tsx b/components/src/molecules/ParametersTable/__tests__/NoParameters.test.tsx deleted file mode 100644 index 660a6936d51..00000000000 --- a/components/src/molecules/ParametersTable/__tests__/NoParameters.test.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import * as React from 'react' -import { screen } from '@testing-library/react' -import { describe, it, expect } from 'vitest' - -import { renderWithProviders } from '../../../testing/utils' -import { BORDERS, COLORS } from '../../../helix-design-system' -import { NoParameters } from '../NoParameters' - -const render = () => { - return renderWithProviders() -} - -describe('NoParameters', () => { - it('should render text and icon with proper color', () => { - render() - screen.getByLabelText('alert') - screen.getByText('No parameters specified in this protocol') - }) - - it('should have proper styles', () => { - render() - expect(screen.getByTestId('NoRunTimeParameter')).toHaveStyle( - `background-color: ${COLORS.grey30}` - ) - expect(screen.getByTestId('NoRunTimeParameter')).toHaveStyle( - `border-radius: ${BORDERS.borderRadius8}` - ) - expect(screen.getByLabelText('alert')).toHaveStyle( - `color: ${COLORS.grey60}` - ) - }) -}) diff --git a/components/src/molecules/index.tsx b/components/src/molecules/index.ts similarity index 66% rename from components/src/molecules/index.tsx rename to components/src/molecules/index.ts index 3231c2f93a9..cc7a1eacdbd 100644 --- a/components/src/molecules/index.tsx +++ b/components/src/molecules/index.ts @@ -1,4 +1,4 @@ export * from './LocationIcon' export * from './RoundTab' export * from './ParametersTable' -export * from './ParametersTable/NoParameters' +export * from './ParametersTable/InfoScreen' From 048a533163da56f76bea7502460592abad24b1da Mon Sep 17 00:00:00 2001 From: Jethary Rader <66035149+jerader@users.noreply.github.com> Date: Thu, 4 Apr 2024 09:49:31 -0400 Subject: [PATCH 035/194] feat(protocol-designer, step-generation): custom z offset for blowout (#14793) closes AUTH-7 --- .../protocol/8/doItAllV3MigratedToV8.json | 2 + .../protocol/8/doItAllV4MigratedToV8.json | 1 + .../protocol/8/doItAllV7MigratedToV8.json | 2 + .../fixtures/protocol/8/doItAllV8.json | 1 + .../protocol/8/example_1_1_0MigratedToV8.json | 4 +- .../fixtures/protocol/8/mix_8_0_0.json | 1 + .../8/ninetySixChannelFullAndColumn.json | 2 + .../fields/BlowoutLocationField.tsx | 4 +- .../fields/BlowoutZOffsetField.tsx | 80 +++++++++++++++++++ .../TipPositionInput.module.css | 1 - .../TipPositionField/TipPositionZAxisViz.tsx | 22 ++--- .../TipPositionField/ZTipPositionModal.tsx | 74 ++++++++++++----- .../__tests__/ZTipPositionModal.test.tsx | 50 ++++++++++++ .../fields/TipPositionField/index.tsx | 2 +- .../__tests__/BlowoutZOffsetField.test.tsx | 53 ++++++++++++ .../components/StepEditForm/fields/index.ts | 1 + .../components/StepEditForm/forms/MixForm.tsx | 6 ++ .../forms/MoveLiquidForm/SourceDestFields.tsx | 7 ++ protocol-designer/src/form-types.ts | 2 + .../src/load-file/migration/8_1_0.ts | 7 +- .../src/localization/en/modal.json | 2 + .../src/localization/en/tooltip.json | 13 ++- .../test/createPresavedStepForm.test.ts | 2 + .../utils/getProfileItemsHaveErrors.ts | 3 +- .../formLevel/getDefaultsForStepType.ts | 3 + .../getDisabledFieldsMixForm.ts | 10 +++ .../getDisabledFieldsMoveLiquidForm.ts | 16 ++++ .../formLevel/stepFormToArgs/mixFormToArgs.ts | 3 +- .../stepFormToArgs/moveLiquidFormToArgs.ts | 6 +- .../test/getDefaultsForStepType.test.ts | 2 + .../src/ui/steps/test/selectors.test.ts | 12 +++ step-generation/src/__tests__/blowout.test.ts | 34 +++++--- .../src/__tests__/blowoutUtil.test.ts | 36 ++++++--- .../src/__tests__/consolidate.test.ts | 16 ++-- .../src/__tests__/distribute.test.ts | 6 +- step-generation/src/__tests__/mix.test.ts | 6 +- .../src/__tests__/transfer.test.ts | 25 +++--- .../src/commandCreators/atomic/blowout.ts | 36 ++++----- step-generation/src/utils/misc.ts | 25 +++--- 39 files changed, 454 insertions(+), 124 deletions(-) create mode 100644 protocol-designer/src/components/StepEditForm/fields/BlowoutZOffsetField.tsx create mode 100644 protocol-designer/src/components/StepEditForm/fields/TipPositionField/__tests__/ZTipPositionModal.test.tsx create mode 100644 protocol-designer/src/components/StepEditForm/fields/__tests__/BlowoutZOffsetField.test.tsx diff --git a/protocol-designer/fixtures/protocol/8/doItAllV3MigratedToV8.json b/protocol-designer/fixtures/protocol/8/doItAllV3MigratedToV8.json index 340c594e596..e448368f932 100644 --- a/protocol-designer/fixtures/protocol/8/doItAllV3MigratedToV8.json +++ b/protocol-designer/fixtures/protocol/8/doItAllV3MigratedToV8.json @@ -101,6 +101,7 @@ "dispense_touchTip_mmFromBottom": 40, "disposalVolume_checkbox": true, "disposalVolume_volume": "20", + "blowout_z_offset": 0, "blowout_checkbox": false, "blowout_location": "8053a205-f2dc-4b1d-8d05-bf8233949e2e:trashBin", "preWetTip": false, @@ -157,6 +158,7 @@ "labware": "1e610d40-75c7-11ea-b42f-4b64e50f43e5:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1", "mix_wellOrder_first": "t2b", "mix_wellOrder_second": "l2r", + "blowout_z_offset": 0, "blowout_checkbox": true, "blowout_location": "8053a205-f2dc-4b1d-8d05-bf8233949e2e:trashBin", "mix_mmFromBottom": 0.5, diff --git a/protocol-designer/fixtures/protocol/8/doItAllV4MigratedToV8.json b/protocol-designer/fixtures/protocol/8/doItAllV4MigratedToV8.json index 1e87c78fe87..f8fec2171af 100644 --- a/protocol-designer/fixtures/protocol/8/doItAllV4MigratedToV8.json +++ b/protocol-designer/fixtures/protocol/8/doItAllV4MigratedToV8.json @@ -135,6 +135,7 @@ "dispense_touchTip_mmFromBottom": null, "disposalVolume_checkbox": true, "disposalVolume_volume": "20", + "blowout_z_offset": 0, "blowout_checkbox": false, "blowout_location": "84882326-9cd3-428e-8352-89f133a1fe5d:trashBin", "preWetTip": false, diff --git a/protocol-designer/fixtures/protocol/8/doItAllV7MigratedToV8.json b/protocol-designer/fixtures/protocol/8/doItAllV7MigratedToV8.json index 1d78ba01433..5519ec4f502 100644 --- a/protocol-designer/fixtures/protocol/8/doItAllV7MigratedToV8.json +++ b/protocol-designer/fixtures/protocol/8/doItAllV7MigratedToV8.json @@ -179,6 +179,7 @@ "dispense_touchTip_mmFromBottom": null, "disposalVolume_checkbox": true, "disposalVolume_volume": "100", + "blowout_z_offset": 0, "blowout_checkbox": false, "blowout_location": "4824b094-5999-4549-9e6b-7098a9b30a8b:trashBin", "preWetTip": false, @@ -209,6 +210,7 @@ "labware": "fcba73e7-b88e-438e-963e-f8b9a5de0983:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2", "mix_wellOrder_first": "t2b", "mix_wellOrder_second": "l2r", + "blowout_z_offset": 0, "blowout_checkbox": false, "blowout_location": "4824b094-5999-4549-9e6b-7098a9b30a8b:trashBin", "mix_mmFromBottom": 0.5, diff --git a/protocol-designer/fixtures/protocol/8/doItAllV8.json b/protocol-designer/fixtures/protocol/8/doItAllV8.json index a6b1f61a737..2a0e6bcde5d 100644 --- a/protocol-designer/fixtures/protocol/8/doItAllV8.json +++ b/protocol-designer/fixtures/protocol/8/doItAllV8.json @@ -158,6 +158,7 @@ "dispense_y_position": 0, "aspirate_x_position": 0, "aspirate_y_position": 0, + "blowout_z_offset": 0, "id": "d2f74144-a7bf-4ba2-aaab-30d70b2b62c7", "stepType": "moveLiquid", "stepName": "transfer", diff --git a/protocol-designer/fixtures/protocol/8/example_1_1_0MigratedToV8.json b/protocol-designer/fixtures/protocol/8/example_1_1_0MigratedToV8.json index ed550749d7a..56b9885aea9 100644 --- a/protocol-designer/fixtures/protocol/8/example_1_1_0MigratedToV8.json +++ b/protocol-designer/fixtures/protocol/8/example_1_1_0MigratedToV8.json @@ -114,6 +114,7 @@ "disposalVolume_checkbox": true, "disposalVolume_volume": "1", "blowout_checkbox": true, + "blowout_z_offset": 0, "blowout_location": "9b1c0d01-9d4f-4016-afe6-9e08b46acf5e:trashBin", "preWetTip": false, "aspirate_airGap_checkbox": false, @@ -143,6 +144,7 @@ "labware": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "mix_wellOrder_first": "t2b", "mix_wellOrder_second": "l2r", + "blowout_z_offset": 0, "blowout_checkbox": true, "blowout_location": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "mix_mmFromBottom": 0.5, @@ -5695,7 +5697,7 @@ "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", "flowRate": 7, - "wellLocation": { "origin": "bottom", "offset": { "z": 41.3 } } + "wellLocation": { "origin": "top", "offset": { "z": 0 } } } }, { diff --git a/protocol-designer/fixtures/protocol/8/mix_8_0_0.json b/protocol-designer/fixtures/protocol/8/mix_8_0_0.json index efa4b0ac6d6..6ace9e70926 100644 --- a/protocol-designer/fixtures/protocol/8/mix_8_0_0.json +++ b/protocol-designer/fixtures/protocol/8/mix_8_0_0.json @@ -58,6 +58,7 @@ "labware": null, "mix_wellOrder_first": "t2b", "mix_wellOrder_second": "l2r", + "blowout_z_offset": 0, "blowout_checkbox": false, "blowout_location": "5ba7047d-d3e2-4845-9eaa-1974af796ead:trashBin", "mix_mmFromBottom": 0.5, diff --git a/protocol-designer/fixtures/protocol/8/ninetySixChannelFullAndColumn.json b/protocol-designer/fixtures/protocol/8/ninetySixChannelFullAndColumn.json index 07384926f57..702945f0b8c 100644 --- a/protocol-designer/fixtures/protocol/8/ninetySixChannelFullAndColumn.json +++ b/protocol-designer/fixtures/protocol/8/ninetySixChannelFullAndColumn.json @@ -78,6 +78,7 @@ "dispense_touchTip_mmFromBottom": null, "disposalVolume_checkbox": true, "disposalVolume_volume": "5", + "blowout_z_offset": 0, "blowout_checkbox": false, "blowout_location": null, "preWetTip": false, @@ -133,6 +134,7 @@ "dispense_touchTip_mmFromBottom": null, "disposalVolume_checkbox": true, "disposalVolume_volume": "5", + "blowout_z_offset": 0, "blowout_checkbox": false, "blowout_location": null, "preWetTip": false, diff --git a/protocol-designer/src/components/StepEditForm/fields/BlowoutLocationField.tsx b/protocol-designer/src/components/StepEditForm/fields/BlowoutLocationField.tsx index 6e8f91d1ec2..6637092deab 100644 --- a/protocol-designer/src/components/StepEditForm/fields/BlowoutLocationField.tsx +++ b/protocol-designer/src/components/StepEditForm/fields/BlowoutLocationField.tsx @@ -7,8 +7,8 @@ import styles from '../StepEditForm.module.css' import { FieldProps } from '../types' type BlowoutLocationDropdownProps = FieldProps & { - className?: string options: Options + className?: string } export const BlowoutLocationField = ( @@ -28,7 +28,7 @@ export const BlowoutLocationField = ( return ( (false) + const [targetProps, tooltipProps] = useHoverTooltip() + const labwareEntities = useSelector(getLabwareEntities) + + let labwareId = null + if (blowoutLabwareId === SOURCE_WELL_BLOWOUT_DESTINATION) { + labwareId = sourceLabwareId + } else if (blowoutLabwareId === DEST_WELL_BLOWOUT_DESTINATION) { + labwareId = destLabwareId + } + + const labwareZDimension = + labwareId != null + ? labwareEntities[String(labwareId)]?.def.dimensions.zDimension + : 0 + + return ( + <> + {tooltipContent} + {isModalOpen ? ( + setModalOpen(false)} + name={name} + zValue={Number(value)} + updateValue={updateValue} + wellDepthMm={labwareZDimension} + /> + ) : null} + setModalOpen(true)} + id={`BlowoutZOffsetField_${name}`} + data-testid={`BlowoutZOffsetField_${name}`} + > + + + + ) +} diff --git a/protocol-designer/src/components/StepEditForm/fields/TipPositionField/TipPositionInput.module.css b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/TipPositionInput.module.css index 36818a42e4b..d7e6344e1ea 100644 --- a/protocol-designer/src/components/StepEditForm/fields/TipPositionField/TipPositionInput.module.css +++ b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/TipPositionInput.module.css @@ -56,7 +56,6 @@ font-weight: var(--fw-semibold); color: var(--c-blue); position: absolute; - right: 10px; bottom: 45px; align-self: flex-end; } diff --git a/protocol-designer/src/components/StepEditForm/fields/TipPositionField/TipPositionZAxisViz.tsx b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/TipPositionZAxisViz.tsx index 4b0dc3d512e..cff1fa05a9a 100644 --- a/protocol-designer/src/components/StepEditForm/fields/TipPositionField/TipPositionZAxisViz.tsx +++ b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/TipPositionZAxisViz.tsx @@ -8,19 +8,23 @@ import styles from './TipPositionInput.module.css' const WELL_HEIGHT_PIXELS = 145 const PIXEL_DECIMALS = 2 -interface Props { - mmFromBottom: number +interface TipPositionZAxisVizProps { wellDepthMm: number + mmFromBottom?: number + mmFromTop?: number } -export const TipPositionZAxisViz = (props: Props): JSX.Element => { - const fractionOfWellHeight = props.mmFromBottom / props.wellDepthMm +export function TipPositionZAxisViz( + props: TipPositionZAxisVizProps +): JSX.Element { + const { mmFromBottom, mmFromTop, wellDepthMm } = props + const positionInTube = mmFromBottom ?? mmFromTop ?? 0 + const fractionOfWellHeight = positionInTube / wellDepthMm const pixelsFromBottom = - Number(fractionOfWellHeight) * WELL_HEIGHT_PIXELS - WELL_HEIGHT_PIXELS - const roundedPixelsFromBottom = round(pixelsFromBottom, PIXEL_DECIMALS) - const bottomPx = props.wellDepthMm - ? roundedPixelsFromBottom - : props.mmFromBottom - WELL_HEIGHT_PIXELS + fractionOfWellHeight * WELL_HEIGHT_PIXELS - + (mmFromBottom != null ? WELL_HEIGHT_PIXELS : 0) + const bottomPx = round(pixelsFromBottom, PIXEL_DECIMALS) + return (
void - mmFromBottom: number | null + zValue: number | null name: StepFieldName updateValue: (val?: number | null) => unknown wellDepthMm: number @@ -36,21 +37,26 @@ export function ZTipPositionModal(props: ZTipPositionModalProps): JSX.Element { isIndeterminate, name, wellDepthMm, - mmFromBottom, + zValue, closeModal, updateValue, } = props const { t } = useTranslation(['modal', 'button']) - const defaultMmFromBottom = utils.getDefaultMmFromBottom({ - name, - wellDepthMm, - }) + + const isBlowout = name === 'blowout_z_offset' + const defaultMm = isBlowout + ? 0 + : utils.getDefaultMmFromBottom({ + name, + wellDepthMm, + }) const [value, setValue] = React.useState( - mmFromBottom === null ? null : String(mmFromBottom) + zValue !== null ? String(zValue) : null ) + const isSetDefault = isBlowout ? zValue === 0 : zValue === null const [isDefault, setIsDefault] = React.useState( - !isIndeterminate && mmFromBottom === null + !isIndeterminate && isSetDefault ) // in this modal, pristinity hides the OUT_OF_BOUNDS error only. const [isPristine, setPristine] = React.useState(true) @@ -71,20 +77,29 @@ export function ZTipPositionModal(props: ZTipPositionModalProps): JSX.Element { } } const { maxMmFromBottom, minMmFromBottom } = getMinMaxMmFromBottom() + + // For blowout from the top of the well + const minFromTop = DEFAULT_MM_BLOWOUT_OFFSET_FROM_TOP + const maxFromTop = -wellDepthMm + + const minMm = isBlowout ? maxFromTop : minMmFromBottom + const maxMm = isBlowout ? minFromTop : maxMmFromBottom + const errors = utils.getErrors({ isDefault, - minMm: minMmFromBottom, - maxMm: maxMmFromBottom, + minMm, + maxMm, value, }) const hasErrors = errors.length > 0 const hasVisibleErrors = isPristine ? errors.includes(TOO_MANY_DECIMALS) : hasErrors + const errorText = utils.getErrorText({ errors, - minMm: maxMmFromBottom, - maxMm: minMmFromBottom, + minMm, + maxMm, isPristine, t, }) @@ -110,13 +125,17 @@ export function ZTipPositionModal(props: ZTipPositionModalProps): JSX.Element { // if string, strip non-number characters from string and cast to number const newValue = typeof newValueRaw === 'string' - ? newValueRaw.replace(/[^.0-9]/, '') + ? newValueRaw.replace(/[^-.0-9]/, '') : String(newValueRaw) if (newValue === '.') { setValue('0.') + } else if (newValue === '-0') { + setValue('0') } else { - setValue(Number(newValue) >= 0 ? newValue : '0') + isBlowout + ? setValue(newValue) + : setValue(Number(newValue) >= 0 ? newValue : '0') } } @@ -127,7 +146,7 @@ export function ZTipPositionModal(props: ZTipPositionModalProps): JSX.Element { } const handleIncrementDecrement = (delta: number): void => { - const prevValue = value === null ? defaultMmFromBottom : Number(value) + const prevValue = value === null ? defaultMm : Number(value) setIsDefault(false) handleChange(utils.roundValue(prevValue + delta)) } @@ -143,8 +162,8 @@ export function ZTipPositionModal(props: ZTipPositionModalProps): JSX.Element { const TipPositionInputField = !isDefault && ( diff --git a/protocol-designer/src/components/StepEditForm/fields/TipPositionField/__tests__/ZTipPositionModal.test.tsx b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/__tests__/ZTipPositionModal.test.tsx new file mode 100644 index 00000000000..015d5437dbb --- /dev/null +++ b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/__tests__/ZTipPositionModal.test.tsx @@ -0,0 +1,50 @@ +import * as React from 'react' +import { fireEvent, screen } from '@testing-library/react' +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { renderWithProviders } from '../../../../../__testing-utils__' +import { i18n } from '../../../../../localization' +import { ZTipPositionModal } from '../ZTipPositionModal' +import { TipPositionZAxisViz } from '../TipPositionZAxisViz' + +vi.mock('../TipPositionZAxisViz') +const render = (props: React.ComponentProps) => { + return renderWithProviders(, { + i18nInstance: i18n, + })[0] +} + +describe('ZTipPositionModal', () => { + let props: React.ComponentProps + + beforeEach(() => { + props = { + closeModal: vi.fn(), + zValue: -2, + updateValue: vi.fn(), + wellDepthMm: 30, + name: 'blowout_z_offset', + } + vi.mocked(TipPositionZAxisViz).mockReturnValue( +
mock TipPositionZAxisViz
+ ) + }) + it('renders the text and radio buttons', () => { + render(props) + screen.getByText('Tip Positioning') + screen.getByText('Change from where in the well the robot emits blowout') + screen.getByRole('radio', { name: '0 mm from the top center (default)' }) + screen.getByRole('radio', { name: 'Custom' }) + fireEvent.click(screen.getByText('cancel')) + expect(props.closeModal).toHaveBeenCalled() + fireEvent.click(screen.getByText('done')) + expect(props.closeModal).toHaveBeenCalled() + expect(props.updateValue).toHaveBeenCalled() + }) + it('renders the custom option, caption, and visual', () => { + render(props) + fireEvent.click(screen.getByRole('radio', { name: 'Custom' })) + expect(screen.getAllByRole('textbox', { name: '' })).toHaveLength(1) + screen.getByText('between -30 and 0') + screen.getByText('mock TipPositionZAxisViz') + }) +}) diff --git a/protocol-designer/src/components/StepEditForm/fields/TipPositionField/index.tsx b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/index.tsx index 91ececa71c8..5f60d13cd79 100644 --- a/protocol-designer/src/components/StepEditForm/fields/TipPositionField/index.tsx +++ b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/index.tsx @@ -109,7 +109,7 @@ export function TipPositionField(props: TipPositionFieldProps): JSX.Element { name={zName} closeModal={handleClose} wellDepthMm={wellDepthMm} - mmFromBottom={mmFromBottom} + zValue={mmFromBottom} updateValue={zUpdateValue} isIndeterminate={isIndeterminate} /> diff --git a/protocol-designer/src/components/StepEditForm/fields/__tests__/BlowoutZOffsetField.test.tsx b/protocol-designer/src/components/StepEditForm/fields/__tests__/BlowoutZOffsetField.test.tsx new file mode 100644 index 00000000000..fec53a25ac4 --- /dev/null +++ b/protocol-designer/src/components/StepEditForm/fields/__tests__/BlowoutZOffsetField.test.tsx @@ -0,0 +1,53 @@ +import * as React from 'react' +import { describe, it, vi, beforeEach } from 'vitest' +import { fireEvent, screen } from '@testing-library/react' +import { fixture96Plate } from '@opentrons/shared-data' +import { SOURCE_WELL_BLOWOUT_DESTINATION } from '@opentrons/step-generation' +import { getLabwareEntities } from '../../../../step-forms/selectors' +import { renderWithProviders } from '../../../../__testing-utils__' +import { ZTipPositionModal } from '../TipPositionField/ZTipPositionModal' +import { BlowoutZOffsetField } from '../BlowoutZOffsetField' +import type { LabwareDefinition2 } from '@opentrons/shared-data' + +vi.mock('../../../../step-forms/selectors') +vi.mock('../TipPositionField/ZTipPositionModal') +const render = (props: React.ComponentProps) => { + return renderWithProviders()[0] +} +const mockSourceId = 'sourceId' +describe('BlowoutZOffsetField', () => { + let props: React.ComponentProps + + beforeEach(() => { + props = { + disabled: false, + value: null, + name: 'blowout_z_offset', + updateValue: vi.fn(), + onFieldBlur: vi.fn(), + onFieldFocus: vi.fn(), + destLabwareId: SOURCE_WELL_BLOWOUT_DESTINATION, + sourceLabwareId: mockSourceId, + blowoutLabwareId: 'blowoutId', + } + vi.mocked(getLabwareEntities).mockReturnValue({ + [mockSourceId]: { + id: 'mockLabwareId', + labwareDefURI: 'mock uri', + def: fixture96Plate as LabwareDefinition2, + }, + }) + vi.mocked(ZTipPositionModal).mockReturnValue( +
mock ZTipPositionModal
+ ) + }) + it('renders the input field', () => { + render(props) + screen.getByTestId('BlowoutZOffsetField_blowout_z_offset') + }) + it('renders the modal when input field is clicked on', () => { + render(props) + fireEvent.click(screen.getByTestId('BlowoutZOffsetField_blowout_z_offset')) + screen.getByText('mock ZTipPositionModal') + }) +}) diff --git a/protocol-designer/src/components/StepEditForm/fields/index.ts b/protocol-designer/src/components/StepEditForm/fields/index.ts index 15d7f4bb21f..70d10ffa616 100644 --- a/protocol-designer/src/components/StepEditForm/fields/index.ts +++ b/protocol-designer/src/components/StepEditForm/fields/index.ts @@ -7,6 +7,7 @@ export { TextField } from './TextField' /* Specialized Fields */ export { BlowoutLocationField } from './BlowoutLocationField' +export { BlowoutZOffsetField } from './BlowoutZOffsetField' export { ChangeTipField } from './ChangeTipField' export { DelayFields } from './DelayFields' export { DisposalVolumeField } from './DisposalVolumeField' diff --git a/protocol-designer/src/components/StepEditForm/forms/MixForm.tsx b/protocol-designer/src/components/StepEditForm/forms/MixForm.tsx index 7b5f8fb9503..ef1b408cfe4 100644 --- a/protocol-designer/src/components/StepEditForm/forms/MixForm.tsx +++ b/protocol-designer/src/components/StepEditForm/forms/MixForm.tsx @@ -17,6 +17,7 @@ import { VolumeField, WellOrderField, WellSelectionField, + BlowoutZOffsetField, } from '../fields' import { TiprackField } from '../fields/TiprackField' import { @@ -209,6 +210,11 @@ export const MixForm = (props: StepFormProps): JSX.Element => { stepType: formData.stepType, })} /> +
diff --git a/protocol-designer/src/components/StepEditForm/forms/MoveLiquidForm/SourceDestFields.tsx b/protocol-designer/src/components/StepEditForm/forms/MoveLiquidForm/SourceDestFields.tsx index 4797375d0dd..eadd4fad2a9 100644 --- a/protocol-designer/src/components/StepEditForm/forms/MoveLiquidForm/SourceDestFields.tsx +++ b/protocol-designer/src/components/StepEditForm/forms/MoveLiquidForm/SourceDestFields.tsx @@ -11,6 +11,7 @@ import { TextField, TipPositionField, WellOrderField, + BlowoutZOffsetField, } from '../../fields' import { MixFields } from '../../fields/MixFields' import { @@ -176,6 +177,12 @@ export const SourceDestFields = (props: SourceDestFieldsProps): JSX.Element => { stepType: formData.stepType, })} /> + )} { aspirate_y_position: 0, dispense_x_position: 0, dispense_y_position: 0, + blowout_z_offset: 0, }) }) describe('mix step', () => { @@ -216,6 +217,7 @@ describe('createPresavedStepForm', () => { blowout_checkbox: false, mix_x_position: 0, mix_y_position: 0, + blowout_z_offset: 0, blowout_location: null, changeTip: 'always', stepDetails: '', diff --git a/protocol-designer/src/step-forms/utils/getProfileItemsHaveErrors.ts b/protocol-designer/src/step-forms/utils/getProfileItemsHaveErrors.ts index 68e2f151172..6b5fc39fbad 100644 --- a/protocol-designer/src/step-forms/utils/getProfileItemsHaveErrors.ts +++ b/protocol-designer/src/step-forms/utils/getProfileItemsHaveErrors.ts @@ -1,5 +1,6 @@ import { getProfileFieldErrors } from '../../steplist/fieldLevel' -import { ProfileItem, PROFILE_CYCLE } from '../../form-types' +import { PROFILE_CYCLE } from '../../form-types' +import type { ProfileItem } from '../../form-types' const _someFieldsHaveErrors = (item: ProfileItem): boolean => { for (const fieldName in item) { diff --git a/protocol-designer/src/steplist/formLevel/getDefaultsForStepType.ts b/protocol-designer/src/steplist/formLevel/getDefaultsForStepType.ts index 25442fac9af..b90eb6f028e 100644 --- a/protocol-designer/src/steplist/formLevel/getDefaultsForStepType.ts +++ b/protocol-designer/src/steplist/formLevel/getDefaultsForStepType.ts @@ -4,6 +4,7 @@ import { DEFAULT_WELL_ORDER_FIRST_OPTION, DEFAULT_WELL_ORDER_SECOND_OPTION, DEFAULT_DELAY_SECONDS, + DEFAULT_MM_BLOWOUT_OFFSET_FROM_TOP, } from '../../constants' import { StepType, StepFieldName } from '../../form-types' export function getDefaultsForStepType( @@ -39,6 +40,7 @@ export function getDefaultsForStepType( tipRack: null, mix_x_position: 0, mix_y_position: 0, + blowout_z_offset: DEFAULT_MM_BLOWOUT_OFFSET_FROM_TOP, } case 'moveLiquid': @@ -92,6 +94,7 @@ export function getDefaultsForStepType( dispense_y_position: 0, aspirate_x_position: 0, aspirate_y_position: 0, + blowout_z_offset: DEFAULT_MM_BLOWOUT_OFFSET_FROM_TOP, } case 'moveLabware': diff --git a/protocol-designer/src/steplist/formLevel/getDisabledFields/getDisabledFieldsMixForm.ts b/protocol-designer/src/steplist/formLevel/getDisabledFields/getDisabledFieldsMixForm.ts index 16765d26436..d480b455666 100644 --- a/protocol-designer/src/steplist/formLevel/getDisabledFields/getDisabledFieldsMixForm.ts +++ b/protocol-designer/src/steplist/formLevel/getDisabledFields/getDisabledFieldsMixForm.ts @@ -1,3 +1,4 @@ +import { DEST_WELL_BLOWOUT_DESTINATION } from '@opentrons/step-generation' import type { HydratedFormdata } from '../../../form-types' // NOTE: expects that '_checkbox' fields are implemented so that // when checkbox is disabled, its dependent fields are hidden @@ -21,5 +22,14 @@ export function getDisabledFieldsMixForm( disabled.add('mix_touchTip_checkbox') } + if ( + !hydratedForm.blowout_location || + hydratedForm.blowout_location.includes('wasteChute') || + hydratedForm.blowout_location.includes('trashBin') || + (hydratedForm.blowout_location === DEST_WELL_BLOWOUT_DESTINATION && + !hydratedForm.labware) + ) { + disabled.add('blowout_z_offset') + } return disabled } diff --git a/protocol-designer/src/steplist/formLevel/getDisabledFields/getDisabledFieldsMoveLiquidForm.ts b/protocol-designer/src/steplist/formLevel/getDisabledFields/getDisabledFieldsMoveLiquidForm.ts index ec514c81cce..5ca7db1395f 100644 --- a/protocol-designer/src/steplist/formLevel/getDisabledFields/getDisabledFieldsMoveLiquidForm.ts +++ b/protocol-designer/src/steplist/formLevel/getDisabledFields/getDisabledFieldsMoveLiquidForm.ts @@ -1,3 +1,7 @@ +import { + DEST_WELL_BLOWOUT_DESTINATION, + SOURCE_WELL_BLOWOUT_DESTINATION, +} from '@opentrons/step-generation' import type { HydratedFormdata } from '../../../form-types' // NOTE: expects that '_checkbox' fields are implemented so that // when checkbox is disabled, its dependent fields are hidden @@ -37,5 +41,17 @@ export function getDisabledFieldsMoveLiquidForm( disabled.add(prefix + '_wells') } }) + + if ( + !hydratedForm.blowout_location || + hydratedForm.blowout_location.includes('wasteChute') || + hydratedForm.blowout_location.includes('trashBin') || + (hydratedForm.blowout_location === SOURCE_WELL_BLOWOUT_DESTINATION && + !hydratedForm.aspirate_labware) || + (hydratedForm.blowout_location === DEST_WELL_BLOWOUT_DESTINATION && + !hydratedForm.dispense_labware) + ) { + disabled.add('blowout_z_offset') + } return disabled } diff --git a/protocol-designer/src/steplist/formLevel/stepFormToArgs/mixFormToArgs.ts b/protocol-designer/src/steplist/formLevel/stepFormToArgs/mixFormToArgs.ts index d9d4936b71e..d28f6dc42df 100644 --- a/protocol-designer/src/steplist/formLevel/stepFormToArgs/mixFormToArgs.ts +++ b/protocol-designer/src/steplist/formLevel/stepFormToArgs/mixFormToArgs.ts @@ -22,6 +22,7 @@ export const mixFormToArgs = ( nozzles, mix_x_position, mix_y_position, + blowout_z_offset, } = hydratedFormData const matchingTipLiquidSpecs = getMatchingTipLiquidSpecs( pipette, @@ -73,7 +74,7 @@ export const mixFormToArgs = ( matchingTipLiquidSpecs?.defaultBlowOutFlowRate.default const blowoutOffsetFromTopMm = blowoutLocation - ? DEFAULT_MM_BLOWOUT_OFFSET_FROM_TOP + ? blowout_z_offset ?? DEFAULT_MM_BLOWOUT_OFFSET_FROM_TOP : 0 // Delay settings const aspirateDelaySeconds = getMixDelayData( diff --git a/protocol-designer/src/steplist/formLevel/stepFormToArgs/moveLiquidFormToArgs.ts b/protocol-designer/src/steplist/formLevel/stepFormToArgs/moveLiquidFormToArgs.ts index 4b3023fdad3..05910f13332 100644 --- a/protocol-designer/src/steplist/formLevel/stepFormToArgs/moveLiquidFormToArgs.ts +++ b/protocol-designer/src/steplist/formLevel/stepFormToArgs/moveLiquidFormToArgs.ts @@ -82,6 +82,7 @@ export const moveLiquidFormToArgs = ( dispense_x_position, aspirate_y_position, dispense_y_position, + blowout_z_offset, } = fields let sourceWells = getOrderedWells( fields.aspirate_wells, @@ -165,7 +166,10 @@ export const moveLiquidFormToArgs = ( ) const blowoutLocation = (fields.blowout_checkbox && fields.blowout_location) || null - const blowoutOffsetFromTopMm = DEFAULT_MM_BLOWOUT_OFFSET_FROM_TOP + const blowoutOffsetFromTopMm = + blowoutLocation != null + ? blowout_z_offset ?? DEFAULT_MM_BLOWOUT_OFFSET_FROM_TOP + : DEFAULT_MM_BLOWOUT_OFFSET_FROM_TOP const aspirateAirGapVolume = getAirGapData( fields, 'aspirate_airGap_checkbox', diff --git a/protocol-designer/src/steplist/formLevel/test/getDefaultsForStepType.test.ts b/protocol-designer/src/steplist/formLevel/test/getDefaultsForStepType.test.ts index cf0b72b84b0..081d7809566 100644 --- a/protocol-designer/src/steplist/formLevel/test/getDefaultsForStepType.test.ts +++ b/protocol-designer/src/steplist/formLevel/test/getDefaultsForStepType.test.ts @@ -69,6 +69,7 @@ describe('getDefaultsForStepType', () => { tipRack: null, dispense_x_position: 0, dispense_y_position: 0, + blowout_z_offset: 0, }) }) }) @@ -99,6 +100,7 @@ describe('getDefaultsForStepType', () => { tipRack: null, mix_x_position: 0, mix_y_position: 0, + blowout_z_offset: 0, }) }) }) diff --git a/protocol-designer/src/ui/steps/test/selectors.test.ts b/protocol-designer/src/ui/steps/test/selectors.test.ts index 7cfa25c5e22..e5aa13d10c5 100644 --- a/protocol-designer/src/ui/steps/test/selectors.test.ts +++ b/protocol-designer/src/ui/steps/test/selectors.test.ts @@ -435,6 +435,9 @@ describe('_getSavedMultiSelectFieldValues', () => { dispense_y_position: { isIndeterminate: false, }, + blowout_z_offset: { + isIndeterminate: false, + }, aspirate_wells: { isIndeterminate: true, }, @@ -694,6 +697,9 @@ describe('_getSavedMultiSelectFieldValues', () => { dispense_y_position: { isIndeterminate: false, }, + blowout_z_offset: { + isIndeterminate: false, + }, preWetTip: { isIndeterminate: true, }, @@ -881,6 +887,9 @@ describe('_getSavedMultiSelectFieldValues', () => { mix_y_position: { isIndeterminate: false, }, + blowout_z_offset: { + isIndeterminate: false, + }, dropTip_location: { value: 'fixedTrash', isIndeterminate: false, @@ -957,6 +966,9 @@ describe('_getSavedMultiSelectFieldValues', () => { mix_y_position: { isIndeterminate: false, }, + blowout_z_offset: { + isIndeterminate: false, + }, dropTip_location: { value: 'fixedTrash', isIndeterminate: false, diff --git a/step-generation/src/__tests__/blowout.test.ts b/step-generation/src/__tests__/blowout.test.ts index c52cac83042..8e16cafb331 100644 --- a/step-generation/src/__tests__/blowout.test.ts +++ b/step-generation/src/__tests__/blowout.test.ts @@ -11,7 +11,7 @@ import { DEFAULT_PIPETTE, SOURCE_LABWARE, } from '../fixtures' -import { BlowoutParams } from '@opentrons/shared-data/protocol/types/schemaV3' +import type { BlowoutParams } from '@opentrons/shared-data' import type { RobotState, InvariantContext } from '../types' describe('blowout', () => { @@ -24,11 +24,15 @@ describe('blowout', () => { initialRobotState = getInitialRobotStateStandard(invariantContext) robotStateWithTip = getRobotStateWithTipStandard(invariantContext) params = { - pipette: DEFAULT_PIPETTE, - labware: SOURCE_LABWARE, - well: 'A1', + pipetteId: DEFAULT_PIPETTE, + labwareId: SOURCE_LABWARE, + wellName: 'A1', flowRate: 21.1, - offsetFromBottomMm: 1.3, + wellLocation: { + offset: { + z: -1.3, + }, + }, } }) it('blowout with tip', () => { @@ -44,9 +48,9 @@ describe('blowout', () => { wellName: 'A1', flowRate: 21.1, wellLocation: { - origin: 'bottom', + origin: 'top', offset: { - z: 1.3, + z: -1.3, }, }, }, @@ -55,7 +59,7 @@ describe('blowout', () => { }) it('blowout with invalid pipette ID should throw error', () => { const result = blowout( - { ...params, pipette: 'badPipette' }, + { ...params, pipetteId: 'badPipette' }, invariantContext, robotStateWithTip ) @@ -63,7 +67,7 @@ describe('blowout', () => { }) it('blowout with invalid labware ID should throw error', () => { const result = blowout( - { ...params, labware: 'badLabware' }, + { ...params, labwareId: 'badLabware' }, invariantContext, robotStateWithTip ) @@ -88,11 +92,15 @@ describe('blowout', () => { const result = blowout( { flowRate: 10, - offsetFromBottomMm: 5, - pipette: DEFAULT_PIPETTE, + wellLocation: { + offset: { + z: -3, + }, + }, + pipetteId: DEFAULT_PIPETTE, volume: 50, - labware: SOURCE_LABWARE, - well: 'A1', + labwareId: SOURCE_LABWARE, + wellName: 'A1', } as BlowoutParams, invariantContext, initialRobotState diff --git a/step-generation/src/__tests__/blowoutUtil.test.ts b/step-generation/src/__tests__/blowoutUtil.test.ts index 33ff3770567..ac2a1c1cd87 100644 --- a/step-generation/src/__tests__/blowoutUtil.test.ts +++ b/step-generation/src/__tests__/blowoutUtil.test.ts @@ -63,11 +63,15 @@ describe('blowoutUtil', () => { blowoutLocation: SOURCE_WELL_BLOWOUT_DESTINATION, }) expect(curryCommandCreator).toHaveBeenCalledWith(blowout, { - pipette: blowoutArgs.pipette, - labware: blowoutArgs.sourceLabwareId, - well: blowoutArgs.sourceWell, + pipetteId: blowoutArgs.pipette, + labwareId: blowoutArgs.sourceLabwareId, + wellName: blowoutArgs.sourceWell, flowRate: blowoutArgs.flowRate, - offsetFromBottomMm: expect.any(Number), + wellLocation: { + offset: { + z: expect.any(Number), + }, + }, }) }) it('blowoutUtil curries waste chute commands when there is no well', () => { @@ -104,11 +108,15 @@ describe('blowoutUtil', () => { blowoutLocation: DEST_WELL_BLOWOUT_DESTINATION, }) expect(curryCommandCreator).toHaveBeenCalledWith(blowout, { - pipette: blowoutArgs.pipette, - labware: blowoutArgs.destLabwareId, - well: blowoutArgs.destWell, + pipetteId: blowoutArgs.pipette, + labwareId: blowoutArgs.destLabwareId, + wellName: blowoutArgs.destWell, flowRate: blowoutArgs.flowRate, - offsetFromBottomMm: expect.any(Number), + wellLocation: { + offset: { + z: expect.any(Number), + }, + }, }) }) it('blowoutUtil curries blowout with an arbitrary labware Id', () => { @@ -117,11 +125,15 @@ describe('blowoutUtil', () => { blowoutLocation: TROUGH_LABWARE, }) expect(curryCommandCreator).toHaveBeenCalledWith(blowout, { - pipette: blowoutArgs.pipette, - labware: TROUGH_LABWARE, - well: 'A1', + pipetteId: blowoutArgs.pipette, + labwareId: TROUGH_LABWARE, + wellName: 'A1', flowRate: blowoutArgs.flowRate, - offsetFromBottomMm: expect.any(Number), + wellLocation: { + offset: { + z: expect.any(Number), + }, + }, }) }) it('blowoutUtil returns an empty array if not given a blowoutLocation', () => { diff --git a/step-generation/src/__tests__/consolidate.test.ts b/step-generation/src/__tests__/consolidate.test.ts index db0303605af..11b20e65267 100644 --- a/step-generation/src/__tests__/consolidate.test.ts +++ b/step-generation/src/__tests__/consolidate.test.ts @@ -2103,9 +2103,9 @@ describe('consolidate single-channel', () => { wellName: 'B1', flowRate: 2.3, wellLocation: { - origin: 'bottom', + origin: 'top', offset: { - z: 13.84, + z: 3.3, }, }, }, @@ -2378,9 +2378,9 @@ describe('consolidate single-channel', () => { wellName: 'B1', flowRate: 2.3, wellLocation: { - origin: 'bottom', + origin: 'top', offset: { - z: 13.84, + z: 3.3, }, }, }, @@ -2805,9 +2805,9 @@ describe('consolidate single-channel', () => { wellName: 'B1', flowRate: 2.3, wellLocation: { - origin: 'bottom', + origin: 'top', offset: { - z: 13.84, + z: 3.3, }, }, }, @@ -3117,9 +3117,9 @@ describe('consolidate single-channel', () => { wellName: 'B1', flowRate: 2.3, wellLocation: { - origin: 'bottom', + origin: 'top', offset: { - z: 13.84, + z: 3.3, }, }, }, diff --git a/step-generation/src/__tests__/distribute.test.ts b/step-generation/src/__tests__/distribute.test.ts index 3e8fa31f749..6793b9df81e 100644 --- a/step-generation/src/__tests__/distribute.test.ts +++ b/step-generation/src/__tests__/distribute.test.ts @@ -96,7 +96,7 @@ beforeEach(() => { blowoutSingleToTrash = blowoutInPlaceHelper() blowoutSingleToSourceA1 = blowoutHelper(SOURCE_LABWARE, { wellLocation: { - origin: 'bottom', + origin: 'top', offset: { z: BLOWOUT_OFFSET_ANY, }, @@ -104,7 +104,7 @@ beforeEach(() => { }) blowoutSingleToDestA4 = blowoutHelper(DEST_LABWARE, { wellLocation: { - origin: 'bottom', + origin: 'top', offset: { z: BLOWOUT_OFFSET_ANY, }, @@ -113,7 +113,7 @@ beforeEach(() => { }) blowoutSingleToDestA3 = blowoutHelper(DEST_LABWARE, { wellLocation: { - origin: 'bottom', + origin: 'top', offset: { z: BLOWOUT_OFFSET_ANY, }, diff --git a/step-generation/src/__tests__/mix.test.ts b/step-generation/src/__tests__/mix.test.ts index cc2115c42da..9fd099a5388 100644 --- a/step-generation/src/__tests__/mix.test.ts +++ b/step-generation/src/__tests__/mix.test.ts @@ -195,7 +195,7 @@ describe('mix: advanced options', () => { dispenseHelper(well, volume), blowoutHelper(blowoutLabwareId, { wellLocation: { - origin: 'bottom', + origin: 'top', offset: { z: BLOWOUT_OFFSET_ANY, }, @@ -229,7 +229,7 @@ describe('mix: advanced options', () => { dispenseHelper(well, volume), blowoutHelper(blowoutLabwareId, { wellLocation: { - origin: 'bottom', + origin: 'top', offset: { z: BLOWOUT_OFFSET_ANY, }, @@ -319,7 +319,7 @@ describe('mix: advanced options', () => { delayCommand(12), blowoutHelper(blowoutLabwareId, { wellLocation: { - origin: 'bottom', + origin: 'top', offset: { z: BLOWOUT_OFFSET_ANY, }, diff --git a/step-generation/src/__tests__/transfer.test.ts b/step-generation/src/__tests__/transfer.test.ts index f0c9b9fce7e..b3da39db41d 100644 --- a/step-generation/src/__tests__/transfer.test.ts +++ b/step-generation/src/__tests__/transfer.test.ts @@ -1461,6 +1461,7 @@ describe('advanced options', () => { key: expect.any(String), params: { pipetteId: 'p300SingleId', + labwareId: 'destPlateId', wellName: 'B1', wellLocation: { @@ -2142,9 +2143,9 @@ describe('advanced options', () => { wellName: 'B1', flowRate: 2.3, wellLocation: { - origin: 'bottom', + origin: 'top', offset: { - z: 13.84, + z: 3.3, }, }, }, @@ -2443,9 +2444,9 @@ describe('advanced options', () => { wellName: 'B1', flowRate: 2.3, wellLocation: { - origin: 'bottom', + origin: 'top', offset: { - z: 13.84, + z: 3.3, }, }, }, @@ -2866,9 +2867,9 @@ describe('advanced options', () => { wellName: 'B1', flowRate: 2.3, wellLocation: { - origin: 'bottom', + origin: 'top', offset: { - z: 13.84, + z: 3.3, }, }, }, @@ -3168,9 +3169,9 @@ describe('advanced options', () => { wellName: 'B1', flowRate: 2.3, wellLocation: { - origin: 'bottom', + origin: 'top', offset: { - z: 13.84, + z: 3.3, }, }, }, @@ -3589,9 +3590,9 @@ describe('advanced options', () => { wellName: 'A1', flowRate: 2.3, wellLocation: { - origin: 'bottom', + origin: 'top', offset: { - z: 13.84, + z: 3.3, }, }, }, @@ -3942,9 +3943,9 @@ describe('advanced options', () => { wellName: 'A1', flowRate: 2.3, wellLocation: { - origin: 'bottom', + origin: 'top', offset: { - z: 13.84, + z: 3.3, }, }, }, diff --git a/step-generation/src/commandCreators/atomic/blowout.ts b/step-generation/src/commandCreators/atomic/blowout.ts index 497257a98d6..ff3be46d786 100644 --- a/step-generation/src/commandCreators/atomic/blowout.ts +++ b/step-generation/src/commandCreators/atomic/blowout.ts @@ -1,8 +1,7 @@ import { uuid, getLabwareSlot } from '../../utils' import { COLUMN_4_SLOTS } from '../../constants' import * as errorCreators from '../../errorCreators' -import type { CreateCommand } from '@opentrons/shared-data' -import type { BlowoutParams } from '@opentrons/shared-data/protocol/types/schemaV3' +import type { CreateCommand, BlowoutParams } from '@opentrons/shared-data' import type { CommandCreatorError, CommandCreator } from '../../types' export const blowout: CommandCreator = ( @@ -11,12 +10,13 @@ export const blowout: CommandCreator = ( prevRobotState ) => { /** Blowout with given args. Requires tip. */ - const { pipette, labware, well, offsetFromBottomMm, flowRate } = args + const { pipetteId, labwareId, wellName, wellLocation, flowRate } = args + const actionName = 'blowout' const errors: CommandCreatorError[] = [] - const pipetteData = prevRobotState.pipettes[pipette] + const pipetteData = prevRobotState.pipettes[pipetteId] const slotName = getLabwareSlot( - labware, + labwareId, prevRobotState.labware, prevRobotState.modules ) @@ -27,30 +27,30 @@ export const blowout: CommandCreator = ( errors.push( errorCreators.pipetteDoesNotExist({ actionName, - pipette, + pipette: pipetteId, }) ) } - if (!prevRobotState.tipState.pipettes[pipette]) { + if (!prevRobotState.tipState.pipettes[pipetteId]) { errors.push( errorCreators.noTipOnPipette({ actionName, - pipette, - labware, - well, + pipette: pipetteId, + labware: labwareId, + well: wellName, }) ) } - if (!labware || !prevRobotState.labware[labware]) { + if (!labwareId || !prevRobotState.labware[labwareId]) { errors.push( errorCreators.labwareDoesNotExist({ actionName, - labware, + labware: labwareId, }) ) - } else if (prevRobotState.labware[labware]?.slot === 'offDeck') { + } else if (prevRobotState.labware[labwareId]?.slot === 'offDeck') { errors.push(errorCreators.labwareOffDeck()) } @@ -69,14 +69,14 @@ export const blowout: CommandCreator = ( commandType: 'blowout', key: uuid(), params: { - pipetteId: pipette, - labwareId: labware, - wellName: well, + pipetteId, + labwareId, + wellName, flowRate, wellLocation: { - origin: 'bottom', + origin: 'top', offset: { - z: offsetFromBottomMm, + z: wellLocation?.offset?.z, }, }, }, diff --git a/step-generation/src/utils/misc.ts b/step-generation/src/utils/misc.ts index 58bf2e9f782..77d91213d63 100644 --- a/step-generation/src/utils/misc.ts +++ b/step-generation/src/utils/misc.ts @@ -5,7 +5,6 @@ import reduce from 'lodash/reduce' import { getIsTiprack, getLabwareDefURI, - getWellsDepth, getWellNamePerMultiTip, WASTE_CHUTE_CUTOUT, PipetteChannels, @@ -26,8 +25,8 @@ import { movableTrashCommandsUtil } from './movableTrashCommandsUtil' import type { AddressableAreaName, LabwareDefinition2, + BlowoutParams, } from '@opentrons/shared-data' -import type { BlowoutParams } from '@opentrons/shared-data/protocol/types/schemaV4' import type { AdditionalEquipmentEntities, AdditionalEquipmentEntity, @@ -244,15 +243,15 @@ export function getWellsForTips( // the SOURCE_WELL_BLOWOUT_DESTINATION / DEST_WELL_BLOWOUT_DESTINATION // special strings, or to a labware ID. export const blowoutUtil = (args: { - pipette: BlowoutParams['pipette'] + pipette: BlowoutParams['pipetteId'] sourceLabwareId: string - sourceWell: BlowoutParams['well'] + sourceWell: BlowoutParams['wellName'] destLabwareId: string blowoutLocation: string | null | undefined flowRate: number offsetFromTopMm: number invariantContext: InvariantContext - destWell: BlowoutParams['well'] | null + destWell: BlowoutParams['wellName'] | null prevRobotState: RobotState }): CurriedCommandCreator[] => { const { @@ -293,18 +292,18 @@ export const blowoutUtil = (args: { well = trashOrLabware === 'labware' ? 'A1' : null } - const wellDepth = - labware != null && well != null ? getWellsDepth(labware.def, [well]) : 0 - - const offsetFromBottomMm = wellDepth + offsetFromTopMm if (well != null && trashOrLabware === 'labware' && labware != null) { return [ curryCommandCreator(blowout, { - pipette: pipette, - labware: labware.id, - well, + pipetteId: pipette, + labwareId: labware.id, + wellName: well, flowRate, - offsetFromBottomMm, + wellLocation: { + offset: { + z: offsetFromTopMm, + }, + }, }), ] } else if (trashOrLabware === 'wasteChute') { From c0700c8c0235d32ee02cd84ed2891d21390b1e51 Mon Sep 17 00:00:00 2001 From: Rhyann Clarke <146747548+rclarke0@users.noreply.github.com> Date: Thu, 4 Apr 2024 09:57:50 -0400 Subject: [PATCH 036/194] added functions to count module commands per run (#14797) # Overview Functions to Count Module commands per run # Test Plan - looked at run logs and used cmd f to double check command counts/times # Changelog Added a function for the thermocycler, temperature module, and heater shaker to count values of interest for lifetime test comparison Added those dictionaries to larger dictionary to be included on run sheet # Review requests # Risk assessment - These functions are not set up to handle multiples of the same module in a protocol. It will group total commands together - some modules do not deactivate at the end of the run. To get total on time, the protocol completedAt timestamp is used. --- .../abr_testing/automation/jira_tool.py | 1 + .../data_collection/abr_google_drive.py | 39 +--- .../data_collection/abr_robot_error.py | 10 +- .../data_collection/error_levels.csv | 8 +- .../data_collection/read_robot_logs.py | 214 +++++++++++++++++- 5 files changed, 233 insertions(+), 39 deletions(-) diff --git a/abr-testing/abr_testing/automation/jira_tool.py b/abr-testing/abr_testing/automation/jira_tool.py index 5ed521c0430..aff3a6798c3 100644 --- a/abr-testing/abr_testing/automation/jira_tool.py +++ b/abr-testing/abr_testing/automation/jira_tool.py @@ -44,6 +44,7 @@ def issues_on_board(self, board_id: str) -> List[str]: def open_issue(self, issue_key: str) -> None: """Open issue on web browser.""" url = f"{self.url}/browse/{issue_key}" + print(f"Opening at {url}.") webbrowser.open(url) def create_ticket( diff --git a/abr-testing/abr_testing/data_collection/abr_google_drive.py b/abr-testing/abr_testing/data_collection/abr_google_drive.py index 1d79bbe2ca2..741ac871d62 100644 --- a/abr-testing/abr_testing/data_collection/abr_google_drive.py +++ b/abr-testing/abr_testing/data_collection/abr_google_drive.py @@ -6,7 +6,7 @@ import gspread # type: ignore[import] from datetime import datetime, timedelta from abr_testing.data_collection import read_robot_logs -from typing import Set, Dict, Any +from typing import Set, Dict, Any, Tuple, List from abr_testing.automation import google_drive_tool, google_sheets_tool @@ -31,7 +31,7 @@ def get_modules(file_results: Dict[str, str]) -> Dict[str, Any]: def create_data_dictionary( runs_to_save: Set[str], storage_directory: str -) -> Dict[Any, Dict[str, Any]]: +) -> Tuple[Dict[Any, Dict[str, Any]], List]: """Pull data from run files and format into a dictionary.""" runs_and_robots = {} for filename in os.listdir(storage_directory): @@ -100,12 +100,17 @@ def create_data_dictionary( "Right Mount": right_pipette, "Extension": extension, } - row_2 = {**row, **all_modules} + tc_dict = read_robot_logs.thermocycler_commands(file_results) + hs_dict = read_robot_logs.hs_commands(file_results) + tm_dict = read_robot_logs.temperature_module_commands(file_results) + notes = {"Note1": "", "Note2": ""} + row_2 = {**row, **all_modules, **notes, **hs_dict, **tm_dict, **tc_dict} + headers = list(row_2.keys()) runs_and_robots[run_id] = row_2 else: os.remove(file_path) print(f"Run ID: {run_id} has a run time of 0 minutes. Run removed.") - return runs_and_robots + return runs_and_robots, headers if __name__ == "__main__": @@ -175,29 +180,9 @@ def create_data_dictionary( run_ids_on_gd, run_ids_on_gs ) # Add missing runs to google sheet - runs_and_robots = create_data_dictionary(missing_runs_from_gs, storage_directory) - headers = [ - "Robot", - "Run_ID", - "Protocol_Name", - "Software Version", - "Date", - "Start_Time", - "End_Time", - "Run_Time (min)", - "Errors", - "Error_Code", - "Error_Type", - "Error_Instrument", - "Error_Level", - "Left Mount", - "Right Mount", - "Extension", - "heaterShakerModuleV1", - "temperatureModuleV2", - "magneticBlockV1", - "thermocyclerModuleV2", - ] + runs_and_robots, headers = create_data_dictionary( + missing_runs_from_gs, storage_directory + ) read_robot_logs.write_to_local_and_google_sheet( runs_and_robots, storage_directory, google_sheet_name, google_sheet, headers ) diff --git a/abr-testing/abr_testing/data_collection/abr_robot_error.py b/abr-testing/abr_testing/data_collection/abr_robot_error.py index 9e9e2240a84..3f7302e8725 100644 --- a/abr-testing/abr_testing/data_collection/abr_robot_error.py +++ b/abr-testing/abr_testing/data_collection/abr_robot_error.py @@ -44,6 +44,7 @@ def get_error_info_from_robot( # JIRA Ticket Fields failure_level = "Level " + str(error_level) + " Failure" components = [failure_level, "Flex-RABR"] + components = ["Flex-RABR"] affects_version = results["API_Version"] parent = results.get("robot_name", "") print(parent) @@ -141,10 +142,15 @@ def get_error_info_from_robot( whole_description_str, saved_file_path, ) = get_error_info_from_robot(ip, one_run, storage_directory) + # get calibration data + saved_file_path_calibration, calibration = read_robot_logs.get_calibration_offsets( + ip, storage_directory + ) print(f"Making ticket for run: {one_run} on robot {robot}.") # TODO: make argument or see if I can get rid of with using board_id. project_key = "RABR" parent_key = project_key + "-" + robot[-1] + issues_ids = ticket.issues_on_board(board_id) issue_url, issue_key = ticket.create_ticket( summary, whole_description_str, @@ -158,8 +164,4 @@ def get_error_info_from_robot( ) ticket.open_issue(issue_key) ticket.post_attachment_to_ticket(issue_key, saved_file_path) - # get calibration data - saved_file_path_calibration, calibration = read_robot_logs.get_calibration_offsets( - ip, storage_directory - ) ticket.post_attachment_to_ticket(issue_key, saved_file_path_calibration) diff --git a/abr-testing/abr_testing/data_collection/error_levels.csv b/abr-testing/abr_testing/data_collection/error_levels.csv index e9d93591967..c2f54c9f09e 100644 --- a/abr-testing/abr_testing/data_collection/error_levels.csv +++ b/abr-testing/abr_testing/data_collection/error_levels.csv @@ -20,7 +20,7 @@ Prefix,Error Code,Description,Categories,Level of Failure, 2,2009,Early Capactivive Sense Trigger,A Robot Action Failed,4, 2,2010,Innacrruate Non Contact Sweep,A Robot Action Failed,3, 2,2011,Misaligned Gantry,A Robot Action Failed,3, -2,2012,Unmatched Tip Presence States,A Robot Action Failed,3-4, +2,2012,Unmatched Tip Presence States,A Robot Action Failed, 4, 2,2013,Position Unknown,A Robot Action Failed,4, 2,2014,Execution Cancelled,A Robot Action Failed, 4, 2,2015,Failed Gripper Pickup Error,A Robot Action Failed,3, @@ -31,18 +31,18 @@ Prefix,Error Code,Description,Categories,Level of Failure, 3,3004,Tip Drop Failed,A Robot Interaction Failed,4, 3,3005,Unexpeted Tip Removal,A Robot Interaction Failed,4, 3,3006,Pipette Overpressure,A Robot Interaction Failed,3, -3,3008,E-Stop Activated,A Robot Interaction Failed,Not an error, +3,3008,E-Stop Activated,A Robot Interaction Failed,5, Not an error, 3,3009,E-Stop Not Present,A Robot Interaction Failed,5, 3,3010,Pipette Not Present,A Robot Interaction Failed,5, 3,3011,Gripper Not Present,A Robot Interaction Failed,5, 3,3012,Unexpected Tip Attach,A Robot Interaction Failed,4, -3,3013,Firmware Update Required,A Robot Interaction Failed,Not an error, +3,3013,Firmware Update Required,A Robot Interaction Failed,5, Not an error, 3,3014,Invalid ID Actuator,A Robot Interaction Failed,3, 3,3015,Module Not Pesent,A Robot Interaction Failed,5,Not an error 3,3016,Invalid Instrument Data,A Robot Interaction Failed,3, 3,3017,Invalid Liquid Class Name,A Robot Interaction Failed,5,Not an error 3,3018,Tip Detector Not Found,A Robot Interaction Failed,3, -4,4000,General Error,A Software Error Occured,2-4,How severe does a general error get +4,4000,General Error,A Software Error Occured,4,How severe does a general error get 4,4001,Robot In Use,A Software Error Occured,5,Not an error 4,4002,API Removed,A Software Error Occured,5,used an old app on a new robot 4,4003,Not Supported On Robot Type,A Software Error Occured,5,Not an error diff --git a/abr-testing/abr_testing/data_collection/read_robot_logs.py b/abr-testing/abr_testing/data_collection/read_robot_logs.py index 6a7276c142b..0e31603b7da 100644 --- a/abr-testing/abr_testing/data_collection/read_robot_logs.py +++ b/abr-testing/abr_testing/data_collection/read_robot_logs.py @@ -5,7 +5,7 @@ saved in a local directory. """ import csv -import datetime +from datetime import datetime import os from abr_testing.data_collection.error_levels import ERROR_LEVELS_PATH from typing import List, Dict, Any, Tuple, Set @@ -14,6 +14,210 @@ import requests +def command_time(command: Dict[str, str]) -> Tuple[float, float]: + """Calculate total create and complete time per command.""" + try: + create_time = datetime.strptime( + command.get("createdAt", ""), "%Y-%m-%dT%H:%M:%S.%f%z" + ) + start_time = datetime.strptime( + command.get("startedAt", ""), "%Y-%m-%dT%H:%M:%S.%f%z" + ) + complete_time = datetime.strptime( + command.get("completedAt", ""), "%Y-%m-%dT%H:%M:%S.%f%z" + ) + create_to_start = (start_time - create_time).total_seconds() + start_to_complete = (complete_time - start_time).total_seconds() + except ValueError: + create_to_start = 0 + start_to_complete = 0 + return create_to_start, start_to_complete + + +def hs_commands(file_results: Dict[str, Any]) -> Dict[str, float]: + """Gets total latch engagements, homes, rotations and total on time (sec) for heater shaker.""" + # TODO: modify for cases that have more than 1 heater shaker. + commandData = file_results.get("commands", "") + hs_latch_count: float = 0.0 + hs_temp: float = 0.0 + hs_home_count: float = 0.0 + hs_speed: float = 0.0 + hs_rotations: Dict[str, float] = dict() + hs_temps: Dict[str, float] = dict() + temp_time = None + shake_time = None + for command in commandData: + commandType = command["commandType"] + # Heatershaker + # Latch count + if ( + commandType == "heaterShaker/closeLabwareLatch" + or commandType == "heaterShaker/openLabwareLatch" + ): + hs_latch_count += 1 + # Home count + elif commandType == "heaterShaker/deactivateShaker": + hs_home_count += 1 + deactivate_time = datetime.strptime( + command.get("startedAt", ""), "%Y-%m-%dT%H:%M:%S.%f%z" + ) + if temp_time is not None and deactivate_time > temp_time: + temp_duration = (deactivate_time - temp_time).total_seconds() + hs_temps[hs_temp] = hs_temps.get(hs_temp, 0.0) + temp_duration + if shake_time is not None and deactivate_time > shake_time: + shake_duration = (deactivate_time - shake_time).total_seconds() + hs_rotations[hs_speed] = hs_rotations.get(hs_speed, 0.0) + ( + (hs_speed * shake_duration) / 60 + ) + # of Rotations + elif commandType == "heaterShaker/setAndWaitForShakeSpeed": + hs_speed = command["params"]["rpm"] + shake_time = datetime.strptime( + command.get("completedAt", ""), "%Y-%m-%dT%H:%M:%S.%f%z" + ) + # On Time + elif commandType == "heaterShaker/setTargetTemperature": + # if heater shaker temp is not deactivated. + hs_temp = command["params"]["celsius"] + temp_time = datetime.strptime( + command.get("completedAt", ""), "%Y-%m-%dT%H:%M:%S.%f%z" + ) + + hs_total_rotations = sum(hs_rotations.values()) + hs_total_temp_time = sum(hs_temps.values()) + hs_dict = { + "Heatershaker # of Latch Engagements": hs_latch_count, + "Heatershaker # of Homes": hs_home_count, + "Heatershaker # of Rotations": hs_total_rotations, + "Heatershaker Temp On Time (sec)": hs_total_temp_time, + } + return hs_dict + + +def temperature_module_commands(file_results: Dict[str, Any]) -> Dict[str, float]: + """Get # of temp changes and total temp on time for temperature module from run log.""" + # TODO: modify for cases that have more than 1 temperature module. + tm_temp_change = 0 + tm_temps: Dict[str, float] = dict() + temp_time = None + deactivate_time = None + commandData = file_results.get("commands", "") + for command in commandData: + commandType = command["commandType"] + if commandType == "temperatureModule/setTargetTemperature": + tm_temp = command["params"]["celsius"] + tm_temp_change += 1 + if commandType == "temperatureModule/waitForTemperature": + temp_time = datetime.strptime( + command.get("completedAt", ""), "%Y-%m-%dT%H:%M:%S.%f%z" + ) + if commandType == "temperatureModule/deactivate": + deactivate_time = datetime.strptime( + command.get("completedAt", ""), "%Y-%m-%dT%H:%M:%S.%f%z" + ) + if temp_time is not None and deactivate_time > temp_time: + temp_duration = (deactivate_time - temp_time).total_seconds() + tm_temps[tm_temp] = tm_temps.get(tm_temp, 0.0) + temp_duration + if temp_time is not None and deactivate_time is None: + # If temperature module is not deactivated, protocol completedAt time stamp used. + protocol_end = datetime.strptime( + file_results.get("completedAt", ""), "%Y-%m-%dT%H:%M:%S.%f%z" + ) + temp_duration = (protocol_end - temp_time).total_seconds() + tm_temps[tm_temp] = tm_temps.get(tm_temp, 0.0) + temp_duration + tm_total_temp_time = sum(tm_temps.values()) + tm_dict = { + "Temp Module # of Temp Changes": tm_temp_change, + "Temp Module Temp On Time (sec)": tm_total_temp_time, + } + return tm_dict + + +def thermocycler_commands(file_results: Dict[str, Any]) -> Dict[str, float]: + """Counts # of lid engagements, temp changes, and temp sustaining mins.""" + # TODO: modify for cases that have more than 1 thermocycler. + commandData = file_results.get("commands", "") + lid_engagements: float = 0.0 + block_temp_changes: float = 0.0 + lid_temp_changes: float = 0.0 + lid_temps: Dict[str, float] = dict() + block_temps: Dict[str, float] = dict() + lid_on_time = None + lid_off_time = None + block_on_time = None + block_off_time = None + for command in commandData: + commandType = command["commandType"] + if ( + commandType == "thermocycler/openLid" + or commandType == "thermocycler/closeLid" + ): + lid_engagements += 1 + if commandType == "thermocycler/setTargetBlockTemperature": + block_temp = command["params"]["celsius"] + block_temp_changes += 1 + block_on_time = datetime.strptime( + command.get("completedAt", ""), "%Y-%m-%dT%H:%M:%S.%f%z" + ) + if commandType == "thermocycler/setTargetLidTemperature": + lid_temp_changes += 1 + lid_temp = command["params"]["celsius"] + lid_on_time = datetime.strptime( + command.get("completedAt", ""), "%Y-%m-%dT%H:%M:%S.%f%z" + ) + if commandType == "thermocycler/deactivateLid": + lid_off_time = datetime.strptime( + command.get("completedAt", ""), "%Y-%m-%dT%H:%M:%S.%f%z" + ) + if lid_on_time is not None and lid_off_time > lid_on_time: + lid_duration = (lid_off_time - lid_on_time).total_seconds() + lid_temps[lid_temp] = lid_temps.get(lid_temp, 0.0) + lid_duration + if commandType == "thermocycler/deactivateBlock": + block_off_time = datetime.strptime( + command.get("completedAt", ""), "%Y-%m-%dT%H:%M:%S.%f%z" + ) + if block_on_time is not None and block_off_time > block_on_time: + block_duration = (block_off_time - block_on_time).total_seconds() + block_temps[block_temp] = ( + block_temps.get(block_temp, 0.0) + block_duration + ) + if commandType == "thermocycler/runProfile": + profile = command["params"]["profile"] + total_changes = len(profile) + block_temp_changes += total_changes + for cycle in profile: + block_temp = cycle["celsius"] + block_time = cycle["holdSeconds"] + block_temps[block_temp] = block_temps.get(block_temp, 0.0) + block_time + if block_on_time is not None and block_off_time is None: + # If thermocycler block not deactivated protocol completedAt time stamp used + protocol_end = datetime.strptime( + file_results.get("completedAt", ""), "%Y-%m-%dT%H:%M:%S.%f%z" + ) + temp_duration = (protocol_end - block_on_time).total_seconds() + block_temps[block_temp] = block_temps.get(block_temp, 0.0) + temp_duration + if lid_on_time is not None and lid_off_time is None: + # If thermocycler lid not deactivated protocol completedAt time stamp used + protocol_end = datetime.strptime( + file_results.get("completedAt", ""), "%Y-%m-%dT%H:%M:%S.%f%z" + ) + temp_duration = (protocol_end - lid_on_time).total_seconds() + lid_temps[lid_temp] = block_temps.get(lid_temp, 0.0) + temp_duration + + block_total_time = sum(block_temps.values()) + lid_total_time = sum(lid_temps.values()) + + tc_dict = { + "Thermocycler # of Lid Engagements": lid_engagements, + "Thermocycler Block # of Temp Changes": block_temp_changes, + "Thermocycler Block Temp On Time (sec)": block_total_time, + "Thermocycler Lid # of Temp Changes": lid_temp_changes, + "Thermocycler Lid Temp On Time (sec)": lid_total_time, + } + + return tc_dict + + def create_abr_data_sheet( storage_directory: str, file_name: str, headers: List[str] ) -> str: @@ -112,7 +316,7 @@ def read_abr_data_sheet( runs_in_sheet.add(run_id) print(f"There are {str(len(runs_in_sheet))} runs documented in the ABR sheet.") # Read Google Sheet - google_sheet.check_token() + google_sheet.token_check() google_sheet.write_header(headers) google_sheet.update_row_index() return runs_in_sheet @@ -189,7 +393,7 @@ def get_calibration_offsets( health_data = response.json() robot_name = health_data.get("name", "") api_version = health_data.get("api_version", "") - pull_date_timestamp = datetime.datetime.now() + pull_date_timestamp = datetime.now() date = pull_date_timestamp.date().isoformat() file_date = str(pull_date_timestamp).replace(":", "").split(".")[0] calibration["Robot"] = robot_name @@ -219,5 +423,7 @@ def get_calibration_offsets( ) deck: Dict[str, Any] = response.json() calibration["Deck"] = deck.get("deckCalibration", "") - saved_file_path = save_run_log_to_json(ip, calibration, storage_directory) + save_name = ip + "_calibration.json" + saved_file_path = os.path.join(storage_directory, save_name) + json.dump(calibration, open(saved_file_path, mode="w")) return saved_file_path, calibration From 78507d8ff8d1eefb2a74a95e0276bafd81c2e5d1 Mon Sep 17 00:00:00 2001 From: koji Date: Thu, 4 Apr 2024 10:50:00 -0400 Subject: [PATCH 037/194] docs(app): webpack to vite (#14799) * docs(app): change from webpack to vite in README.md --- app/README.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/app/README.md b/app/README.md index f73f215a48a..93bf6182ed9 100644 --- a/app/README.md +++ b/app/README.md @@ -27,7 +27,7 @@ make -C app dev **Note:** If you would like to interact with a virtual robot server being served at `localhost`, you will need to manually add `localhost` to the discovery candidates list. This can be done through the app's GUI settings for "Connect to a robot via IP address / Add Manual IP Address" -At this point, the Electron app will be running with [HMR][] and various Chrome devtools enabled. The app and dev server look for the following environment variables (defaults set in Makefile): +At this point, the Electron app will be running with various Chrome devtools enabled. The app and dev server look for the following environment variables (defaults set in Makefile): | Variable | Default | Description | | -------------------- | ------------ | --------------------------------------------------- | @@ -46,7 +46,7 @@ The UI stack is built using: - [Redux][] - [CSS modules][css-modules] - [Babel][] -- [Webpack][] +- [Vite][] Some important directories: @@ -54,7 +54,6 @@ Some important directories: - API clients (see [`api/opentrons/server`][api-server-source]) - `api-client` - HTTP Robot API client - `react-api-client` - react utilities for Robot API client -- `app/webpack` - Webpack configuration helpers ## Copy management @@ -131,10 +130,9 @@ ANALYZER=1 make -C app [api-server-source]: ../api/opentrons/server [electron]: https://www.electronjs.org/ [electron-renderer]: https://electronjs.org/docs/tutorial/quick-start#renderer-process -[hmr]: https://webpack.js.org/concepts/hot-module-replacement/ [react]: https://react.dev/ [redux]: http://redux.js.org/ [css-modules]: https://github.com/css-modules/css-modules [babel]: https://babeljs.io/ -[webpack]: https://webpack.js.org/ +[vite]: https://vitejs.dev/ [bundle-analyzer]: https://github.com/webpack-contrib/webpack-bundle-analyzer From 136e1ec5f23e283f7c68473c3a0b08655a04710e Mon Sep 17 00:00:00 2001 From: Jamey Huffnagle Date: Thu, 4 Apr 2024 10:50:20 -0400 Subject: [PATCH 038/194] refactor(app, robot-server): Rename refetchUsingHTTP -> refetch (#14800) --- .../src/notifications/deserialize.ts | 2 +- .../__tests__/deserialize.test.ts | 2 +- app-shell/src/notifications/deserialize.ts | 2 +- app/src/redux/shell/types.ts | 2 +- .../__tests__/useNotifyService.test.ts | 22 ++++++++--------- .../useNotifyCurrentMaintenanceRun.ts | 14 ++++------- .../resources/runs/useNotifyAllRunsQuery.ts | 14 ++++------- .../runs/useNotifyLastRunCommandKey.ts | 14 ++++------- app/src/resources/runs/useNotifyRunQuery.ts | 14 ++++------- app/src/resources/useNotifyService.ts | 14 +++++------ .../robot_server/service/json_api/response.py | 2 +- .../notifications/notification_client.py | 24 ++++++++++--------- .../tests/service/json_api/test_response.py | 2 +- 13 files changed, 53 insertions(+), 75 deletions(-) diff --git a/app-shell-odd/src/notifications/deserialize.ts b/app-shell-odd/src/notifications/deserialize.ts index 4539bc97faa..01fd4bc933b 100644 --- a/app-shell-odd/src/notifications/deserialize.ts +++ b/app-shell-odd/src/notifications/deserialize.ts @@ -12,7 +12,7 @@ import type { import { FAILURE_STATUSES } from '../constants' const VALID_NOTIFY_RESPONSES: [NotifyRefetchData, NotifyUnsubscribeData] = [ - { refetchUsingHTTP: true }, + { refetch: true }, { unsubscribe: true }, ] diff --git a/app-shell/src/notifications/__tests__/deserialize.test.ts b/app-shell/src/notifications/__tests__/deserialize.test.ts index 9c6642d3931..ca9bab984fb 100644 --- a/app-shell/src/notifications/__tests__/deserialize.test.ts +++ b/app-shell/src/notifications/__tests__/deserialize.test.ts @@ -4,7 +4,7 @@ import { deserializeExpectedMessages } from '../deserialize' import type { NotifyResponseData } from '@opentrons/app/src/redux/shell/types' -const MOCK_VALID_RESPONSE: NotifyResponseData = { refetchUsingHTTP: true } +const MOCK_VALID_RESPONSE: NotifyResponseData = { refetch: true } const MOCK_VALID_STRING_RESPONSE = JSON.stringify(MOCK_VALID_RESPONSE) const MOCK_INVALID_OBJECT = JSON.stringify({ test: 'MOCK_RESPONSE' }) const MOCK_INVALID_STRING = 'MOCK_STRING' diff --git a/app-shell/src/notifications/deserialize.ts b/app-shell/src/notifications/deserialize.ts index c96d6d19203..53752b32a0f 100644 --- a/app-shell/src/notifications/deserialize.ts +++ b/app-shell/src/notifications/deserialize.ts @@ -18,7 +18,7 @@ interface SendToBrowserParams { } const VALID_NOTIFY_RESPONSES: [NotifyRefetchData, NotifyUnsubscribeData] = [ - { refetchUsingHTTP: true }, + { refetch: true }, { unsubscribe: true }, ] diff --git a/app/src/redux/shell/types.ts b/app/src/redux/shell/types.ts index 1a4cb343d64..d83cee94b15 100644 --- a/app/src/redux/shell/types.ts +++ b/app/src/redux/shell/types.ts @@ -20,7 +20,7 @@ export type IpcListener = ( ) => void export interface NotifyRefetchData { - refetchUsingHTTP: boolean + refetch: boolean } export interface NotifyUnsubscribeData { diff --git a/app/src/resources/__tests__/useNotifyService.test.ts b/app/src/resources/__tests__/useNotifyService.test.ts index 32dad607a75..fdb531ab1cd 100644 --- a/app/src/resources/__tests__/useNotifyService.test.ts +++ b/app/src/resources/__tests__/useNotifyService.test.ts @@ -53,7 +53,7 @@ describe('useNotifyService', () => { renderHook(() => useNotifyService({ topic: MOCK_TOPIC, - setRefetchUsingHTTP: mockHTTPRefetch, + setRefetch: mockHTTPRefetch, options: MOCK_OPTIONS, } as any) ) @@ -68,7 +68,7 @@ describe('useNotifyService', () => { renderHook(() => useNotifyService({ topic: MOCK_TOPIC, - setRefetchUsingHTTP: mockHTTPRefetch, + setRefetch: mockHTTPRefetch, options: { ...MOCK_OPTIONS, forceHttpPolling: true }, } as any) ) @@ -81,7 +81,7 @@ describe('useNotifyService', () => { renderHook(() => useNotifyService({ topic: MOCK_TOPIC, - setRefetchUsingHTTP: mockHTTPRefetch, + setRefetch: mockHTTPRefetch, options: { ...MOCK_OPTIONS, enabled: false }, } as any) ) @@ -94,7 +94,7 @@ describe('useNotifyService', () => { renderHook(() => useNotifyService({ topic: MOCK_TOPIC, - setRefetchUsingHTTP: mockHTTPRefetch, + setRefetch: mockHTTPRefetch, options: { ...MOCK_OPTIONS, staleTime: Infinity }, } as any) ) @@ -111,7 +111,7 @@ describe('useNotifyService', () => { renderHook(() => useNotifyService({ topic: MOCK_TOPIC, - setRefetchUsingHTTP: mockHTTPRefetch, + setRefetch: mockHTTPRefetch, options: MOCK_OPTIONS, } as any) ) @@ -128,7 +128,7 @@ describe('useNotifyService', () => { const { rerender } = renderHook(() => useNotifyService({ topic: MOCK_TOPIC, - setRefetchUsingHTTP: mockHTTPRefetch, + setRefetch: mockHTTPRefetch, options: MOCK_OPTIONS, } as any) ) @@ -142,12 +142,12 @@ describe('useNotifyService', () => { callback, }): any { // eslint-disable-next-line n/no-callback-literal - callback({ refetchUsingHTTP: true }) + callback({ refetch: true }) }) const { rerender } = renderHook(() => useNotifyService({ topic: MOCK_TOPIC, - setRefetchUsingHTTP: mockHTTPRefetch, + setRefetch: mockHTTPRefetch, options: MOCK_OPTIONS, } as any) ) @@ -165,7 +165,7 @@ describe('useNotifyService', () => { const { rerender } = renderHook(() => useNotifyService({ topic: MOCK_TOPIC, - setRefetchUsingHTTP: mockHTTPRefetch, + setRefetch: mockHTTPRefetch, options: MOCK_OPTIONS, } as any) ) @@ -177,7 +177,7 @@ describe('useNotifyService', () => { const { unmount } = renderHook(() => useNotifyService({ topic: MOCK_TOPIC, - setRefetchUsingHTTP: mockHTTPRefetch, + setRefetch: mockHTTPRefetch, options: MOCK_OPTIONS, }) ) @@ -190,7 +190,7 @@ describe('useNotifyService', () => { useNotifyService({ hostOverride: MOCK_HOST_CONFIG, topic: MOCK_TOPIC, - setRefetchUsingHTTP: mockHTTPRefetch, + setRefetch: mockHTTPRefetch, options: MOCK_OPTIONS, }) ) diff --git a/app/src/resources/maintenance_runs/useNotifyCurrentMaintenanceRun.ts b/app/src/resources/maintenance_runs/useNotifyCurrentMaintenanceRun.ts index 2692e032d6a..28859afe393 100644 --- a/app/src/resources/maintenance_runs/useNotifyCurrentMaintenanceRun.ts +++ b/app/src/resources/maintenance_runs/useNotifyCurrentMaintenanceRun.ts @@ -14,24 +14,18 @@ import type { export function useNotifyCurrentMaintenanceRun( options: QueryOptionsWithPolling = {} ): UseQueryResult | UseQueryResult { - const [ - refetchUsingHTTP, - setRefetchUsingHTTP, - ] = React.useState(null) + const [refetch, setRefetch] = React.useState(null) useNotifyService({ topic: 'robot-server/maintenance_runs/current_run', - setRefetchUsingHTTP, + setRefetch, options, }) const httpQueryResult = useCurrentMaintenanceRun({ ...options, - enabled: options?.enabled !== false && refetchUsingHTTP != null, - onSettled: - refetchUsingHTTP === 'once' - ? () => setRefetchUsingHTTP(null) - : () => null, + enabled: options?.enabled !== false && refetch != null, + onSettled: refetch === 'once' ? () => setRefetch(null) : () => null, }) return httpQueryResult diff --git a/app/src/resources/runs/useNotifyAllRunsQuery.ts b/app/src/resources/runs/useNotifyAllRunsQuery.ts index 690d7a4ac11..1ae93ffc713 100644 --- a/app/src/resources/runs/useNotifyAllRunsQuery.ts +++ b/app/src/resources/runs/useNotifyAllRunsQuery.ts @@ -18,14 +18,11 @@ export function useNotifyAllRunsQuery( options: QueryOptionsWithPolling = {}, hostOverride?: HostConfig | null ): UseQueryResult { - const [ - refetchUsingHTTP, - setRefetchUsingHTTP, - ] = React.useState(null) + const [refetch, setRefetch] = React.useState(null) useNotifyService({ topic: 'robot-server/runs', - setRefetchUsingHTTP, + setRefetch, options, hostOverride, }) @@ -34,11 +31,8 @@ export function useNotifyAllRunsQuery( params, { ...(options as UseAllRunsQueryOptions), - enabled: options?.enabled !== false && refetchUsingHTTP != null, - onSettled: - refetchUsingHTTP === 'once' - ? () => setRefetchUsingHTTP(null) - : () => null, + enabled: options?.enabled !== false && refetch != null, + onSettled: refetch === 'once' ? () => setRefetch(null) : () => null, }, hostOverride ) diff --git a/app/src/resources/runs/useNotifyLastRunCommandKey.ts b/app/src/resources/runs/useNotifyLastRunCommandKey.ts index 8600c4d66b6..9c908a70749 100644 --- a/app/src/resources/runs/useNotifyLastRunCommandKey.ts +++ b/app/src/resources/runs/useNotifyLastRunCommandKey.ts @@ -13,24 +13,18 @@ export function useNotifyLastRunCommandKey( runId: string, options: QueryOptionsWithPolling = {} ): string | null { - const [ - refetchUsingHTTP, - setRefetchUsingHTTP, - ] = React.useState(null) + const [refetch, setRefetch] = React.useState(null) useNotifyService({ topic: 'robot-server/runs/current_command', - setRefetchUsingHTTP, + setRefetch, options, }) const httpResponse = useLastRunCommandKey(runId, { ...options, - enabled: options?.enabled !== false && refetchUsingHTTP != null, - onSettled: - refetchUsingHTTP === 'once' - ? () => setRefetchUsingHTTP(null) - : () => null, + enabled: options?.enabled !== false && refetch != null, + onSettled: refetch === 'once' ? () => setRefetch(null) : () => null, }) return httpResponse diff --git a/app/src/resources/runs/useNotifyRunQuery.ts b/app/src/resources/runs/useNotifyRunQuery.ts index dde7bc84448..2ca72687341 100644 --- a/app/src/resources/runs/useNotifyRunQuery.ts +++ b/app/src/resources/runs/useNotifyRunQuery.ts @@ -16,26 +16,20 @@ export function useNotifyRunQuery( runId: string | null, options: QueryOptionsWithPolling = {} ): UseQueryResult { - const [ - refetchUsingHTTP, - setRefetchUsingHTTP, - ] = React.useState(null) + const [refetch, setRefetch] = React.useState(null) const isEnabled = options.enabled !== false && runId != null useNotifyService({ topic: `robot-server/runs/${runId}` as NotifyTopic, - setRefetchUsingHTTP, + setRefetch, options: { ...options, enabled: options.enabled != null && runId != null }, }) const httpResponse = useRunQuery(runId, { ...options, - enabled: isEnabled && refetchUsingHTTP != null, - onSettled: - refetchUsingHTTP === 'once' - ? () => setRefetchUsingHTTP(null) - : () => null, + enabled: isEnabled && refetch != null, + onSettled: refetch === 'once' ? () => setRefetch(null) : () => null, }) return httpResponse diff --git a/app/src/resources/useNotifyService.ts b/app/src/resources/useNotifyService.ts index 8068c2d4ade..ae0100a2103 100644 --- a/app/src/resources/useNotifyService.ts +++ b/app/src/resources/useNotifyService.ts @@ -25,14 +25,14 @@ export interface QueryOptionsWithPolling interface UseNotifyServiceProps { topic: NotifyTopic - setRefetchUsingHTTP: (refetch: HTTPRefetchFrequency) => void + setRefetch: (refetch: HTTPRefetchFrequency) => void options: QueryOptionsWithPolling hostOverride?: HostConfig | null } export function useNotifyService({ topic, - setRefetchUsingHTTP, + setRefetch, options, hostOverride, }: UseNotifyServiceProps): void { @@ -55,7 +55,7 @@ export function useNotifyService({ React.useEffect(() => { if (shouldUseNotifications) { // Always fetch on initial mount. - setRefetchUsingHTTP('once') + setRefetch('once') appShellListener({ hostname, topic, @@ -65,7 +65,7 @@ export function useNotifyService({ hasUsedNotifyService.current = true seenHostname.current = hostname } else { - setRefetchUsingHTTP('always') + setRefetch('always') } return () => { @@ -82,7 +82,7 @@ export function useNotifyService({ function onDataEvent(data: NotifyResponseData): void { if (data === 'ECONNFAILED' || data === 'ECONNREFUSED') { - setRefetchUsingHTTP('always') + setRefetch('always') // TODO(jh 2023-02-23): remove the robot type check once OT-2s support MQTT. if (data === 'ECONNREFUSED' && isFlex) { doTrackEvent({ @@ -90,8 +90,8 @@ export function useNotifyService({ properties: {}, }) } - } else if ('refetchUsingHTTP' in data || 'unsubscribe' in data) { - setRefetchUsingHTTP('once') + } else if ('refetch' in data || 'unsubscribe' in data) { + setRefetch('once') } } } diff --git a/robot-server/robot_server/service/json_api/response.py b/robot-server/robot_server/service/json_api/response.py index 9d2c2cb76b9..e1e422f255c 100644 --- a/robot-server/robot_server/service/json_api/response.py +++ b/robot-server/robot_server/service/json_api/response.py @@ -287,7 +287,7 @@ class ResponseList(BaseModel, Generic[ResponseDataT]): class NotifyRefetchBody(BaseResponseBody): """A notification response that returns a flag for refetching via HTTP.""" - refetchUsingHTTP: bool = True + refetch: bool = True class NotifyUnsubscribeBody(BaseResponseBody): diff --git a/robot-server/robot_server/service/notifications/notification_client.py b/robot-server/robot_server/service/notifications/notification_client.py index 6b51eba9cc9..f53de3bbe39 100644 --- a/robot-server/robot_server/service/notifications/notification_client.py +++ b/robot-server/robot_server/service/notifications/notification_client.py @@ -59,24 +59,26 @@ def __init__( # MQTT is somewhat particular about the client_id format and will connect erratically # if an unexpected string is supplied. This clientId is derived from the paho-mqtt library. self._client_id: str = f"robot-server-{random.randint(0, 1000000)}" - self.client: mqtt.Client = mqtt.Client( + self._client: mqtt.Client = mqtt.Client( client_id=self._client_id, protocol=protocol_version ) - self.client.on_connect = self._on_connect - self.client.on_disconnect = self._on_disconnect + self._client.on_connect = self._on_connect + self._client.on_disconnect = self._on_disconnect def connect(self) -> None: """Connect the client to the MQTT broker.""" - self.client.on_connect = self._on_connect - self.client.on_disconnect = self._on_disconnect + self._client.on_connect = self._on_connect + self._client.on_disconnect = self._on_disconnect - self.client.connect(host=self._host, port=self._port, keepalive=self._keepalive) - self.client.loop_start() + self._client.connect( + host=self._host, port=self._port, keepalive=self._keepalive + ) + self._client.loop_start() async def disconnect(self) -> None: """Disconnect the client from the MQTT broker.""" - self.client.loop_stop() - await to_thread.run_sync(self.client.disconnect) + self._client.loop_stop() + await to_thread.run_sync(self._client.disconnect) async def publish_advise_refetch_async(self, topic: str) -> None: """Asynchronously publish a refetch message on a specific topic to the MQTT broker. @@ -105,7 +107,7 @@ def publish_advise_refetch( """ message = NotifyRefetchBody.construct() payload = message.json() - self.client.publish( + self._client.publish( topic=topic, payload=payload, qos=self._default_qos, @@ -123,7 +125,7 @@ def publish_advise_unsubscribe( """ message = NotifyUnsubscribeBody.construct() payload = message.json() - self.client.publish( + self._client.publish( topic=topic, payload=payload, qos=self._default_qos, diff --git a/robot-server/tests/service/json_api/test_response.py b/robot-server/tests/service/json_api/test_response.py index 1429d88b5e0..6952468229b 100644 --- a/robot-server/tests/service/json_api/test_response.py +++ b/robot-server/tests/service/json_api/test_response.py @@ -116,7 +116,7 @@ class ResponseSpec(NamedTuple): "links": {"sibling": {"href": "/bar", "meta": None}}, }, ), - ResponseSpec(subject=NotifyRefetchBody(), expected={"refetchUsingHTTP": True}), + ResponseSpec(subject=NotifyRefetchBody(), expected={"refetch": True}), ResponseSpec( subject=NotifyUnsubscribeBody(), expected={"unsubscribe": True}, From 6145da1717c94216d16b5036c1eee315337c5f95 Mon Sep 17 00:00:00 2001 From: Brayan Almonte Date: Thu, 4 Apr 2024 10:59:35 -0400 Subject: [PATCH 039/194] feat(hardware): add new hepa fan rpm field to HepaFanStateResponse (#14754) --- .../firmware_bindings/messages/payloads.py | 1 + .../hardware_control/hepa_uv_settings.py | 2 ++ .../hardware_control/test_hepauv_settings.py | 15 ++++++++++----- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/hardware/opentrons_hardware/firmware_bindings/messages/payloads.py b/hardware/opentrons_hardware/firmware_bindings/messages/payloads.py index f4bca8cb881..c351495ba5b 100644 --- a/hardware/opentrons_hardware/firmware_bindings/messages/payloads.py +++ b/hardware/opentrons_hardware/firmware_bindings/messages/payloads.py @@ -665,6 +665,7 @@ class GetHepaFanStatePayloadResponse(EmptyPayload): duty_cycle: utils.UInt32Field fan_on: utils.UInt8Field + fan_rpm: utils.UInt16Field @dataclass(eq=False) diff --git a/hardware/opentrons_hardware/hardware_control/hepa_uv_settings.py b/hardware/opentrons_hardware/hardware_control/hepa_uv_settings.py index 0716a4f4c90..2812cdf3f7d 100644 --- a/hardware/opentrons_hardware/hardware_control/hepa_uv_settings.py +++ b/hardware/opentrons_hardware/hardware_control/hepa_uv_settings.py @@ -35,6 +35,7 @@ class HepaFanState: fan_on: bool duty_cycle: int + fan_rpm: int @dataclass(frozen=True) @@ -80,6 +81,7 @@ def _listener(message: MessageDefinition, arb_id: ArbitrationId) -> None: fan_state = HepaFanState( fan_on=bool(message.payload.fan_on.value), duty_cycle=int(message.payload.duty_cycle.value), + fan_rpm=int(message.payload.fan_rpm.value), ) def _filter(arb_id: ArbitrationId) -> bool: diff --git a/hardware/tests/opentrons_hardware/hardware_control/test_hepauv_settings.py b/hardware/tests/opentrons_hardware/hardware_control/test_hepauv_settings.py index 2401aee34b4..dcaf85a8653 100644 --- a/hardware/tests/opentrons_hardware/hardware_control/test_hepauv_settings.py +++ b/hardware/tests/opentrons_hardware/hardware_control/test_hepauv_settings.py @@ -37,12 +37,15 @@ def mock_can_messenger() -> AsyncMock: return AsyncMock() -def create_hepa_fan_state_response(fan_on: bool, duty_cycle: int) -> MessageDefinition: +def create_hepa_fan_state_response( + fan_on: bool, duty_cycle: int, fan_rpm: int +) -> MessageDefinition: """Create a GetHepaFanStateResponse.""" return md.GetHepaFanStateResponse( payload=GetHepaFanStatePayloadResponse( fan_on=UInt8Field(fan_on), duty_cycle=UInt32Field(duty_cycle), + fan_rpm=UInt16Field(fan_rpm), ) ) @@ -111,10 +114,11 @@ async def test_set_hepa_uv_state( @pytest.mark.parametrize( "response", [ - (NodeId.host, create_hepa_fan_state_response(True, 75), NodeId.hepa_uv), - (NodeId.host, create_hepa_fan_state_response(True, 0), NodeId.hepa_uv), - (NodeId.host, create_hepa_fan_state_response(False, 75), NodeId.hepa_uv), - (NodeId.host, create_hepa_fan_state_response(False, 100), NodeId.hepa_uv), + (NodeId.host, create_hepa_fan_state_response(True, 50, 4540), NodeId.hepa_uv), + (NodeId.host, create_hepa_fan_state_response(True, 75, 6790), NodeId.hepa_uv), + (NodeId.host, create_hepa_fan_state_response(True, 0, 0), NodeId.hepa_uv), + (NodeId.host, create_hepa_fan_state_response(False, 75, 0), NodeId.hepa_uv), + (NodeId.host, create_hepa_fan_state_response(False, 100, 0), NodeId.hepa_uv), ], ) async def test_get_hepa_fan_state( @@ -147,6 +151,7 @@ def responder( HepaFanState( bool(payload.fan_on.value), int(payload.duty_cycle.value), + int(payload.fan_rpm.value), ) == res ) From 6a6720e9b5b814efcb1cb5ededab3a4d9d2e60ed Mon Sep 17 00:00:00 2001 From: Nick Diehl <47604184+ncdiehl11@users.noreply.github.com> Date: Thu, 4 Apr 2024 11:42:56 -0400 Subject: [PATCH 040/194] feat(app): disable confirm values button if error in RTP (#14794) closes AUTH-265 --- .../ChooseProtocolSlideout/index.tsx | 57 ++++++++++++------- .../organisms/ChooseRobotSlideout/index.tsx | 42 ++++++++------ .../ChooseRobotToRunProtocolSlideout.test.tsx | 3 +- .../index.tsx | 10 +++- 4 files changed, 70 insertions(+), 42 deletions(-) diff --git a/app/src/organisms/ChooseProtocolSlideout/index.tsx b/app/src/organisms/ChooseProtocolSlideout/index.tsx index 6c1e11d9105..b2d48540ae8 100644 --- a/app/src/organisms/ChooseProtocolSlideout/index.tsx +++ b/app/src/organisms/ChooseProtocolSlideout/index.tsx @@ -92,6 +92,7 @@ export function ChooseProtocolSlideoutComponent( setRunTimeParametersOverrides, ] = React.useState([]) const [currentPage, setCurrentPage] = React.useState(1) + const [hasParamError, setHasParamError] = React.useState(false) const enableRunTimeParametersFF = useFeatureFlag('enableRunTimeParameters') React.useEffect(() => { @@ -99,6 +100,10 @@ export function ChooseProtocolSlideoutComponent( selectedProtocol?.mostRecentAnalysis?.runTimeParameters ?? [] ) }, [selectedProtocol]) + React.useEffect(() => { + setHasParamError(errors.length > 0) + }, [runTimeParametersOverrides]) + const runTimeParametersFromAnalysis = selectedProtocol?.mostRecentAnalysis?.runTimeParameters ?? [] @@ -187,6 +192,7 @@ export function ChooseProtocolSlideoutComponent( parameter => parameter.value !== parameter.default ) ?? false + const errors: string[] = [] const runTimeParametersInputs = runTimeParametersOverrides?.map((runtimeParam, index) => { if ('choices' in runtimeParam) { @@ -240,6 +246,9 @@ export function ChooseProtocolSlideoutComponent( : runtimeParam.max.toFixed(1), }) : null + if (error != null) { + errors.push(error) + } return ( setCurrentPage(1)} width="51%"> {t('shared:change_protocol')} - + {isCreatingRun ? ( ) : ( @@ -409,26 +422,28 @@ export function ChooseProtocolSlideoutComponent( robot?.ip === OPENTRONS_USB ? appShellRequestor : undefined } > - + {currentPage === 1 ? ( + + ) : null} {hasRunTimeParameters ? multiPageFooter : singlePageFooter} } diff --git a/app/src/organisms/ChooseRobotSlideout/index.tsx b/app/src/organisms/ChooseRobotSlideout/index.tsx index b21e417774b..904615b9ca5 100644 --- a/app/src/organisms/ChooseRobotSlideout/index.tsx +++ b/app/src/organisms/ChooseRobotSlideout/index.tsx @@ -113,6 +113,7 @@ interface ChooseRobotSlideoutProps isAnalysisStale?: boolean showIdleOnly?: boolean multiSlideout?: { currentPage: number } | null + setHasParamError?: (isError: boolean) => void } export function ChooseRobotSlideout( @@ -138,6 +139,7 @@ export function ChooseRobotSlideout( multiSlideout = null, runTimeParametersOverrides, setRunTimeParametersOverrides, + setHasParamError, } = props const enableRunTimeParametersFF = useFeatureFlag('enableRunTimeParameters') @@ -330,6 +332,7 @@ export function ChooseRobotSlideout( ) + const errors: string[] = [] const runTimeParameters = runTimeParametersOverrides?.map((runtimeParam, index) => { if ('choices' in runtimeParam) { @@ -370,6 +373,24 @@ export function ChooseRobotSlideout( } else if (runtimeParam.type === 'int' || runtimeParam.type === 'float') { const value = runtimeParam.value as number const id = `InputField_${runtimeParam.variableName}_${index.toString()}` + const error = + Number.isNaN(value) || + value < runtimeParam.min || + value > runtimeParam.max + ? t(`value_out_of_range`, { + min: + runtimeParam.type === 'int' + ? runtimeParam.min + : runtimeParam.min.toFixed(1), + max: + runtimeParam.type === 'int' + ? runtimeParam.max + : runtimeParam.max.toFixed(1), + }) + : null + if (error != null) { + errors.push(error) + } return ( runtimeParam.max - ? t(`value_out_of_range`, { - min: - runtimeParam.type === 'int' - ? runtimeParam.min - : runtimeParam.min.toFixed(1), - max: - runtimeParam.type === 'int' - ? runtimeParam.max - : runtimeParam.max.toFixed(1), - }) - : null - } + error={error} onChange={e => { const clone = runTimeParametersOverrides.map((parameter, i) => { if (i === index) { @@ -474,6 +480,10 @@ export function ChooseRobotSlideout( } }) ?? null + if (setHasParamError != null) { + setHasParamError(errors.length > 0) + } + const isRestoreDefaultsLinkEnabled = runTimeParametersOverrides?.some( parameter => parameter.value !== parameter.default diff --git a/app/src/organisms/ChooseRobotToRunProtocolSlideout/__tests__/ChooseRobotToRunProtocolSlideout.test.tsx b/app/src/organisms/ChooseRobotToRunProtocolSlideout/__tests__/ChooseRobotToRunProtocolSlideout.test.tsx index 8a7c9f64597..b7d2b32cb75 100644 --- a/app/src/organisms/ChooseRobotToRunProtocolSlideout/__tests__/ChooseRobotToRunProtocolSlideout.test.tsx +++ b/app/src/organisms/ChooseRobotToRunProtocolSlideout/__tests__/ChooseRobotToRunProtocolSlideout.test.tsx @@ -383,8 +383,7 @@ describe('ChooseRobotToRunProtocolSlideout', () => { ], {} ) - expect(vi.mocked(useCreateRunFromProtocol)).nthCalledWith( - 3, + expect(vi.mocked(useCreateRunFromProtocol)).toHaveBeenLastCalledWith( expect.any(Object), { hostname: 'otherIp' }, [], diff --git a/app/src/organisms/ChooseRobotToRunProtocolSlideout/index.tsx b/app/src/organisms/ChooseRobotToRunProtocolSlideout/index.tsx index 39cf498b0e5..ac3f50301cf 100644 --- a/app/src/organisms/ChooseRobotToRunProtocolSlideout/index.tsx +++ b/app/src/organisms/ChooseRobotToRunProtocolSlideout/index.tsx @@ -60,14 +60,13 @@ export function ChooseRobotToRunProtocolSlideoutComponent( storedProtocolData, selectedRobot?.name ?? '' ) - - // TODO: (nd: 3/20/24) remove stubs and pull parameters from analysis const runTimeParameters = storedProtocolData.mostRecentAnalysis?.runTimeParameters ?? [] const [ runTimeParametersOverrides, setRunTimeParametersOverrides, ] = React.useState(runTimeParameters) + const [hasParamError, setHasParamError] = React.useState(false) const offsetCandidates = useOffsetCandidatesForAnalysis( mostRecentAnalysis, @@ -229,7 +228,11 @@ export function ChooseRobotToRunProtocolSlideoutComponent( setCurrentPage(1)} width="50%"> {t('shared:change_robot')} - + {isCreatingRun ? ( ) : ( @@ -251,6 +254,7 @@ export function ChooseRobotToRunProtocolSlideoutComponent( runCreationError={runCreationError} runCreationErrorCode={runCreationErrorCode} showIdleOnly={true} + setHasParamError={setHasParamError} /> ) } From eecf11767e97e80f8c1ff7ea81c99eb006f2e076 Mon Sep 17 00:00:00 2001 From: Max Marrone Date: Thu, 4 Apr 2024 11:59:04 -0400 Subject: [PATCH 041/194] refactor(api): Rename opentrons.commands to opentrons.legacy_commands (#14796) --- api/.flake8 | 4 ++-- api/src/opentrons/execute.py | 4 ++-- api/src/opentrons/legacy_broker.py | 4 ++-- api/src/opentrons/legacy_commands/__init__.py | 1 + .../{commands => legacy_commands}/commands.py | 0 .../opentrons/{commands => legacy_commands}/helpers.py | 0 .../{commands => legacy_commands}/module_commands.py | 0 .../{commands => legacy_commands}/protocol_commands.py | 0 .../{commands => legacy_commands}/publisher.py | 0 .../opentrons/{commands => legacy_commands}/types.py | 0 api/src/opentrons/protocol_api/instrument_context.py | 4 ++-- api/src/opentrons/protocol_api/module_contexts.py | 4 ++-- api/src/opentrons/protocol_api/protocol_context.py | 10 +++++++--- .../opentrons/protocol_engine/clients/sync_client.py | 4 +++- .../opentrons/protocol_runner/legacy_command_mapper.py | 2 +- .../opentrons/protocol_runner/legacy_context_plugin.py | 2 +- api/src/opentrons/protocols/duration/estimator.py | 2 +- api/src/opentrons/simulate.py | 6 +++--- api/tests/opentrons/commands/__init__.py | 0 .../opentrons/legacy_commands}/__init__.py | 0 .../test_protocol_commands.py | 2 +- .../{commands => legacy_commands}/test_publisher.py | 10 +++++++--- .../protocol_runner/test_legacy_command_mapper.py | 2 +- .../protocol_runner/test_legacy_context_plugin.py | 5 ++++- .../opentrons/protocols/duration/test_estimator.py | 2 +- api/tests/opentrons/test_legacy_broker.py | 4 ++-- 26 files changed, 43 insertions(+), 29 deletions(-) create mode 100644 api/src/opentrons/legacy_commands/__init__.py rename api/src/opentrons/{commands => legacy_commands}/commands.py (100%) rename api/src/opentrons/{commands => legacy_commands}/helpers.py (100%) rename api/src/opentrons/{commands => legacy_commands}/module_commands.py (100%) rename api/src/opentrons/{commands => legacy_commands}/protocol_commands.py (100%) rename api/src/opentrons/{commands => legacy_commands}/publisher.py (100%) rename api/src/opentrons/{commands => legacy_commands}/types.py (100%) delete mode 100644 api/tests/opentrons/commands/__init__.py rename api/{src/opentrons/commands => tests/opentrons/legacy_commands}/__init__.py (100%) rename api/tests/opentrons/{commands => legacy_commands}/test_protocol_commands.py (96%) rename api/tests/opentrons/{commands => legacy_commands}/test_publisher.py (97%) diff --git a/api/.flake8 b/api/.flake8 index d654020fa7f..ee1a726e611 100644 --- a/api/.flake8 +++ b/api/.flake8 @@ -33,7 +33,7 @@ per-file-ignores = src/opentrons/simulate.py:ANN,D src/opentrons/types.py:ANN,D src/opentrons/calibration_storage/*:ANN,D - src/opentrons/commands/*:D + src/opentrons/legacy_commands/*:D src/opentrons/config/*:ANN,D src/opentrons/drivers/*:ANN,D src/opentrons/hardware_control/*:ANN,D @@ -51,7 +51,7 @@ per-file-ignores = tests/opentrons/test_types.py:ANN,D tests/opentrons/conftest.py:ANN,D tests/opentrons/calibration_storage/*:ANN,D - tests/opentrons/commands/*:ANN,D + tests/opentrons/legacy_commands/*:ANN,D tests/opentrons/config/*:ANN,D tests/opentrons/data/*:ANN,D tests/opentrons/drivers/*:ANN,D diff --git a/api/src/opentrons/execute.py b/api/src/opentrons/execute.py index a35f4a91d8d..e851d8a44f0 100644 --- a/api/src/opentrons/execute.py +++ b/api/src/opentrons/execute.py @@ -28,7 +28,7 @@ from opentrons import protocol_api, __version__, should_use_ot3 -from opentrons.commands import types as command_types +from opentrons.legacy_commands import types as command_types from opentrons.hardware_control import ( API as OT2API, @@ -333,7 +333,7 @@ def execute( # noqa: C901 'text': string_command_text, # The rest of this struct is # command-dependent; see - # opentrons.commands.commands. + # opentrons.legacy_commands.commands. } } diff --git a/api/src/opentrons/legacy_broker.py b/api/src/opentrons/legacy_broker.py index 838a75b7759..b58a779134e 100644 --- a/api/src/opentrons/legacy_broker.py +++ b/api/src/opentrons/legacy_broker.py @@ -5,7 +5,7 @@ from typing import Callable, Dict, List from typing_extensions import Literal -from opentrons.commands import types +from opentrons.legacy_commands import types MODULE_LOG = logging.getLogger(__name__) @@ -16,7 +16,7 @@ class LegacyBroker: Deprecated: Use the newer, more generic `opentrons.utils.Broker` class instead. - This class is coupled to old types from `opentrons.commands`. + This class is coupled to old types from `opentrons.legacy_commands`. https://opentrons.atlassian.net/browse/RSS-270 """ diff --git a/api/src/opentrons/legacy_commands/__init__.py b/api/src/opentrons/legacy_commands/__init__.py new file mode 100644 index 00000000000..558ad9b87c0 --- /dev/null +++ b/api/src/opentrons/legacy_commands/__init__.py @@ -0,0 +1 @@ +"""Command models from before v5.0, before Protocol Engine.""" diff --git a/api/src/opentrons/commands/commands.py b/api/src/opentrons/legacy_commands/commands.py similarity index 100% rename from api/src/opentrons/commands/commands.py rename to api/src/opentrons/legacy_commands/commands.py diff --git a/api/src/opentrons/commands/helpers.py b/api/src/opentrons/legacy_commands/helpers.py similarity index 100% rename from api/src/opentrons/commands/helpers.py rename to api/src/opentrons/legacy_commands/helpers.py diff --git a/api/src/opentrons/commands/module_commands.py b/api/src/opentrons/legacy_commands/module_commands.py similarity index 100% rename from api/src/opentrons/commands/module_commands.py rename to api/src/opentrons/legacy_commands/module_commands.py diff --git a/api/src/opentrons/commands/protocol_commands.py b/api/src/opentrons/legacy_commands/protocol_commands.py similarity index 100% rename from api/src/opentrons/commands/protocol_commands.py rename to api/src/opentrons/legacy_commands/protocol_commands.py diff --git a/api/src/opentrons/commands/publisher.py b/api/src/opentrons/legacy_commands/publisher.py similarity index 100% rename from api/src/opentrons/commands/publisher.py rename to api/src/opentrons/legacy_commands/publisher.py diff --git a/api/src/opentrons/commands/types.py b/api/src/opentrons/legacy_commands/types.py similarity index 100% rename from api/src/opentrons/commands/types.py rename to api/src/opentrons/legacy_commands/types.py diff --git a/api/src/opentrons/protocol_api/instrument_context.py b/api/src/opentrons/protocol_api/instrument_context.py index 1b58bcfc524..26f24899fad 100644 --- a/api/src/opentrons/protocol_api/instrument_context.py +++ b/api/src/opentrons/protocol_api/instrument_context.py @@ -11,9 +11,9 @@ from opentrons.legacy_broker import LegacyBroker from opentrons.hardware_control.dev_types import PipetteDict from opentrons import types -from opentrons.commands import commands as cmds +from opentrons.legacy_commands import commands as cmds -from opentrons.commands import publisher +from opentrons.legacy_commands import publisher from opentrons.protocols.advanced_control.mix import mix_from_kwargs from opentrons.protocols.advanced_control import transfers diff --git a/api/src/opentrons/protocol_api/module_contexts.py b/api/src/opentrons/protocol_api/module_contexts.py index f525fe6b320..5e9d412835e 100644 --- a/api/src/opentrons/protocol_api/module_contexts.py +++ b/api/src/opentrons/protocol_api/module_contexts.py @@ -8,8 +8,8 @@ from opentrons.legacy_broker import LegacyBroker from opentrons.hardware_control.modules import ThermocyclerStep -from opentrons.commands import module_commands as cmds -from opentrons.commands.publisher import CommandPublisher, publish +from opentrons.legacy_commands import module_commands as cmds +from opentrons.legacy_commands.publisher import CommandPublisher, publish from opentrons.protocols.api_support.types import APIVersion from opentrons.protocols.api_support.util import APIVersionError, requires_version diff --git a/api/src/opentrons/protocol_api/protocol_context.py b/api/src/opentrons/protocol_api/protocol_context.py index 2dd7815c09f..feb8f56d91c 100644 --- a/api/src/opentrons/protocol_api/protocol_context.py +++ b/api/src/opentrons/protocol_api/protocol_context.py @@ -20,9 +20,13 @@ from opentrons.legacy_broker import LegacyBroker from opentrons.hardware_control import SyncHardwareAPI from opentrons.hardware_control.modules.types import MagneticBlockModel -from opentrons.commands import protocol_commands as cmds, types as cmd_types -from opentrons.commands.helpers import stringify_labware_movement_command -from opentrons.commands.publisher import CommandPublisher, publish, publish_context +from opentrons.legacy_commands import protocol_commands as cmds, types as cmd_types +from opentrons.legacy_commands.helpers import stringify_labware_movement_command +from opentrons.legacy_commands.publisher import ( + CommandPublisher, + publish, + publish_context, +) from opentrons.protocols.api_support import instrument as instrument_support from opentrons.protocols.api_support.deck_type import ( NoTrashDefinedError, diff --git a/api/src/opentrons/protocol_engine/clients/sync_client.py b/api/src/opentrons/protocol_engine/clients/sync_client.py index 53703c16dee..f9c9e2ee6c6 100644 --- a/api/src/opentrons/protocol_engine/clients/sync_client.py +++ b/api/src/opentrons/protocol_engine/clients/sync_client.py @@ -6,7 +6,9 @@ from opentrons_shared_data.labware.dev_types import LabwareUri from opentrons_shared_data.labware.labware_definition import LabwareDefinition -from opentrons.commands.protocol_commands import comment as make_legacy_comment_command +from opentrons.legacy_commands.protocol_commands import ( + comment as make_legacy_comment_command, +) from opentrons.types import MountType from opentrons.hardware_control.modules.types import ThermocyclerStep diff --git a/api/src/opentrons/protocol_runner/legacy_command_mapper.py b/api/src/opentrons/protocol_runner/legacy_command_mapper.py index 53846baf653..ea212123cb3 100644 --- a/api/src/opentrons/protocol_runner/legacy_command_mapper.py +++ b/api/src/opentrons/protocol_runner/legacy_command_mapper.py @@ -6,7 +6,7 @@ from opentrons_shared_data.pipette.dev_types import PipetteNameType from opentrons.types import MountType, DeckSlotName, Location -from opentrons.commands import types as legacy_command_types +from opentrons.legacy_commands import types as legacy_command_types from opentrons.protocol_engine import ( ProtocolEngineError, actions as pe_actions, diff --git a/api/src/opentrons/protocol_runner/legacy_context_plugin.py b/api/src/opentrons/protocol_runner/legacy_context_plugin.py index 3e32877f232..7dd882f0fb7 100644 --- a/api/src/opentrons/protocol_runner/legacy_context_plugin.py +++ b/api/src/opentrons/protocol_runner/legacy_context_plugin.py @@ -5,7 +5,7 @@ from contextlib import ExitStack from typing import List, Optional -from opentrons.commands.types import CommandMessage as LegacyCommand +from opentrons.legacy_commands.types import CommandMessage as LegacyCommand from opentrons.legacy_broker import LegacyBroker from opentrons.protocol_engine import AbstractPlugin, actions as pe_actions from opentrons.util.broker import ReadOnlyBroker diff --git a/api/src/opentrons/protocols/duration/estimator.py b/api/src/opentrons/protocols/duration/estimator.py index 6f481c29772..5e3b6ef2663 100644 --- a/api/src/opentrons/protocols/duration/estimator.py +++ b/api/src/opentrons/protocols/duration/estimator.py @@ -7,7 +7,7 @@ from dataclasses import dataclass -from opentrons.commands import types +from opentrons.legacy_commands import types from opentrons.protocols.api_support.deck_type import ( guess_from_global_config as guess_deck_type_from_global_config, ) diff --git a/api/src/opentrons/simulate.py b/api/src/opentrons/simulate.py index c5f48c9d1bd..f552a99571f 100644 --- a/api/src/opentrons/simulate.py +++ b/api/src/opentrons/simulate.py @@ -54,7 +54,7 @@ from opentrons.legacy_broker import LegacyBroker from opentrons.config import IS_ROBOT from opentrons import protocol_api -from opentrons.commands import types as command_types +from opentrons.legacy_commands import types as command_types from opentrons.protocols import parse, bundle from opentrons.protocols.types import ( @@ -114,7 +114,7 @@ # TODO(mm, 2023-10-05): Type _SimulateResultRunLog more precisely by using TypedDicts from -# opentrons.commands. +# opentrons.legacy_commands. _SimulateResultRunLog = List[Mapping[str, Any]] _SimulateResult = Tuple[_SimulateResultRunLog, Optional[BundleContents]] @@ -453,7 +453,7 @@ def simulate( - ``payload``: The command. The human-readable run log text is available at ``payload["text"]``. The other keys of ``payload`` are command-dependent; - see ``opentrons.commands``. + see ``opentrons.legacy_commands``. .. note:: In older software versions, ``payload["text"]`` was a diff --git a/api/tests/opentrons/commands/__init__.py b/api/tests/opentrons/commands/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/api/src/opentrons/commands/__init__.py b/api/tests/opentrons/legacy_commands/__init__.py similarity index 100% rename from api/src/opentrons/commands/__init__.py rename to api/tests/opentrons/legacy_commands/__init__.py diff --git a/api/tests/opentrons/commands/test_protocol_commands.py b/api/tests/opentrons/legacy_commands/test_protocol_commands.py similarity index 96% rename from api/tests/opentrons/commands/test_protocol_commands.py rename to api/tests/opentrons/legacy_commands/test_protocol_commands.py index e7fb31aed1c..1ff5475f95b 100644 --- a/api/tests/opentrons/commands/test_protocol_commands.py +++ b/api/tests/opentrons/legacy_commands/test_protocol_commands.py @@ -1,5 +1,5 @@ import pytest -from opentrons.commands import protocol_commands +from opentrons.legacy_commands import protocol_commands @pytest.mark.parametrize( diff --git a/api/tests/opentrons/commands/test_publisher.py b/api/tests/opentrons/legacy_commands/test_publisher.py similarity index 97% rename from api/tests/opentrons/commands/test_publisher.py rename to api/tests/opentrons/legacy_commands/test_publisher.py index a88e6c04523..359b6b3c5fd 100644 --- a/api/tests/opentrons/commands/test_publisher.py +++ b/api/tests/opentrons/legacy_commands/test_publisher.py @@ -1,12 +1,16 @@ -"""Tests for opentrons.commands.publisher.""" +"""Tests for opentrons.legacy_commands.publisher.""" from __future__ import annotations import pytest from decoy import Decoy, matchers from typing import Any, Dict, cast from opentrons.legacy_broker import LegacyBroker -from opentrons.commands.types import Command as CommandDict, CommandMessage -from opentrons.commands.publisher import CommandPublisher, publish, publish_context +from opentrons.legacy_commands.types import Command as CommandDict, CommandMessage +from opentrons.legacy_commands.publisher import ( + CommandPublisher, + publish, + publish_context, +) @pytest.fixture diff --git a/api/tests/opentrons/protocol_runner/test_legacy_command_mapper.py b/api/tests/opentrons/protocol_runner/test_legacy_command_mapper.py index 8a8ec50b779..23b7ecac3bb 100644 --- a/api/tests/opentrons/protocol_runner/test_legacy_command_mapper.py +++ b/api/tests/opentrons/protocol_runner/test_legacy_command_mapper.py @@ -7,7 +7,7 @@ from decoy import matchers, Decoy from opentrons.hardware_control.dev_types import PipetteDict -from opentrons.commands.types import CommentMessage, PauseMessage, CommandMessage +from opentrons.legacy_commands.types import CommentMessage, PauseMessage, CommandMessage from opentrons.protocol_engine import ( DeckSlotLocation, ModuleLocation, diff --git a/api/tests/opentrons/protocol_runner/test_legacy_context_plugin.py b/api/tests/opentrons/protocol_runner/test_legacy_context_plugin.py index 1f7de8388ca..f11676bcd37 100644 --- a/api/tests/opentrons/protocol_runner/test_legacy_context_plugin.py +++ b/api/tests/opentrons/protocol_runner/test_legacy_context_plugin.py @@ -5,7 +5,10 @@ from datetime import datetime from typing import Callable -from opentrons.commands.types import CommandMessage as LegacyCommand, PauseMessage +from opentrons.legacy_commands.types import ( + CommandMessage as LegacyCommand, + PauseMessage, +) from opentrons.protocol_engine import ( StateView, actions as pe_actions, diff --git a/api/tests/opentrons/protocols/duration/test_estimator.py b/api/tests/opentrons/protocols/duration/test_estimator.py index 92614869641..594f1cfad57 100644 --- a/api/tests/opentrons/protocols/duration/test_estimator.py +++ b/api/tests/opentrons/protocols/duration/test_estimator.py @@ -3,7 +3,7 @@ import math import pytest -from opentrons.commands import types +from opentrons.legacy_commands import types from opentrons.protocol_api import InstrumentContext from opentrons.protocols.duration.estimator import ( DurationEstimator, diff --git a/api/tests/opentrons/test_legacy_broker.py b/api/tests/opentrons/test_legacy_broker.py index 2351f73e348..719fe43052d 100644 --- a/api/tests/opentrons/test_legacy_broker.py +++ b/api/tests/opentrons/test_legacy_broker.py @@ -2,8 +2,8 @@ from typing import List, NamedTuple, cast -from opentrons.commands.types import CommandMessage -from opentrons.commands.publisher import CommandPublisher, publish +from opentrons.legacy_commands.types import CommandMessage +from opentrons.legacy_commands.publisher import CommandPublisher, publish def _my_command(arg1: int, arg2: str = "", arg3: str = "") -> CommandMessage: From f95af4f98955411eeacd518982263591319c8dec Mon Sep 17 00:00:00 2001 From: Max Marrone Date: Thu, 4 Apr 2024 12:17:24 -0400 Subject: [PATCH 042/194] refactor(protocol-engine): Keep track of failed commands' error recovery types (#14795) --- .../protocol_engine/state/commands.py | 32 +++++++- .../state/test_command_state.py | 79 +++++++++++++++++++ ...and_store.py => test_command_store_old.py} | 24 +++++- ...mmand_view.py => test_command_view_old.py} | 13 ++- 4 files changed, 142 insertions(+), 6 deletions(-) create mode 100644 api/tests/opentrons/protocol_engine/state/test_command_state.py rename api/tests/opentrons/protocol_engine/state/{test_command_store.py => test_command_store_old.py} (98%) rename api/tests/opentrons/protocol_engine/state/{test_command_view.py => test_command_view_old.py} (98%) diff --git a/api/src/opentrons/protocol_engine/state/commands.py b/api/src/opentrons/protocol_engine/state/commands.py index ab4d3b8f5cb..2c66e45826d 100644 --- a/api/src/opentrons/protocol_engine/state/commands.py +++ b/api/src/opentrons/protocol_engine/state/commands.py @@ -4,7 +4,7 @@ import enum from dataclasses import dataclass from datetime import datetime -from typing import List, Optional, Union +from typing import Dict, List, Optional, Union from typing_extensions import assert_never from opentrons_shared_data.errors import EnumeratedError, ErrorCodes, PythonException @@ -164,6 +164,19 @@ class CommandState: # that we're doing error recovery. See if we can implement robot-server pagination # atop simpler concepts, like "the last command that ran" or "the next command that # would run." + # + # TODO(mm, 2024-04-03): Can this be replaced by + # CommandHistory.get_terminal_command() now? + + command_error_recovery_types: Dict[str, ErrorRecoveryType] + """For each command that failed (indexed by ID), what its recovery type was. + + This only includes commands that actually failed, not the ones that we mark as + failed but that are effectively "cancelled" because a command before them failed. + + This separate attribute is a stopgap until error recovery concepts are a bit more + stable. Eventually, we might want this info to be stored directly on each command. + """ finish_error: Optional[ErrorOccurrence] """The error that happened during the post-run finish steps (homing & dropping tips), if any.""" @@ -199,6 +212,7 @@ def __init__( run_error=None, finish_error=None, failed_command=None, + command_error_recovery_types={}, run_completed_at=None, run_started_at=None, latest_command_hash=None, @@ -253,11 +267,11 @@ def handle_action(self, action: Action) -> None: # noqa: C901 error=action.error, ) - # TODO(mc, 2022-06-06): add new "cancelled" status or similar self._update_to_failed( command_id=action.command_id, failed_at=action.failed_at, error_occurrence=error_occurrence, + error_recovery_type=action.type, notes=action.notes, ) @@ -271,10 +285,12 @@ def handle_action(self, action: Action) -> None: # noqa: C901 self._state.command_history.get_setup_queue_ids() ) for command_id in other_command_ids_to_fail: + # TODO(mc, 2022-06-06): add new "cancelled" status or similar self._update_to_failed( command_id=command_id, failed_at=action.failed_at, error_occurrence=None, + error_recovery_type=None, notes=None, ) self._state.command_history.clear_setup_queue() @@ -289,10 +305,12 @@ def handle_action(self, action: Action) -> None: # noqa: C901 self._state.command_history.get_queue_ids() ) for command_id in other_command_ids_to_fail: + # TODO(mc, 2022-06-06): add new "cancelled" status or similar self._update_to_failed( command_id=command_id, failed_at=action.failed_at, error_occurrence=None, + error_recovery_type=None, notes=None, ) self._state.command_history.clear_queue() @@ -376,6 +394,7 @@ def _update_to_failed( command_id: str, failed_at: datetime, error_occurrence: Optional[ErrorOccurrence], + error_recovery_type: Optional[ErrorRecoveryType], notes: Optional[List[CommandNote]], ) -> None: prev_entry = self._state.command_history.get(command_id) @@ -391,6 +410,8 @@ def _update_to_failed( } ) self._state.command_history.set_command_failed(failed_command) + if error_recovery_type is not None: + self._state.command_error_recovery_types[command_id] = error_recovery_type @staticmethod def _map_run_exception_to_error_occurrence( @@ -709,6 +730,13 @@ def raise_fatal_command_error(self) -> None: message=failed_command.command.error.detail, ) + def get_error_recovery_type(self, command_id: str) -> ErrorRecoveryType: + """Return the error recovery type with which the given command failed. + + The command ID is assumed to point to a failed command. + """ + return self.state.command_error_recovery_types[command_id] + def get_is_stopped(self) -> bool: """Get whether an engine stop has completed.""" return self._state.run_completed_at is not None diff --git a/api/tests/opentrons/protocol_engine/state/test_command_state.py b/api/tests/opentrons/protocol_engine/state/test_command_state.py new file mode 100644 index 00000000000..001b1b7640c --- /dev/null +++ b/api/tests/opentrons/protocol_engine/state/test_command_state.py @@ -0,0 +1,79 @@ +"""Tests for the CommandStore+CommandState+CommandView trifecta. + +The trifecta is tested here as a single unit, treating CommandState as a private +implementation detail. +""" + +from datetime import datetime + +from opentrons_shared_data.errors import PythonException + +from opentrons.protocol_engine import actions, commands +from opentrons.protocol_engine.error_recovery_policy import ErrorRecoveryType +from opentrons.protocol_engine.state.commands import CommandStore, CommandView +from opentrons.protocol_engine.state.config import Config +from opentrons.protocol_engine.types import DeckType + + +def _make_config() -> Config: + return Config( + # Choice of robot and deck type is arbitrary. + robot_type="OT-2 Standard", + deck_type=DeckType.OT2_STANDARD, + ) + + +def test_error_recovery_type_tracking() -> None: + """It should keep track of each failed command's error recovery type.""" + subject = CommandStore(config=_make_config(), is_door_open=False) + + subject.handle_action( + actions.QueueCommandAction( + command_id="c1", + created_at=datetime.now(), + request=commands.CommentCreate( + params=commands.CommentParams(message="yeehaw"), + ), + request_hash=None, + ) + ) + subject.handle_action( + actions.QueueCommandAction( + command_id="c2", + created_at=datetime.now(), + request=commands.CommentCreate( + params=commands.CommentParams(message="yeehaw"), + ), + request_hash=None, + ) + ) + subject.handle_action( + actions.RunCommandAction(command_id="c1", started_at=datetime.now()) + ) + subject.handle_action( + actions.FailCommandAction( + command_id="c1", + error_id="c1-error", + failed_at=datetime.now(), + error=PythonException(RuntimeError("new sheriff in town")), + notes=[], + type=ErrorRecoveryType.WAIT_FOR_RECOVERY, + ) + ) + subject.handle_action( + actions.RunCommandAction(command_id="c2", started_at=datetime.now()) + ) + subject.handle_action( + actions.FailCommandAction( + command_id="c2", + error_id="c2-error", + failed_at=datetime.now(), + error=PythonException(RuntimeError("new sheriff in town")), + notes=[], + type=ErrorRecoveryType.FAIL_RUN, + ) + ) + + view = CommandView(subject.state) + assert view.get_error_recovery_type("c1") == ErrorRecoveryType.WAIT_FOR_RECOVERY + assert view.get_error_recovery_type("c2") == ErrorRecoveryType.FAIL_RUN diff --git a/api/tests/opentrons/protocol_engine/state/test_command_store.py b/api/tests/opentrons/protocol_engine/state/test_command_store_old.py similarity index 98% rename from api/tests/opentrons/protocol_engine/state/test_command_store.py rename to api/tests/opentrons/protocol_engine/state/test_command_store_old.py index d5bfc1e963a..7afde4a6e4b 100644 --- a/api/tests/opentrons/protocol_engine/state/test_command_store.py +++ b/api/tests/opentrons/protocol_engine/state/test_command_store_old.py @@ -1,4 +1,10 @@ -"""Tests for the command lifecycle state.""" +"""Tests for CommandStore. + +DEPRECATED: Testing CommandStore independently of CommandView is no longer helpful. +Add new tests to test_command_state.py, where they can be tested together. +""" + + import pytest from datetime import datetime from typing import NamedTuple, Type @@ -79,6 +85,7 @@ def test_initial_state( run_error=None, finish_error=None, failed_command=None, + command_error_recovery_types={}, latest_command_hash=None, stopped_by_estop=False, ) @@ -826,6 +833,7 @@ def test_command_store_handles_pause_action(pause_source: PauseSource) -> None: run_error=None, finish_error=None, failed_command=None, + command_error_recovery_types={}, latest_command_hash=None, stopped_by_estop=False, ) @@ -850,6 +858,7 @@ def test_command_store_handles_play_action(pause_source: PauseSource) -> None: run_error=None, finish_error=None, failed_command=None, + command_error_recovery_types={}, run_started_at=datetime(year=2021, month=1, day=1), latest_command_hash=None, stopped_by_estop=False, @@ -880,6 +889,7 @@ def test_command_store_handles_finish_action() -> None: run_error=None, finish_error=None, failed_command=None, + command_error_recovery_types={}, run_started_at=datetime(year=2021, month=1, day=1), latest_command_hash=None, stopped_by_estop=False, @@ -925,6 +935,7 @@ def test_command_store_handles_stop_action(from_estop: bool) -> None: run_error=None, finish_error=None, failed_command=None, + command_error_recovery_types={}, run_started_at=datetime(year=2021, month=1, day=1), latest_command_hash=None, stopped_by_estop=from_estop, @@ -954,6 +965,7 @@ def test_command_store_cannot_restart_after_should_stop() -> None: run_error=None, finish_error=None, failed_command=None, + command_error_recovery_types={}, run_started_at=None, latest_command_hash=None, stopped_by_estop=False, @@ -1085,6 +1097,7 @@ def test_command_store_wraps_unknown_errors() -> None: ), run_started_at=None, failed_command=None, + command_error_recovery_types={}, latest_command_hash=None, stopped_by_estop=False, ) @@ -1145,6 +1158,7 @@ def __init__(self, message: str) -> None: errorCode=ErrorCodes.PIPETTE_NOT_PRESENT.value.code, ), failed_command=None, + command_error_recovery_types={}, run_started_at=None, latest_command_hash=None, stopped_by_estop=False, @@ -1176,6 +1190,7 @@ def test_command_store_ignores_stop_after_graceful_finish() -> None: run_error=None, finish_error=None, failed_command=None, + command_error_recovery_types={}, run_started_at=datetime(year=2021, month=1, day=1), latest_command_hash=None, stopped_by_estop=False, @@ -1207,6 +1222,7 @@ def test_command_store_ignores_finish_after_non_graceful_stop() -> None: run_error=None, finish_error=None, failed_command=None, + command_error_recovery_types={}, run_started_at=datetime(year=2021, month=1, day=1), latest_command_hash=None, stopped_by_estop=False, @@ -1219,6 +1235,8 @@ def test_command_store_ignores_finish_after_non_graceful_stop() -> None: def test_command_store_handles_command_failed() -> None: """It should store an error and mark the command if it fails.""" + error_recovery_type = ErrorRecoveryType.FAIL_RUN + expected_error_occurrence = errors.ErrorOccurrence( id="error-id", errorType="ProtocolEngineError", @@ -1281,7 +1299,7 @@ def test_command_store_handles_command_failed() -> None: source="source", ) ], - type=ErrorRecoveryType.FAIL_RUN, + type=error_recovery_type, ) ) @@ -1299,6 +1317,7 @@ def test_command_store_handles_command_failed() -> None: run_error=None, finish_error=None, failed_command=failed_command_entry, + command_error_recovery_types={expected_failed_command.id: error_recovery_type}, run_started_at=None, latest_command_hash=None, stopped_by_estop=False, @@ -1327,6 +1346,7 @@ def test_handles_hardware_stopped() -> None: run_error=None, finish_error=None, failed_command=None, + command_error_recovery_types={}, run_started_at=None, latest_command_hash=None, stopped_by_estop=False, diff --git a/api/tests/opentrons/protocol_engine/state/test_command_view.py b/api/tests/opentrons/protocol_engine/state/test_command_view_old.py similarity index 98% rename from api/tests/opentrons/protocol_engine/state/test_command_view.py rename to api/tests/opentrons/protocol_engine/state/test_command_view_old.py index 047230d4f6d..64d7670f662 100644 --- a/api/tests/opentrons/protocol_engine/state/test_command_view.py +++ b/api/tests/opentrons/protocol_engine/state/test_command_view_old.py @@ -1,8 +1,14 @@ -"""Labware state store tests.""" +"""Tests for CommandView. + +DEPRECATED: Testing CommandView independently of CommandStore is no longer helpful. +Add new tests to test_command_state.py, where they can be tested together. +""" + + import pytest from contextlib import nullcontext as does_not_raise from datetime import datetime -from typing import List, NamedTuple, Optional, Sequence, Type, Union +from typing import Dict, List, NamedTuple, Optional, Sequence, Type, Union from opentrons.protocol_engine import EngineStatus, commands as cmd, errors from opentrons.protocol_engine.actions import ( @@ -14,6 +20,7 @@ ) from opentrons.protocol_engine.actions.actions import ResumeFromRecoveryAction +from opentrons.protocol_engine.error_recovery_policy import ErrorRecoveryType from opentrons.protocol_engine.state.commands import ( CommandState, CommandView, @@ -50,6 +57,7 @@ def get_command_view( queued_setup_command_ids: Sequence[str] = (), run_error: Optional[errors.ErrorOccurrence] = None, failed_command: Optional[CommandEntry] = None, + command_error_recovery_types: Optional[Dict[str, ErrorRecoveryType]] = None, finish_error: Optional[errors.ErrorOccurrence] = None, commands: Sequence[cmd.Command] = (), latest_command_hash: Optional[str] = None, @@ -81,6 +89,7 @@ def get_command_view( run_error=run_error, finish_error=finish_error, failed_command=failed_command, + command_error_recovery_types=command_error_recovery_types or {}, run_started_at=run_started_at, latest_command_hash=latest_command_hash, stopped_by_estop=False, From 3c5d1604c4b271f8ddb355fc067c540b700efb06 Mon Sep 17 00:00:00 2001 From: Ryan Howard Date: Thu, 4 Apr 2024 12:28:21 -0400 Subject: [PATCH 043/194] feat(hardware-testing): collected work for upgrading scripts to PE (#14245) # Overview This upgrades our pipette QC scripts to using protocol engine interface, this simplifies a lot of the code and lets us use new features: push out labware adapters using the normal ot3 deck definitions partial tip configuration With the reduction in the amount of changes to the base repo required, we now don't need to use patches before running the tests, this means we only need the hardware-testing directory to be pushed to the bot with no changes to the base software. # Test Plan # Changelog # Review requests # Risk assessment --- .../backends/ot3controller.py | 5 + .../hardware_control/backends/ot3simulator.py | 16 +- api/src/opentrons/hardware_control/ot3api.py | 5 + .../protocol_api/core/engine/instrument.py | 5 + .../opentrons/protocol_api/core/instrument.py | 3 + .../core/legacy/legacy_instrument_core.py | 4 + .../legacy_instrument_core.py | 4 + .../protocol_api/instrument_context.py | 6 + api/src/opentrons/simulate.py | 18 +- hardware-testing/Makefile | 20 +- .../hardware_testing/drivers/asair_sensor.py | 2 +- .../hardware_testing/gravimetric/__main__.py | 89 +- .../hardware_testing/gravimetric/config.py | 5 +- .../gravimetric/daily_setup.py | 5 +- .../hardware_testing/gravimetric/execute.py | 57 +- .../gravimetric/execute_photometric.py | 14 +- .../hardware_testing/gravimetric/helpers.py | 175 ++- .../gravimetric/liquid_class/defaults.py | 38 +- .../gravimetric/liquid_class/pipetting.py | 56 +- .../gravimetric/overrides/api.patch | 111 -- .../gravimetric/overrides/shared-data.patch | 1052 +++-------------- .../hardware_testing/gravimetric/tips.py | 24 +- .../gravimetric/workarounds.py | 17 +- .../1.json | 1017 ---------------- .../1.json | 1017 ---------------- .../opentrons_flex_96_tiprack_50ul_adp/1.json | 1017 ---------------- .../opentrons_api/helpers_ot3.py | 2 +- .../gravimetric/gravimetric_ot3_p1000_96.py | 37 +- .../photometric/photometric_ot3_p1000_96.py | 38 +- .../hardware_testing/liquid/test_heights.py | 2 +- 30 files changed, 549 insertions(+), 4312 deletions(-) delete mode 100644 hardware-testing/hardware_testing/labware/opentrons_flex_96_tiprack_1000ul_adp/1.json delete mode 100644 hardware-testing/hardware_testing/labware/opentrons_flex_96_tiprack_200ul_adp/1.json delete mode 100644 hardware-testing/hardware_testing/labware/opentrons_flex_96_tiprack_50ul_adp/1.json diff --git a/api/src/opentrons/hardware_control/backends/ot3controller.py b/api/src/opentrons/hardware_control/backends/ot3controller.py index 83439c0896b..0edf7e4dfd3 100644 --- a/api/src/opentrons/hardware_control/backends/ot3controller.py +++ b/api/src/opentrons/hardware_control/backends/ot3controller.py @@ -1647,3 +1647,8 @@ async def get_hepa_uv_state(self) -> Optional[HepaUVState]: if res else None ) + + def _update_tip_state(self, mount: OT3Mount, status: bool) -> None: + """This is something we only use in the simulator. + It is required so that PE simulations using ot3api don't break.""" + pass diff --git a/api/src/opentrons/hardware_control/backends/ot3simulator.py b/api/src/opentrons/hardware_control/backends/ot3simulator.py index 638b0094a85..741018adc52 100644 --- a/api/src/opentrons/hardware_control/backends/ot3simulator.py +++ b/api/src/opentrons/hardware_control/backends/ot3simulator.py @@ -506,13 +506,20 @@ def _attached_pipette_to_mount( ), "id": None, } - if found_model and expected_instr or found_model: + if found_model and init_instr["id"] is not None: # Instrument detected matches instrument expected (note: # "instrument detected" means passed as an argument to the # constructor of this class) # OR Instrument detected and no expected instrument specified - converted_name = pipette_load_name.convert_pipette_model(found_model) + + found_model_version = "" + if found_model.find("flex") > -1: + found_model = found_model.replace("_flex", "") # type: ignore + found_model_version = f"{init_instr['id'][4]}.{init_instr['id'][5]}" + converted_name = pipette_load_name.convert_pipette_model( + found_model, found_model_version + ) return { "config": load_pipette_data.load_definition( converted_name.pipette_type, @@ -843,3 +850,8 @@ async def set_hepa_uv_state(self, light_on: bool, timeout_s: int) -> bool: async def get_hepa_uv_state(self) -> Optional[HepaUVState]: return None + + def _update_tip_state(self, mount: OT3Mount, status: bool) -> None: + """This is something we only use in the simulator. + It is required so that PE simulations using ot3api don't break.""" + self._sim_tip_state[mount] = status diff --git a/api/src/opentrons/hardware_control/ot3api.py b/api/src/opentrons/hardware_control/ot3api.py index ae7be339673..e6ae891359b 100644 --- a/api/src/opentrons/hardware_control/ot3api.py +++ b/api/src/opentrons/hardware_control/ot3api.py @@ -2133,6 +2133,8 @@ async def pick_up_tip( def add_tip_to_instr() -> None: instrument.add_tip(tip_length=tip_length) instrument.set_current_volume(0) + if isinstance(self._backend, OT3Simulator): + self._backend._update_tip_state(realmount, True) await self._move_to_plunger_bottom(realmount, rate=1.0) if ( @@ -2233,6 +2235,9 @@ def _remove_tips() -> None: await self._home([Axis.by_mount(mount)]) _remove_tips() + # call this in case we're simulating + if isinstance(self._backend, OT3Simulator): + self._backend._update_tip_state(realmount, False) async def clean_up(self) -> None: """Get the API ready to stop cleanly.""" diff --git a/api/src/opentrons/protocol_api/core/engine/instrument.py b/api/src/opentrons/protocol_api/core/engine/instrument.py index 6bf569bcd67..9c88a4f7ecb 100644 --- a/api/src/opentrons/protocol_api/core/engine/instrument.py +++ b/api/src/opentrons/protocol_api/core/engine/instrument.py @@ -777,3 +777,8 @@ def configure_nozzle_layout( self._engine_client.configure_nozzle_layout( pipette_id=self._pipette_id, configuration_params=configuration_model ) + + def retract(self) -> None: + """Retract this instrument to the top of the gantry.""" + z_axis = self._engine_client.state.pipettes.get_z_axis(self._pipette_id) + self._engine_client.home([z_axis]) diff --git a/api/src/opentrons/protocol_api/core/instrument.py b/api/src/opentrons/protocol_api/core/instrument.py index 061e7d13960..fec252a009e 100644 --- a/api/src/opentrons/protocol_api/core/instrument.py +++ b/api/src/opentrons/protocol_api/core/instrument.py @@ -289,6 +289,9 @@ def configure_nozzle_layout( @abstractmethod def is_tip_tracking_available(self) -> bool: """Return whether auto tip tracking is available for the pipette's current nozzle configuration.""" + + def retract(self) -> None: + """Retract this instrument to the top of the gantry.""" ... diff --git a/api/src/opentrons/protocol_api/core/legacy/legacy_instrument_core.py b/api/src/opentrons/protocol_api/core/legacy/legacy_instrument_core.py index 57f129c32b3..3755b093e78 100644 --- a/api/src/opentrons/protocol_api/core/legacy/legacy_instrument_core.py +++ b/api/src/opentrons/protocol_api/core/legacy/legacy_instrument_core.py @@ -558,3 +558,7 @@ def get_nozzle_map(self) -> NozzleMap: def is_tip_tracking_available(self) -> bool: # Tip tracking is always available in legacy context return True + + def retract(self) -> None: + """Retract this instrument to the top of the gantry.""" + self._protocol_interface.get_hardware.retract(self._mount) # type: ignore [attr-defined] diff --git a/api/src/opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py b/api/src/opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py index 2ee61adf24e..ffcdda5019c 100644 --- a/api/src/opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py +++ b/api/src/opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py @@ -476,3 +476,7 @@ def get_nozzle_map(self) -> NozzleMap: def is_tip_tracking_available(self) -> bool: # Tip tracking is always available in legacy context return True + + def retract(self) -> None: + """Retract this instrument to the top of the gantry.""" + self._protocol_interface.get_hardware.retract(self._mount) # type: ignore [attr-defined] diff --git a/api/src/opentrons/protocol_api/instrument_context.py b/api/src/opentrons/protocol_api/instrument_context.py index 26f24899fad..e070b896a6e 100644 --- a/api/src/opentrons/protocol_api/instrument_context.py +++ b/api/src/opentrons/protocol_api/instrument_context.py @@ -1532,6 +1532,12 @@ def move_to( return self + @requires_version(2, 18) + def _retract( + self, + ) -> None: + self._core.retract() + @property @requires_version(2, 0) def mount(self) -> str: diff --git a/api/src/opentrons/simulate.py b/api/src/opentrons/simulate.py index f552a99571f..9626fa86b96 100644 --- a/api/src/opentrons/simulate.py +++ b/api/src/opentrons/simulate.py @@ -223,6 +223,7 @@ def get_protocol_api( # type checking, like Jupyter Notebook. *, robot_type: Optional[_UserSpecifiedRobotType] = None, + use_virtual_hardware: bool = True, ) -> protocol_api.ProtocolContext: """ Build and return a ``protocol_api.ProtocolContext`` @@ -260,6 +261,7 @@ def get_protocol_api( :param robot_type: The type of robot to simulate: either ``"Flex"`` or ``"OT-2"``. If you're running this function on a robot, the default is the type of that robot. Otherwise, the default is ``"OT-2"``, for backwards compatibility. + :param use_virtual_hardware: If true, use the protocol engines virtual hardware, if false use the lower level hardware simulator. :return: The protocol context. """ if isinstance(version, str): @@ -317,6 +319,7 @@ def get_protocol_api( hardware_api=checked_hardware, bundled_data=bundled_data, extra_labware=extra_labware, + use_virtual_hardware=use_virtual_hardware, ) # Intentional difference from execute.get_protocol_api(): @@ -790,6 +793,7 @@ def _create_live_context_pe( deck_type: str, extra_labware: Dict[str, "LabwareDefinitionDict"], bundled_data: Optional[Dict[str, bytes]], + use_virtual_hardware: bool = True, ) -> ProtocolContext: """Return a live ProtocolContext that controls the robot through ProtocolEngine.""" assert api_version >= ENGINE_CORE_API_VERSION @@ -798,7 +802,9 @@ def _create_live_context_pe( pe, loop = _LIVE_PROTOCOL_ENGINE_CONTEXTS.enter_context( create_protocol_engine_in_thread( hardware_api=hardware_api.wrapped(), - config=_get_protocol_engine_config(robot_type), + config=_get_protocol_engine_config( + robot_type, virtual=use_virtual_hardware + ), drop_tips_after_run=False, post_run_hardware_state=PostRunHardwareState.STAY_ENGAGED_IN_PLACE, load_fixed_trash=should_load_fixed_trash_labware_for_python_protocol( @@ -899,7 +905,7 @@ def _run_file_pe( async def run(protocol_source: ProtocolSource) -> _SimulateResult: protocol_engine = await create_protocol_engine( hardware_api=hardware_api.wrapped(), - config=_get_protocol_engine_config(robot_type), + config=_get_protocol_engine_config(robot_type, virtual=True), load_fixed_trash=should_load_fixed_trash(protocol_source.config), ) @@ -934,15 +940,15 @@ async def run(protocol_source: ProtocolSource) -> _SimulateResult: return asyncio.run(run(protocol_source)) -def _get_protocol_engine_config(robot_type: RobotType) -> Config: +def _get_protocol_engine_config(robot_type: RobotType, virtual: bool) -> Config: """Return a Protocol Engine config to execute protocols on this device.""" return Config( robot_type=robot_type, deck_type=DeckType(deck_type_for_simulation(robot_type)), ignore_pause=True, - use_virtual_pipettes=True, - use_virtual_modules=True, - use_virtual_gripper=True, + use_virtual_pipettes=virtual, + use_virtual_modules=virtual, + use_virtual_gripper=virtual, use_simulated_deck_config=True, ) diff --git a/hardware-testing/Makefile b/hardware-testing/Makefile index 5e6d7264113..6c12dc305a0 100755 --- a/hardware-testing/Makefile +++ b/hardware-testing/Makefile @@ -79,36 +79,26 @@ sdist: .PHONY: test test: - -$(MAKE) apply-patches-gravimetric $(pytest) $(tests) $(test_opts) - -$(MAKE) remove-patches-gravimetric .PHONY: test-cov test-cov: - -$(MAKE) apply-patches-gravimetric $(pytest) $(tests) $(test_opts) $(cov_opts) - -$(MAKE) remove-patches-gravimetric .PHONY: test-photometric-single test-photometric-single: - -$(MAKE) apply-patches-gravimetric $(python) -m hardware_testing.gravimetric --photometric --simulate --pipette 50 --channels 1 --tip 50 $(python) -m hardware_testing.gravimetric --photometric --simulate --pipette 50 --channels 1 --tip 50 --photoplate-col-offset 3 $(python) -m hardware_testing.gravimetric --photometric --simulate --pipette 50 --channels 1 --tip 50 --dye-well-col-offset 3 - -$(MAKE) remove-patches-gravimetric .PHONY: test-photometric-multi test-photometric-multi: - -$(MAKE) apply-patches-gravimetric $(python) -m hardware_testing.gravimetric --photometric --simulate --pipette 50 --channels 8 --tip 50 - -$(MAKE) remove-patches-gravimetric .PHONY: test-photometric test-photometric: - -$(MAKE) apply-patches-gravimetric $(python) -m hardware_testing.gravimetric --photometric --simulate --pipette 1000 --channels 96 --tip 50 --trials 1 $(python) -m hardware_testing.gravimetric --photometric --simulate --pipette 1000 --channels 96 --tip 200 --trials 1 - -$(MAKE) remove-patches-gravimetric .PHONY: test-gravimetric-single test-gravimetric-single: @@ -134,14 +124,12 @@ test-gravimetric-96: .PHONY: test-gravimetric test-gravimetric: - -$(MAKE) apply-patches-gravimetric $(python) -m hardware_testing.gravimetric.daily_setup --simulate $(python) -m hardware_testing.gravimetric.daily_setup --simulate --calibrate $(MAKE) test-gravimetric-single $(MAKE) test-gravimetric-multi $(MAKE) test-gravimetric-96 $(MAKE) test-photometric - -$(MAKE) remove-patches-gravimetric .PHONY: test-production-qc test-production-qc: @@ -172,11 +160,9 @@ test-integration: test-production-qc test-examples test-scripts test-gravimetric .PHONY: lint lint: - -$(MAKE) apply-patches-gravimetric $(python) -m mypy hardware_testing tests $(python) -m black --check hardware_testing tests setup.py $(python) -m flake8 hardware_testing tests setup.py - -$(MAKE) remove-patches-gravimetric .PHONY: format format: @@ -297,9 +283,11 @@ sync-ot3: sync-sw-ot3 sync-fw-ot3 .PHONY: push-ot3-gravimetric push-ot3-gravimetric: + $(MAKE) push-ot3 + ssh $(ssh_helper_ot3) root@$(host) "mkdir -p /data/labware/v2/custom_definitions/custom_beta" + scp $(ssh_helper_ot3) -r hardware_testing/labware/* root@$(host):/data/labware/v2/custom_definitions/custom_beta/ $(MAKE) apply-patches-gravimetric - -$(MAKE) sync-sw-ot3 - scp $(ssh_helper_ot3) -r hardware_testing/labware root@$(host):/data/labware/v2/custom_definitions/custom_beta/ + cd ../ && $(MAKE) -C shared-data push-ot3 $(MAKE) remove-patches-gravimetric .PHONY: apply-patches-gravimetric diff --git a/hardware-testing/hardware_testing/drivers/asair_sensor.py b/hardware-testing/hardware_testing/drivers/asair_sensor.py index 350741ebc79..00b73893e6d 100644 --- a/hardware-testing/hardware_testing/drivers/asair_sensor.py +++ b/hardware-testing/hardware_testing/drivers/asair_sensor.py @@ -92,7 +92,7 @@ def BuildAsairSensor(simulate: bool, autosearch: bool = True) -> AsairSensorBase ui.print_info(f"Trying to connect to env sensor on port {port}") sensor = AsairSensor.connect(port) ser_id = sensor.get_serial() - if len(ser_id) != 0: + if ser_id == " ": ui.print_info(f"Found env sensor {ser_id} on port {port}") return sensor except: # noqa: E722 diff --git a/hardware-testing/hardware_testing/gravimetric/__main__.py b/hardware-testing/hardware_testing/gravimetric/__main__.py index 54a8278adef..0855345598b 100644 --- a/hardware-testing/hardware_testing/gravimetric/__main__.py +++ b/hardware-testing/hardware_testing/gravimetric/__main__.py @@ -2,10 +2,8 @@ from json import load as json_load from pathlib import Path import argparse -from time import time from typing import List, Union, Dict, Optional, Any, Tuple from dataclasses import dataclass -from opentrons.hardware_control.types import OT3Mount from opentrons.protocol_api import ProtocolContext from . import report import subprocess @@ -42,16 +40,15 @@ from .measurement.record import GravimetricRecorder from .measurement import DELAY_FOR_MEASUREMENT from .measurement.scale import Scale -from .measurement.environment import read_environment_data from .trial import TestResources, _change_pipettes from .tips import get_tips from hardware_testing.drivers import asair_sensor from opentrons.protocol_api import InstrumentContext +from opentrons.protocol_engine.types import LabwareOffset -# FIXME: bump to v2.15 to utilize protocol engine -API_LEVEL = "2.13" +API_LEVEL = "2.18" -LABWARE_OFFSETS: List[dict] = [] +LABWARE_OFFSETS: List[LabwareOffset] = [] # Keyed by pipette volume, channel count, and tip volume in that order GRAVIMETRIC_CFG = { @@ -90,6 +87,19 @@ }, } +PIPETTE_MODEL_NAME = { + 50: { + 1: "p50_single_flex", + 8: "p50_multi_flex", + }, + 1000: { + 1: "p1000_single_flex", + 8: "p1000_multi_flex", + 96: "p1000_96_flex", + }, +} + + PHOTOMETRIC_CFG = { 50: { 1: { @@ -148,22 +158,18 @@ def _get_protocol_context(cls, args: argparse.Namespace) -> ProtocolContext: ui.print_info( "Starting opentrons-robot-server, so we can http GET labware offsets" ) - offsets = workarounds.http_get_all_labware_offsets() - ui.print_info(f"found {len(offsets)} offsets:") - for offset in offsets: - ui.print_info(f"\t{offset['createdAt']}:") - ui.print_info(f"\t\t{offset['definitionUri']}") - ui.print_info(f"\t\t{offset['vector']}") - LABWARE_OFFSETS.append(offset) + LABWARE_OFFSETS.extend(workarounds.http_get_all_labware_offsets()) + ui.print_info(f"found {len(LABWARE_OFFSETS)} offsets:") + for offset in LABWARE_OFFSETS: + ui.print_info(f"\t{offset.createdAt}:") + ui.print_info(f"\t\t{offset.definitionUri}") + ui.print_info(f"\t\t{offset.vector}") # gather the custom labware (for simulation) custom_defs = {} if args.simulate: labware_dir = Path(__file__).parent.parent / "labware" custom_def_uris = [ "radwag_pipette_calibration_vial", - "opentrons_flex_96_tiprack_50ul_adp", - "opentrons_flex_96_tiprack_200ul_adp", - "opentrons_flex_96_tiprack_1000ul_adp", ] for def_uri in custom_def_uris: with open(labware_dir / def_uri / "1.json", "r") as f: @@ -172,9 +178,12 @@ def _get_protocol_context(cls, args: argparse.Namespace) -> ProtocolContext: _ctx = helpers.get_api_context( API_LEVEL, # type: ignore[attr-defined] is_simulating=args.simulate, - deck_version="2", + pipette_left=PIPETTE_MODEL_NAME[args.pipette][args.channels], extra_labware=custom_defs, ) + for offset in LABWARE_OFFSETS: + engine = _ctx._core._engine_client._transport._engine # type: ignore[attr-defined] + engine.state_view._labware_store._add_labware_offset(offset) return _ctx @classmethod @@ -301,7 +310,7 @@ def build_run_args(cls, args: argparse.Namespace) -> "RunArgs": # noqa: C901 trials=trials, name=name, robot_serial=robot_serial, - fw_version=_ctx._core.get_hardware().fw_version, + fw_version=workarounds.get_sync_hw_api(_ctx).fw_version, ) else: if args.increment: @@ -334,7 +343,7 @@ def build_run_args(cls, args: argparse.Namespace) -> "RunArgs": # noqa: C901 name=name, environment_sensor=environment_sensor, trials=trials, - fw_version=_ctx._core.get_hardware().fw_version, + fw_version=workarounds.get_sync_hw_api(_ctx).fw_version, ) return RunArgs( @@ -387,7 +396,6 @@ def build_gravimetric_cfg( pipette_channels=run_args.pipette_channels, tip_volume=tip_volume, trials=run_args.trials, - labware_offsets=LABWARE_OFFSETS, labware_on_scale=run_args.protocol_cfg.LABWARE_ON_SCALE, # type: ignore[attr-defined] slot_scale=run_args.protocol_cfg.SLOT_SCALE, # type: ignore[attr-defined] slots_tiprack=run_args.protocol_cfg.SLOTS_TIPRACK[tip_volume], # type: ignore[attr-defined] @@ -436,7 +444,6 @@ def build_photometric_cfg( increment=False, tip_volume=tip_volume, trials=run_args.trials, - labware_offsets=LABWARE_OFFSETS, photoplate=run_args.protocol_cfg.PHOTOPLATE_LABWARE, # type: ignore[attr-defined] photoplate_slot=run_args.protocol_cfg.SLOT_PLATE, # type: ignore[attr-defined] reservoir=run_args.protocol_cfg.RESERVOIR_LABWARE, # type: ignore[attr-defined] @@ -569,7 +576,6 @@ def _main( parser.add_argument( "--mode", type=str, choices=["", "default", "lowVolumeDefault"], default="" ) - parser.add_argument("--pre-heat", action="store_true") args = parser.parse_args() run_args = RunArgs.build_run_args(args) if not run_args.ctx.is_simulating(): @@ -580,48 +586,13 @@ def _main( shell=True, ) sleep(1) - hw = run_args.ctx._core.get_hardware() + hw = workarounds.get_sync_hw_api(run_args.ctx) try: if not run_args.ctx.is_simulating() and not args.photometric: ui.get_user_ready("CLOSE the door, and MOVE AWAY from machine") ui.print_info("homing...") run_args.ctx.home() - if args.pre_heat: - ui.print_header("PRE-HEAT") - mnt = OT3Mount.LEFT - hw.add_tip(mnt, 1) - hw.prepare_for_aspirate(mnt) - env_data = read_environment_data( - mnt.name.lower(), hw.is_simulator, run_args.environment_sensor - ) - start_temp = env_data.celsius_pipette - temp_limit = min(start_temp + 3.0, 28.0) - max_pre_heat_seconds = 60 * 10 - now = time() - start_time = now - while ( - now - start_time < max_pre_heat_seconds - and env_data.celsius_pipette < temp_limit - ): - ui.print_info( - f"pre-heat {int(now - start_time)} seconds " - f"({max_pre_heat_seconds} limit): " - f"{round(env_data.celsius_pipette, 2)} C " - f"({round(temp_limit, 2)} C limit)" - ) - # NOTE: moving slowly helps make sure full current is sent to coils - hw.aspirate(mnt, rate=0.1) - hw.dispense(mnt, rate=0.1, push_out=0) - env_data = read_environment_data( - mnt.name.lower(), hw.is_simulator, run_args.environment_sensor - ) - if run_args.ctx.is_simulating(): - now += 1 - else: - now = time() - hw.remove_tip(mnt) - for tip, volumes in run_args.volumes: if args.channels == 96 and not run_args.ctx.is_simulating(): ui.alert_user_ready(f"prepare the {tip}ul tipracks", hw) @@ -634,5 +605,5 @@ def _main( _change_pipettes(run_args.ctx, run_args.pipette) if not run_args.ctx.is_simulating(): serial_logger.terminate() - del hw._backend.eeprom_driver._gpio + del hw._backend.eeprom_driver._gpio # still need this? print("done\n\n") diff --git a/hardware-testing/hardware_testing/gravimetric/config.py b/hardware-testing/hardware_testing/gravimetric/config.py index 3af376a04cf..993e8716a92 100644 --- a/hardware-testing/hardware_testing/gravimetric/config.py +++ b/hardware-testing/hardware_testing/gravimetric/config.py @@ -24,7 +24,6 @@ class VolumetricConfig: pipette_mount: str tip_volume: int trials: int - labware_offsets: List[dict] slots_tiprack: List[int] increment: bool return_tip: bool @@ -194,11 +193,11 @@ def _get_liquid_probe_settings( plunger_speed=lqid_cfg["plunger_speed"], sensor_threshold_pascals=lqid_cfg["sensor_threshold_pascals"], expected_liquid_height=110, - output_option=OutputOptions.stream_to_csv, + output_option=OutputOptions.sync_only, aspirate_while_sensing=False, auto_zero_sensor=True, num_baseline_reads=10, - data_file="/var/pressure_sensor_data.csv", + data_file="/data/testing_data/pressure.csv", ) diff --git a/hardware-testing/hardware_testing/gravimetric/daily_setup.py b/hardware-testing/hardware_testing/gravimetric/daily_setup.py index bc13dc9d0bf..77569b43c11 100644 --- a/hardware-testing/hardware_testing/gravimetric/daily_setup.py +++ b/hardware-testing/hardware_testing/gravimetric/daily_setup.py @@ -13,8 +13,9 @@ ) from hardware_testing.gravimetric.config import GANTRY_MAX_SPEED from hardware_testing.gravimetric.measurement.scale import Scale # type: ignore[import] -from hardware_testing.gravimetric import helpers, workarounds +from hardware_testing.gravimetric import helpers from hardware_testing.gravimetric.__main__ import API_LEVEL +from hardware_testing.gravimetric.workarounds import get_sync_hw_api TEST_NAME = "gravimetric-daily-setup" @@ -253,7 +254,7 @@ def _calibrate() -> None: API_LEVEL, # type: ignore[attr-defined] is_simulating=args.simulate, ) - _hw = workarounds.get_sync_hw_api(_ctx) + _hw = get_sync_hw_api(_ctx) _hw.set_status_bar_state(COLOR_STATES["idle"]) _rec = GravimetricRecorder( GravimetricRecorderConfig( diff --git a/hardware-testing/hardware_testing/gravimetric/execute.py b/hardware-testing/hardware_testing/gravimetric/execute.py index cf2b8fb1ecc..76b8ff037e2 100644 --- a/hardware-testing/hardware_testing/gravimetric/execute.py +++ b/hardware-testing/hardware_testing/gravimetric/execute.py @@ -18,7 +18,6 @@ _calculate_average, _jog_to_find_liquid_height, _sense_liquid_height, - _apply_labware_offsets, _pick_up_tip, _drop_tip, ) @@ -53,6 +52,7 @@ import glob from opentrons.hardware_control.types import StatusBarState +from hardware_testing.gravimetric.workarounds import get_sync_hw_api _MEASUREMENTS: List[Tuple[str, MeasurementData]] = list() @@ -89,7 +89,7 @@ def _generate_callbacks_for_trial( if blank_measurement: volume = None - hw_api = ctx._core.get_hardware() + hw_api = get_sync_hw_api(ctx) hw_mount = OT3Mount.LEFT if pipette.mount == "left" else OT3Mount.RIGHT pip_ax = Axis.of_main_tool_actuator(hw_mount) estimate_bottom: float = -1 @@ -179,7 +179,6 @@ def _load_labware(ctx: ProtocolContext, cfg: config.GravimetricConfig) -> Labwar labware_on_scale = ctx.load_labware( cfg.labware_on_scale, location=cfg.slot_scale, namespace=namespace ) - _apply_labware_offsets(cfg, [labware_on_scale]) return labware_on_scale @@ -283,9 +282,13 @@ def _record_measurement_and_store(m_type: MeasurementType) -> MeasurementData: m_tag = _tag(m_type) if trial.recorder.is_simulator and not trial.blank: if m_type == MeasurementType.ASPIRATE: - trial.recorder.add_simulation_mass(trial.volume * -0.001) + trial.recorder.add_simulation_mass( + trial.channel_count * trial.volume * -0.001 + ) elif m_type == MeasurementType.DISPENSE: - trial.recorder.add_simulation_mass(trial.volume * 0.001) + trial.recorder.add_simulation_mass( + trial.channel_count * trial.volume * 0.001 + ) m_data = record_measurement_data( trial.ctx, m_tag, @@ -327,8 +330,7 @@ def _record_measurement_and_store(m_type: MeasurementType) -> MeasurementData: else: # center channel over well trial.pipette.move_to(trial.well.top(50).move(trial.channel_offset)) - mnt = OT3Mount.RIGHT if trial.pipette.mount == "right" else OT3Mount.LEFT - trial.ctx._core.get_hardware().retract(mnt) # retract to top of gantry + trial.pipette._retract() # retract to top of gantry m_data_init = _record_measurement_and_store(MeasurementType.INIT) ui.print_info(f"\tinitial grams: {m_data_init.grams_average} g") # update the vials volumes, using the last-known weight @@ -357,7 +359,7 @@ def _record_measurement_and_store(m_type: MeasurementType) -> MeasurementData: mode=trial.mode, clear_accuracy_function=trial.cfg.increment, ) - trial.ctx._core.get_hardware().retract(mnt) # retract to top of gantry + trial.pipette._retract() # retract to top of gantry _take_photos(trial, "aspirate") m_data_aspirate = _record_measurement_and_store(MeasurementType.ASPIRATE) @@ -379,7 +381,7 @@ def _record_measurement_and_store(m_type: MeasurementType) -> MeasurementData: mode=trial.mode, clear_accuracy_function=trial.cfg.increment, ) - trial.ctx._core.get_hardware().retract(mnt) # retract to top of gantry + trial.pipette._retract() # retract to top of gantry _take_photos(trial, "dispense") m_data_dispense = _record_measurement_and_store(MeasurementType.DISPENSE) ui.print_info(f"\tgrams after dispense: {m_data_dispense.grams_average} g") @@ -500,8 +502,7 @@ def _calculate_evaporation( resources.env_sensor, ) ui.print_info(f"running {config.NUM_BLANK_TRIALS}x blank measurements") - mnt = OT3Mount.RIGHT if resources.pipette.mount == "right" else OT3Mount.LEFT - resources.ctx._core.get_hardware().retract(mnt) + resources.pipette._retract() for i in range(config.SCALE_SECONDS_TO_TRUE_STABILIZE): ui.print_info( f"wait for scale to stabilize " @@ -545,7 +546,7 @@ def _get_liquid_height( if not resources.ctx.is_simulating() and not cfg.same_tip: ui.alert_user_ready( f"Please replace the {cfg.tip_volume}ul tips in slot 2", - resources.ctx._core.get_hardware(), + get_sync_hw_api(resources.ctx), ) _tip_counter[0] = 0 if cfg.jog: @@ -595,7 +596,7 @@ def run(cfg: config.GravimetricConfig, resources: TestResources) -> None: # noq recorder._recording = GravimetricRecording() report.store_config_gm(resources.test_report, cfg) calibration_tip_in_use = True - hw_api = resources.ctx._core.get_hardware() + hw_api = get_sync_hw_api(resources.ctx) if resources.ctx.is_simulating(): _PREV_TRIAL_GRAMS = None _MEASUREMENTS = list() @@ -605,8 +606,6 @@ def run(cfg: config.GravimetricConfig, resources: TestResources) -> None: # noq setup_channel_offset = _get_channel_offset(cfg, channel=0) first_tip_location = first_tip.top().move(setup_channel_offset) _pick_up_tip(resources.ctx, resources.pipette, cfg, location=first_tip_location) - mnt = OT3Mount.LEFT if cfg.pipette_mount == "left" else OT3Mount.RIGHT - resources.ctx._core.get_hardware().retract(mnt) ui.print_info("moving to scale") well = labware_on_scale["A1"] _liquid_height = _get_liquid_height(resources, cfg, well) @@ -642,6 +641,7 @@ def run(cfg: config.GravimetricConfig, resources: TestResources) -> None: # noq resources.pipette, return_tip=False, minimum_z_height=_minimum_z_height(cfg), + offset=_get_channel_offset(cfg, 0), ) # always trash calibration tips calibration_tip_in_use = False trial_count = 0 @@ -662,7 +662,7 @@ def run(cfg: config.GravimetricConfig, resources: TestResources) -> None: # noq actual_asp_list_all = [] actual_disp_list_all = [] ui.print_title(f"{volume} uL") - + resources.pipette.configure_for_volume(volume) trial_asp_dict: Dict[int, List[float]] = { trial: [] for trial in range(cfg.trials) } @@ -694,12 +694,7 @@ def run(cfg: config.GravimetricConfig, resources: TestResources) -> None: # noq cfg, location=next_tip_location, ) - mnt = ( - OT3Mount.LEFT - if cfg.pipette_mount == "left" - else OT3Mount.RIGHT - ) - resources.ctx._core.get_hardware().retract(mnt) + resources.pipette._retract() # retract to top of gantry ( actual_aspirate, aspirate_data, @@ -742,14 +737,12 @@ def run(cfg: config.GravimetricConfig, resources: TestResources) -> None: # noq ) ui.print_info("dropping tip") if not cfg.same_tip: - mnt = ( - OT3Mount.LEFT - if cfg.pipette_mount == "left" - else OT3Mount.RIGHT - ) - resources.ctx._core.get_hardware().retract(mnt) + resources.pipette._retract() # retract to top of gantry _drop_tip( - resources.pipette, cfg.return_tip, _minimum_z_height(cfg) + resources.pipette, + cfg.return_tip, + _minimum_z_height(cfg), + _get_channel_offset(cfg, run_trial.channel), ) ui.print_header(f"{volume} uL channel {channel + 1} CALCULATIONS") @@ -809,7 +802,7 @@ def run(cfg: config.GravimetricConfig, resources: TestResources) -> None: # noq acceptable_d = trials[volume][channel][0].acceptable_d print(f"acceptable cv {acceptable_cv} acceptable_d {acceptable_d}") print(f"dispense cv {dispense_cv} aspirate_cv {aspirate_cv}") - print(f"dispense d {dispense_cv} aspirate_d {aspirate_d}") + print(f"dispense d {dispense_d} aspirate_d {aspirate_d}") if ( not cfg.ignore_fail and acceptable_cv is not None @@ -820,8 +813,8 @@ def run(cfg: config.GravimetricConfig, resources: TestResources) -> None: # noq if ( dispense_cv > acceptable_cv or aspirate_cv > acceptable_cv - or aspirate_d > acceptable_d - or dispense_d > acceptable_d + or abs(aspirate_d) > acceptable_d + or abs(dispense_d) > acceptable_d ): raise RuntimeError( f"Trial with volume {volume} on channel {channel} did not pass spec" diff --git a/hardware-testing/hardware_testing/gravimetric/execute_photometric.py b/hardware-testing/hardware_testing/gravimetric/execute_photometric.py index 5b36acc46f3..217109dd89d 100644 --- a/hardware-testing/hardware_testing/gravimetric/execute_photometric.py +++ b/hardware-testing/hardware_testing/gravimetric/execute_photometric.py @@ -5,7 +5,7 @@ from opentrons.protocol_api import ProtocolContext, Well, Labware from hardware_testing.data import ui -from hardware_testing.opentrons_api.types import Point, OT3Mount +from hardware_testing.opentrons_api.types import Point from .measurement import ( MeasurementType, create_measurement_tag, @@ -18,7 +18,6 @@ from .helpers import ( _jog_to_find_liquid_height, _sense_liquid_height, - _apply_labware_offsets, _pick_up_tip, _drop_tip, get_list_of_wells_affected, @@ -110,7 +109,6 @@ def _load_labware( photoplate = loaded_labwares[cfg.photoplate_slot] else: photoplate = ctx.load_labware(cfg.photoplate, location=cfg.photoplate_slot) - _apply_labware_offsets(cfg, [photoplate]) if ( cfg.reservoir_slot in loaded_labwares.keys() @@ -119,7 +117,6 @@ def _load_labware( reservoir = loaded_labwares[cfg.reservoir_slot] else: reservoir = ctx.load_labware(cfg.reservoir, location=cfg.reservoir_slot) - _apply_labware_offsets(cfg, [reservoir]) return photoplate, reservoir @@ -218,11 +215,10 @@ def _record_measurement_and_store(m_type: MeasurementType) -> EnvironmentData: touch_tip=trial.cfg.touch_tip, ) _record_measurement_and_store(MeasurementType.DISPENSE) - trial.ctx._core.get_hardware().retract(OT3Mount.LEFT) + trial.pipette._retract() # retract to top of gantry if (i + 1) == num_dispenses: if not trial.cfg.same_tip: _drop_tip(trial.pipette, trial.cfg.return_tip) - trial.ctx._core.get_hardware().retract(OT3Mount.LEFT) if not trial.ctx.is_simulating() and trial.channel_count == 96: ui.get_user_ready("add SEAL to plate and remove from DECK") return @@ -350,13 +346,13 @@ def _find_liquid_height( setup_tip = _next_tip(resources, cfg, cfg.pipette_channels == 1) volume_for_setup = max(resources.test_volumes) _pick_up_tip(resources.ctx, resources.pipette, cfg, location=setup_tip.top()) - mnt = OT3Mount.LEFT if cfg.pipette_mount == "left" else OT3Mount.RIGHT - resources.ctx._core.get_hardware().retract(mnt) if ( not resources.ctx.is_simulating() and not cfg.same_tip and cfg.pipette_channels == 96 ): + + resources.pipette._retract() ui.get_user_ready("REPLACE first tip with NEW TIP") required_ul_per_src = (volume_for_setup * channel_count * cfg.trials) / len( cfg.dye_well_column_offset @@ -411,10 +407,8 @@ def _find_liquid_height( raise RuntimeError( f"bad volume in reservoir: {round(reservoir_ul / 1000, 1)} ml" ) - resources.ctx._core.get_hardware().retract(OT3Mount.LEFT) if not cfg.same_tip: resources.pipette.drop_tip(home_after=False) # always trash setup tips - resources.ctx._core.get_hardware().retract(OT3Mount.LEFT) # NOTE: the first tip-rack should have already been replaced # with new tips by the operator diff --git a/hardware-testing/hardware_testing/gravimetric/helpers.py b/hardware-testing/hardware_testing/gravimetric/helpers.py index 179701e0d83..7844f8d8d5e 100644 --- a/hardware-testing/hardware_testing/gravimetric/helpers.py +++ b/hardware-testing/hardware_testing/gravimetric/helpers.py @@ -2,7 +2,7 @@ import asyncio from random import random, randint from types import MethodType -from typing import Any, List, Dict, Optional, Tuple +from typing import Any, List, Dict, Optional, Tuple, Union from statistics import stdev from . import config from .liquid_class.defaults import get_liquid_class @@ -15,21 +15,34 @@ guess_from_global_config as guess_deck_type_from_global_config, ) from opentrons.protocol_api.labware import Well, Labware +from opentrons.protocol_api._types import OffDeckType +from opentrons.protocol_api._nozzle_layout import NozzleLayout from opentrons.protocols.types import APIVersion from opentrons.hardware_control.thread_manager import ThreadManager from opentrons.hardware_control.types import OT3Mount, Axis from opentrons.hardware_control.ot3api import OT3API from opentrons.hardware_control.instruments.ot3.pipette import Pipette +from opentrons import execute, simulate from opentrons.types import Point, Location from opentrons_shared_data.labware.dev_types import LabwareDefinition from hardware_testing.opentrons_api import helpers_ot3 from opentrons.protocol_api import ProtocolContext, InstrumentContext -from .workarounds import get_sync_hw_api, get_latest_offset_for_labware +from .workarounds import get_sync_hw_api from hardware_testing.opentrons_api.helpers_ot3 import clear_pipette_ul_per_mm +import opentrons.protocol_engine.execution.pipetting as PE_pipetting +from opentrons.protocol_engine.notes import CommandNoteAdder + +from opentrons.protocol_engine import ( + StateView, + WellLocation, + DropTipWellLocation, +) +from opentrons.protocol_api.core.engine import deck_conflict as DeckConflit + def _add_fake_simulate( ctx: protocol_api.ProtocolContext, is_simulating: bool @@ -79,13 +92,21 @@ async def _thread_manager_build_hw_api( stall_detection_enable=stall_detection_enable, ) - return protocol_api.create_protocol_context( - api_version=APIVersion.from_string(api_level), - hardware_api=ThreadManager(_thread_manager_build_hw_api), # type: ignore[arg-type] - deck_type="ot3_standard", - extra_labware=extra_labware, - deck_version=2, - ) + papi: protocol_api.ProtocolContext + if is_simulating: + papi = simulate.get_protocol_api( + version=APIVersion.from_string(api_level), + extra_labware=extra_labware, + hardware_simulator=ThreadManager(_thread_manager_build_hw_api), + robot_type="Flex", + use_virtual_hardware=False, + ) + else: + papi = execute.get_protocol_api( + version=APIVersion.from_string(api_level), extra_labware=extra_labware + ) + + return papi def well_is_reservoir(well: protocol_api.labware.Well) -> bool: @@ -203,6 +224,50 @@ def _check_if_software_supports_high_volumes() -> bool: return modified_a and modified_b +def _override_set_current_volume(self, new_volume: float) -> None: # noqa: ANN001 + assert new_volume >= 0 + # assert new_volume <= self.working_volume + self._current_volume = new_volume + + +def _override_add_current_volume(self, volume_incr: float) -> None: # noqa: ANN001 + self._current_volume += volume_incr + + +def _override_ok_to_add_volume(self, volume_incr: float) -> bool: # noqa: ANN001 + return True + + +def _override_validate_asp_vol( + state_view: StateView, + pipette_id: str, + aspirate_volume: float, + command_note_adder: CommandNoteAdder, +) -> float: + return aspirate_volume + + +def _override_check_safe_for_pipette_movement( + engine_state: StateView, + pipette_id: str, + labware_id: str, + well_name: str, + well_location: Union[WellLocation, DropTipWellLocation], +) -> None: + pass + + +def _override_software_supports_high_volumes() -> None: + # yea so ok this is pretty ugly but this is super helpful for us + # with this we don't need to apply patches, and can run the testing scripts + # without pushing modified code to the robot + + Pipette.set_current_volume = _override_set_current_volume # type: ignore[assignment] + Pipette.ok_to_add_volume = _override_ok_to_add_volume # type: ignore[assignment] + Pipette.add_current_volume = _override_add_current_volume # type: ignore[assignment] + PE_pipetting._validate_aspirate_volume = _override_validate_asp_vol # type: ignore[assignment] + + def _get_channel_offset(cfg: config.VolumetricConfig, channel: int) -> Point: assert ( channel < cfg.pipette_channels @@ -252,23 +317,6 @@ def _get_tip_batch(is_simulating: bool, tip: int) -> str: return "simulation-tip-batch" -def _apply(labware: Labware, cfg: config.VolumetricConfig) -> None: - o = get_latest_offset_for_labware(cfg.labware_offsets, labware) - ui.print_info( - f'Apply labware offset to "{labware.name}" (slot={labware.parent}): ' - f"x={round(o.x, 2)}, y={round(o.y, 2)}, z={round(o.z, 2)}" - ) - labware.set_calibration(o) - - -def _apply_labware_offsets( - cfg: config.VolumetricConfig, - labwares: List[Labware], -) -> None: - for lw in labwares: - _apply(lw, cfg) - - def _pick_up_tip( ctx: ProtocolContext, pipette: InstrumentContext, @@ -280,8 +328,6 @@ def _pick_up_tip( f"from slot #{location.labware.parent.parent}" ) pipette.pick_up_tip(location) - if pipette.channels == 96: - get_sync_hw_api(ctx).retract(OT3Mount.LEFT) # NOTE: the accuracy-adjust function gets set on the Pipette # each time we pick-up a new tip. if cfg.increment: @@ -293,12 +339,27 @@ def _pick_up_tip( def _drop_tip( - pipette: InstrumentContext, return_tip: bool, minimum_z_height: int = 0 + pipette: InstrumentContext, + return_tip: bool, + minimum_z_height: int = 0, + offset: Optional[Point] = None, ) -> None: if return_tip: pipette.return_tip(home_after=False) else: - pipette.drop_tip(home_after=False) + if offset is not None: + # we don't actually need the offset, if this is an 8 channel we always center channel + # a1 over the back of the trash + trash_well = pipette.trash_container.well(0) # type: ignore[union-attr] + trash_container = trash_well.center().move( + Point(0, trash_well.width / 2, 0) # type: ignore[union-attr, operator] + ) + pipette.drop_tip( + trash_container, + home_after=False, + ) + else: + pipette.drop_tip(home_after=False) if minimum_z_height > 0: cur_location = pipette._get_last_location_by_api_version() if cur_location is not None: @@ -337,11 +398,8 @@ def _get_volumes( kind, channels, pipette_volume, tip_volume, extra ) if not _check_if_software_supports_high_volumes(): - if ctx.is_simulating(): - test_volumes = _reduce_volumes_to_not_exceed_software_limit( - test_volumes, pipette_volume, channels, tip_volume - ) - else: + _override_software_supports_high_volumes() + if not _check_if_software_supports_high_volumes(): raise RuntimeError("you are not the correct branch") return test_volumes @@ -363,7 +421,9 @@ def _load_pipette( if pipette_mount in loaded_pipettes.keys(): return loaded_pipettes[pipette_mount] + trash = ctx.load_labware("opentrons_1_trash_3200ml_fixed", "A3") pipette = ctx.load_instrument(pip_name, pipette_mount) + loaded_pipettes = ctx.loaded_instruments assert pipette.max_volume == pipette_volume, ( f"expected {pipette_volume} uL pipette, " f"but got a {pipette.max_volume} uL pipette" @@ -374,12 +434,12 @@ def _load_pipette( # NOTE: 8ch QC testing means testing 1 channel at a time, # so we need to decrease the pick-up current to work with 1 tip. if pipette.channels == 8 and not increment and not photometric: - hwapi = get_sync_hw_api(ctx) - mnt = OT3Mount.LEFT if pipette_mount == "left" else OT3Mount.RIGHT - hwpipette: Pipette = hwapi.hardware_pipettes[mnt.to_mount()] - hwpipette._config.pick_up_tip_configurations.press_fit.current_by_tip_count[ - 8 - ] = 0.2 + pipette.configure_nozzle_layout(NozzleLayout.SINGLE, "A1") + # override deck conflict checking cause we specially lay out our tipracks + DeckConflit.check_safe_for_pipette_movement = ( + _override_check_safe_for_pipette_movement + ) + pipette.trash_container = trash return pipette @@ -402,23 +462,22 @@ def _load_tipracks( cfg: config.VolumetricConfig, use_adapters: bool = False, ) -> List[Labware]: - adp_str = "_adp" if use_adapters else "" tiprack_load_settings: List[Tuple[int, str]] = [ ( slot, - f"opentrons_flex_96_tiprack_{cfg.tip_volume}ul{adp_str}", + f"opentrons_flex_96_tiprack_{cfg.tip_volume}ul", ) for slot in cfg.slots_tiprack ] for ls in tiprack_load_settings: ui.print_info(f'Loading tiprack "{ls[1]}" in slot #{ls[0]}') - if use_adapters: - tiprack_namespace = "custom_beta" - else: - tiprack_namespace = "opentrons" + adapter: Optional[str] = ( + "opentrons_flex_96_tiprack_adapter" if use_adapters else None + ) # If running multiple tests in one run, the labware may already be loaded loaded_labwares = ctx.loaded_labwares + print(f"Loaded labwares {loaded_labwares}") pre_loaded_tips: List[Labware] = [] for ls in tiprack_load_settings: if ls[0] in loaded_labwares.keys(): @@ -430,15 +489,25 @@ def _load_tipracks( ui.print_info( f"Removing {loaded_labwares[ls[0]].name} from slot {ls[0]}" ) - del ctx._core.get_deck()[ls[0]] # type: ignore[attr-defined] + ctx._core.move_labware( + loaded_labwares[ls[0]]._core, + new_location=OffDeckType.OFF_DECK, + use_gripper=False, + pause_for_manual_move=False, + pick_up_offset=None, + drop_offset=None, + ) if len(pre_loaded_tips) == len(tiprack_load_settings): return pre_loaded_tips - tipracks = [ - ctx.load_labware(ls[1], location=ls[0], namespace=tiprack_namespace) - for ls in tiprack_load_settings - ] - _apply_labware_offsets(cfg, tipracks) + tipracks: List[Labware] = [] + for ls in tiprack_load_settings: + if ctx.deck[ls[0]] is not None: + tipracks.append( + ctx.deck[ls[0]].load_labware(ls[1]) # type: ignore[union-attr] + ) + else: + tipracks.append(ctx.load_labware(ls[1], location=ls[0], adapter=adapter)) return tipracks diff --git a/hardware-testing/hardware_testing/gravimetric/liquid_class/defaults.py b/hardware-testing/hardware_testing/gravimetric/liquid_class/defaults.py index 1146d6bb432..a37f21b1b36 100644 --- a/hardware-testing/hardware_testing/gravimetric/liquid_class/defaults.py +++ b/hardware-testing/hardware_testing/gravimetric/liquid_class/defaults.py @@ -11,8 +11,6 @@ _default_submerge_aspirate_mm = 1.5 _p50_multi_submerge_aspirate_mm = 1.5 _default_submerge_dispense_mm = 1.5 -_96_default_submerge_aspirate_mm = 2.5 -_96_default_submerge_dispense_mm = 3.0 _default_retract_mm = 5.0 _default_retract_discontinuity = 20 @@ -273,7 +271,7 @@ 1000: { # P1000 50: { # T50 5: DispenseSettings( # 5uL - z_submerge_depth=_96_default_submerge_dispense_mm, + z_submerge_depth=_default_submerge_dispense_mm, plunger_acceleration=_default_accel_96ch_ul_sec_sec, plunger_flow_rate=80, # ul/sec delay=_default_dispense_delay_seconds, @@ -282,7 +280,7 @@ blow_out_submerged=5, ), 10: DispenseSettings( # 10uL - z_submerge_depth=_96_default_submerge_dispense_mm, + z_submerge_depth=_default_submerge_dispense_mm, plunger_acceleration=_default_accel_96ch_ul_sec_sec, plunger_flow_rate=80, # ul/sec delay=_default_dispense_delay_seconds, @@ -291,7 +289,7 @@ blow_out_submerged=5, ), 50: DispenseSettings( # 50uL - z_submerge_depth=_96_default_submerge_dispense_mm, + z_submerge_depth=_default_submerge_dispense_mm, plunger_acceleration=_default_accel_96ch_ul_sec_sec, plunger_flow_rate=80, # ul/sec delay=_default_dispense_delay_seconds, @@ -302,7 +300,7 @@ }, 200: { # T200 5: DispenseSettings( # 5uL - z_submerge_depth=_96_default_submerge_dispense_mm, + z_submerge_depth=_default_submerge_dispense_mm, plunger_acceleration=_default_accel_96ch_ul_sec_sec, plunger_flow_rate=80, # ul/sec delay=_default_dispense_delay_seconds, @@ -311,7 +309,7 @@ blow_out_submerged=5, ), 50: DispenseSettings( # 50uL - z_submerge_depth=_96_default_submerge_dispense_mm, + z_submerge_depth=_default_submerge_dispense_mm, plunger_acceleration=_default_accel_96ch_ul_sec_sec, plunger_flow_rate=80, # ul/sec delay=_default_dispense_delay_seconds, @@ -320,7 +318,7 @@ blow_out_submerged=5, ), 200: DispenseSettings( # 200uL - z_submerge_depth=_96_default_submerge_dispense_mm, + z_submerge_depth=_default_submerge_dispense_mm, plunger_acceleration=_default_accel_96ch_ul_sec_sec, plunger_flow_rate=80, # ul/sec delay=_default_dispense_delay_seconds, @@ -331,7 +329,7 @@ }, 1000: { # T1000 10: DispenseSettings( # 10uL - z_submerge_depth=_96_default_submerge_dispense_mm, + z_submerge_depth=_default_submerge_dispense_mm, plunger_acceleration=_default_accel_96ch_ul_sec_sec, plunger_flow_rate=80, # ul/sec delay=_default_dispense_delay_seconds, @@ -340,7 +338,7 @@ blow_out_submerged=20, ), 100: DispenseSettings( # 100uL - z_submerge_depth=_96_default_submerge_dispense_mm, + z_submerge_depth=_default_submerge_dispense_mm, plunger_acceleration=_default_accel_96ch_ul_sec_sec, plunger_flow_rate=80, # ul/sec delay=_default_dispense_delay_seconds, @@ -349,7 +347,7 @@ blow_out_submerged=20, ), 1000: DispenseSettings( # 1000uL - z_submerge_depth=_96_default_submerge_dispense_mm, + z_submerge_depth=_default_submerge_dispense_mm, plunger_acceleration=_default_accel_96ch_ul_sec_sec, plunger_flow_rate=80, # ul/sec delay=_default_dispense_delay_seconds, @@ -635,7 +633,7 @@ 1000: { # P1000 50: { # T50 5: AspirateSettings( # 5uL - z_submerge_depth=_96_default_submerge_aspirate_mm, + z_submerge_depth=_default_submerge_aspirate_mm, plunger_acceleration=_default_accel_96ch_ul_sec_sec, plunger_flow_rate=6.5, # ul/sec delay=_default_aspirate_delay_seconds, @@ -645,7 +643,7 @@ trailing_air_gap=0.1, ), 10: AspirateSettings( # 10uL - z_submerge_depth=_96_default_submerge_aspirate_mm, + z_submerge_depth=_default_submerge_aspirate_mm, plunger_acceleration=_default_accel_96ch_ul_sec_sec, plunger_flow_rate=6.5, # ul/sec delay=_default_aspirate_delay_seconds, @@ -655,7 +653,7 @@ trailing_air_gap=0.1, ), 50: AspirateSettings( # 50uL - z_submerge_depth=_96_default_submerge_aspirate_mm, + z_submerge_depth=_default_submerge_aspirate_mm, plunger_acceleration=_default_accel_96ch_ul_sec_sec, plunger_flow_rate=6.5, # ul/sec delay=_default_aspirate_delay_seconds, @@ -667,7 +665,7 @@ }, 200: { # T200 5: AspirateSettings( # 5uL - z_submerge_depth=_96_default_submerge_aspirate_mm, + z_submerge_depth=_default_submerge_aspirate_mm, plunger_acceleration=_default_accel_96ch_ul_sec_sec, plunger_flow_rate=80, # ul/sec delay=_default_aspirate_delay_seconds, @@ -677,7 +675,7 @@ trailing_air_gap=2, ), 50: AspirateSettings( # 50uL - z_submerge_depth=_96_default_submerge_aspirate_mm, + z_submerge_depth=_default_submerge_aspirate_mm, plunger_acceleration=_default_accel_96ch_ul_sec_sec, plunger_flow_rate=80, # ul/sec delay=_default_aspirate_delay_seconds, @@ -687,7 +685,7 @@ trailing_air_gap=3.5, ), 200: AspirateSettings( # 200uL - z_submerge_depth=_96_default_submerge_aspirate_mm, + z_submerge_depth=_default_submerge_aspirate_mm, plunger_acceleration=_default_accel_96ch_ul_sec_sec, plunger_flow_rate=80, # ul/sec delay=_default_aspirate_delay_seconds, @@ -699,7 +697,7 @@ }, 1000: { # T1000 10: AspirateSettings( # 10uL - z_submerge_depth=_96_default_submerge_aspirate_mm, + z_submerge_depth=_default_submerge_aspirate_mm, plunger_acceleration=_default_accel_96ch_ul_sec_sec, plunger_flow_rate=160, # ul/sec delay=_default_aspirate_delay_seconds, @@ -709,7 +707,7 @@ trailing_air_gap=10, ), 100: AspirateSettings( # 100uL - z_submerge_depth=_96_default_submerge_aspirate_mm, + z_submerge_depth=_default_submerge_aspirate_mm, plunger_acceleration=_default_accel_96ch_ul_sec_sec, plunger_flow_rate=160, # ul/sec delay=_default_aspirate_delay_seconds, @@ -719,7 +717,7 @@ trailing_air_gap=10, ), 1000: AspirateSettings( # 1000uL - z_submerge_depth=_96_default_submerge_aspirate_mm, + z_submerge_depth=_default_submerge_aspirate_mm, plunger_acceleration=_default_accel_96ch_ul_sec_sec, plunger_flow_rate=160, # ul/sec delay=_default_aspirate_delay_seconds, diff --git a/hardware-testing/hardware_testing/gravimetric/liquid_class/pipetting.py b/hardware-testing/hardware_testing/gravimetric/liquid_class/pipetting.py index 473877208ea..9f059559f13 100644 --- a/hardware-testing/hardware_testing/gravimetric/liquid_class/pipetting.py +++ b/hardware-testing/hardware_testing/gravimetric/liquid_class/pipetting.py @@ -8,6 +8,7 @@ from hardware_testing.opentrons_api.types import OT3AxisKind from hardware_testing.gravimetric import config +from hardware_testing.gravimetric.workarounds import get_sync_hw_api from hardware_testing.gravimetric.liquid_height.height import LiquidTracker from hardware_testing.opentrons_api.types import OT3Mount, Point from hardware_testing.opentrons_api.helpers_ot3 import clear_pipette_ul_per_mm @@ -177,7 +178,7 @@ def _pipette_with_liquid_settings( # noqa: C901 ) -> None: """Run a pipette given some Pipetting Liquid Settings.""" # FIXME: stop using hwapi, and get those functions into core software - hw_api = ctx._core.get_hardware() + hw_api = get_sync_hw_api(ctx) hw_mount = OT3Mount.LEFT if pipette.mount == "left" else OT3Mount.RIGHT hw_pipette = hw_api.hardware_pipettes[hw_mount.to_mount()] _check_aspirate_dispense_args(mix, aspirate, dispense) @@ -189,20 +190,6 @@ def _get_max_blow_out_ul() -> float: blow_out = hw_pipette.plunger_positions.blow_out return (blow_out - bottom) * blow_out_ul_per_mm - def _dispense_with_added_blow_out() -> None: - # dispense all liquid, plus some air - # FIXME: push-out is not supported in Legacy core, so here - # we again use the hardware controller - hw_api = ctx._core.get_hardware() - hw_mount = OT3Mount.LEFT if pipette.mount == "left" else OT3Mount.RIGHT - push_out = min(liquid_class.dispense.blow_out_submerged, _get_max_blow_out_ul()) - hw_api.dispense(hw_mount, push_out=push_out) - - def _blow_out_remaining_air() -> None: - # FIXME: using the HW-API to specify that we want to blow-out the full - # available blow-out volume - hw_api.blow_out(hw_mount, _get_max_blow_out_ul()) - # ASPIRATE/DISPENSE SEQUENCE HAS THREE PHASES: # 1. APPROACH # 2. SUBMERGE @@ -237,16 +224,17 @@ def _aspirate_on_approach() -> None: "WARNING: removing trailing air-gap from pipette, " "this should only happen during blank trials" ) - hw_api.dispense(hw_mount) + pipette.dispense(volume=pipette.current_volume) if mode: # NOTE: increment test requires the plunger's "bottom" position # does not change during the entire test run hw_api.set_liquid_class(hw_mount, mode) else: - hw_api.configure_for_volume(hw_mount, aspirate if aspirate else dispense) + cfg_volume: float = aspirate if aspirate else dispense # type: ignore[assignment] + pipette.configure_for_volume(cfg_volume) if clear_accuracy_function: clear_pipette_ul_per_mm(hw_api, hw_mount) # type: ignore[arg-type] - hw_api.prepare_for_aspirate(hw_mount) + pipette.prepare_to_aspirate() if liquid_class.aspirate.leading_air_gap > 0: pipette.aspirate(liquid_class.aspirate.leading_air_gap) @@ -260,14 +248,18 @@ def _aspirate_on_mix() -> None: if i < _num_mixes - 1: pipette.dispense(mix) else: - _dispense_with_added_blow_out() + if added_blow_out: + push_out = min( + liquid_class.dispense.blow_out_submerged, _get_max_blow_out_ul() + ) + pipette.dispense(dispense, push_out=push_out) ctx.delay(liquid_class.dispense.delay) # don't go all the way up to retract position, but instead just above liquid _retract( ctx, pipette, well, channel_offset, approach_mm, retract_speed, _z_disc ) - _blow_out_remaining_air() - hw_api.prepare_for_aspirate(hw_mount) + pipette.blow_out() + pipette.prepare_to_aspirate() assert pipette.current_volume == 0 def _aspirate_on_submerge() -> None: @@ -283,18 +275,22 @@ def _aspirate_on_submerge() -> None: def _aspirate_on_retract() -> None: # add trailing-air-gap - pipette.aspirate(liquid_class.aspirate.trailing_air_gap) + if not blank: + pipette.air_gap(liquid_class.aspirate.trailing_air_gap, height=0) def _dispense_on_approach() -> None: # remove trailing-air-gap - pipette.dispense(liquid_class.aspirate.trailing_air_gap) + if not blank: + pipette.dispense(liquid_class.aspirate.trailing_air_gap) def _dispense_on_submerge() -> None: callbacks.on_dispensing() + push_out = None if added_blow_out: - _dispense_with_added_blow_out() - else: - pipette.dispense(dispense) + push_out = min( + liquid_class.dispense.blow_out_submerged, _get_max_blow_out_ul() + ) + pipette.dispense(dispense, push_out=push_out) # update liquid-height tracker liquid_tracker.update_affected_wells( well, dispense=dispense, channels=channel_count @@ -306,13 +302,13 @@ def _dispense_on_retract() -> None: if pipette.current_volume <= 0 and added_blow_out: # blow-out any remaining air in pipette (any reason why not?) callbacks.on_blowing_out() - _blow_out_remaining_air() - hw_api.prepare_for_aspirate(hw_mount) + pipette.blow_out() + pipette.prepare_to_aspirate() if touch_tip: pipette.touch_tip(speed=config.TOUCH_TIP_SPEED) # NOTE: always do a trailing-air-gap, regardless of if tip is empty or not # to avoid droplets from forming and falling off the tip - pipette.aspirate(liquid_class.aspirate.trailing_air_gap) + pipette.air_gap(liquid_class.aspirate.trailing_air_gap, height=0) # PHASE 1: APPROACH pipette.flow_rate.aspirate = liquid_class.aspirate.plunger_flow_rate @@ -337,7 +333,7 @@ def _dispense_on_retract() -> None: # EXIT callbacks.on_exiting() - hw_api.retract(hw_mount) + pipette._retract() def mix_with_liquid_class( diff --git a/hardware-testing/hardware_testing/gravimetric/overrides/api.patch b/hardware-testing/hardware_testing/gravimetric/overrides/api.patch index 4e2ab9b6c23..e69de29bb2d 100644 --- a/hardware-testing/hardware_testing/gravimetric/overrides/api.patch +++ b/hardware-testing/hardware_testing/gravimetric/overrides/api.patch @@ -1,111 +0,0 @@ -diff --git a/api/src/opentrons/hardware_control/instruments/ot3/pipette.py b/api/src/opentrons/hardware_control/instruments/ot3/pipette.py -index 2d36460ca6..8578768930 100644 ---- a/api/src/opentrons/hardware_control/instruments/ot3/pipette.py -+++ b/api/src/opentrons/hardware_control/instruments/ot3/pipette.py -@@ -427,11 +427,11 @@ class Pipette(AbstractInstrument[PipetteConfigurations]): - - def set_current_volume(self, new_volume: float) -> None: - assert new_volume >= 0 -- assert new_volume <= self.working_volume -+ # assert new_volume <= self.working_volume - self._current_volume = new_volume - - def add_current_volume(self, volume_incr: float) -> None: -- assert self.ok_to_add_volume(volume_incr) -+ # assert self.ok_to_add_volume(volume_incr) - self._current_volume += volume_incr - - def remove_current_volume(self, volume_incr: float) -> None: -@@ -439,7 +439,8 @@ class Pipette(AbstractInstrument[PipetteConfigurations]): - self._current_volume -= volume_incr - - def ok_to_add_volume(self, volume_incr: float) -> bool: -- return self.current_volume + volume_incr <= self.working_volume -+ # return self.current_volume + volume_incr <= self.working_volume -+ return True - - def ok_to_push_out(self, push_out_dist_mm: float) -> bool: - return push_out_dist_mm <= ( -diff --git a/api/src/opentrons/protocol_api/core/engine/deck_conflict.py b/api/src/opentrons/protocol_api/core/engine/deck_conflict.py -index 0ba7e17621..4d6682f5e4 100644 ---- a/api/src/opentrons/protocol_api/core/engine/deck_conflict.py -+++ b/api/src/opentrons/protocol_api/core/engine/deck_conflict.py -@@ -341,18 +341,12 @@ def check_safe_for_tip_pickup_and_return( - f" when picking up fewer than 96 tips." - ) - elif not is_partial_config and not is_96_ch_tiprack_adapter: -- raise UnsuitableTiprackForPipetteMotion( -- f"{tiprack_name} must be on an Opentrons Flex 96 Tip Rack Adapter" -- f" in order to pick up or return all 96 tips simultaneously." -- ) -+ pass - - elif ( - not is_partial_config - ): # tiprack is not on adapter and pipette is in full config -- raise UnsuitableTiprackForPipetteMotion( -- f"{tiprack_name} must be on an Opentrons Flex 96 Tip Rack Adapter" -- f" in order to pick up or return all 96 tips simultaneously." -- ) -+ pass - - - # TODO (spp, 2023-02-06): update the extents check to use all nozzle bounds instead of -diff --git a/api/src/opentrons/protocol_api/core/legacy/deck.py b/api/src/opentrons/protocol_api/core/legacy/deck.py -index 9a9092af5a..33aa5941ce 100644 ---- a/api/src/opentrons/protocol_api/core/legacy/deck.py -+++ b/api/src/opentrons/protocol_api/core/legacy/deck.py -@@ -55,11 +55,11 @@ class DeckItem(Protocol): - class Deck(UserDict): # type: ignore[type-arg] - data: Dict[int, Optional[DeckItem]] - -- def __init__(self, deck_type: str) -> None: -+ def __init__( -+ self, deck_type: str, version: int = DEFAULT_LEGACY_DECK_DEFINITION_VERSION -+ ) -> None: - super().__init__() -- self._definition = load_deck( -- name=deck_type, version=DEFAULT_LEGACY_DECK_DEFINITION_VERSION -- ) -+ self._definition = load_deck(name=deck_type, version=version) - self._positions = {} - for slot in self._definition["locations"]["orderedSlots"]: - self.data[int(slot["id"])] = None -diff --git a/api/src/opentrons/protocol_api/create_protocol_context.py b/api/src/opentrons/protocol_api/create_protocol_context.py -index 5a64e70cf9..7d5047cc4b 100644 ---- a/api/src/opentrons/protocol_api/create_protocol_context.py -+++ b/api/src/opentrons/protocol_api/create_protocol_context.py -@@ -22,6 +22,7 @@ from .deck import Deck - - from .core.common import ProtocolCore as AbstractProtocolCore - from .core.legacy.deck import Deck as LegacyDeck -+from opentrons_shared_data.deck import DEFAULT_DECK_DEFINITION_VERSION - from .core.legacy.legacy_protocol_core import LegacyProtocolCore - from .core.legacy.labware_offset_provider import ( - AbstractLabwareOffsetProvider, -@@ -52,6 +53,7 @@ def create_protocol_context( - extra_labware: Optional[Dict[str, LabwareDefinition]] = None, - bundled_labware: Optional[Dict[str, LabwareDefinition]] = None, - bundled_data: Optional[Dict[str, bytes]] = None, -+ deck_version: int = DEFAULT_DECK_DEFINITION_VERSION, - ) -> ProtocolContext: - """Create a ProtocolContext for use in a Python protocol. - -@@ -121,7 +123,7 @@ def create_protocol_context( - - # TODO(mc, 2022-8-22): remove `disable_fast_protocol_upload` - elif use_simulating_core and not feature_flags.disable_fast_protocol_upload(): -- legacy_deck = LegacyDeck(deck_type=deck_type) -+ legacy_deck = LegacyDeck(deck_type=deck_type, version=deck_version) - core = LegacyProtocolCoreSimulator( - sync_hardware=sync_hardware, - labware_offset_provider=labware_offset_provider, -@@ -133,7 +135,7 @@ def create_protocol_context( - ) - - else: -- legacy_deck = LegacyDeck(deck_type=deck_type) -+ legacy_deck = LegacyDeck(deck_type=deck_type, version=deck_version) - core = LegacyProtocolCore( - sync_hardware=sync_hardware, - labware_offset_provider=labware_offset_provider, diff --git a/hardware-testing/hardware_testing/gravimetric/overrides/shared-data.patch b/hardware-testing/hardware_testing/gravimetric/overrides/shared-data.patch index b2d08d109e9..5d688841b91 100644 --- a/hardware-testing/hardware_testing/gravimetric/overrides/shared-data.patch +++ b/hardware-testing/hardware_testing/gravimetric/overrides/shared-data.patch @@ -1,872 +1,180 @@ -diff --git a/shared-data/deck/definitions/2/ot3_standard.json b/shared-data/deck/definitions/2/ot3_standard.json -new file mode 100644 -index 0000000000..8ad4397cba ---- /dev/null -+++ b/shared-data/deck/definitions/2/ot3_standard.json -@@ -0,0 +1,866 @@ -+{ -+ "otId": "ot3_standard", -+ "schemaVersion": 3, -+ "cornerOffsetFromOrigin": [-204.31, -76.59, 0], -+ "dimensions": [854.995, 581.74, 0], -+ "metadata": { -+ "displayName": "OT-3 Standard Deck", -+ "tags": ["ot3", "12 slots", "standard"] -+ }, -+ "robot": { -+ "model": "OT-3 Standard" -+ }, -+ "locations": { -+ "orderedSlots": [ -+ { -+ "id": "1", -+ "position": [0.0, 0.0, 0.0], -+ "matingSurfaceUnitVector": [-1, 1, -1], -+ "boundingBox": { -+ "xDimension": 128.0, -+ "yDimension": 86.0, -+ "zDimension": 0 -+ }, -+ "displayName": "Slot D1", -+ "compatibleModuleTypes": [ -+ "magneticModuleType", -+ "temperatureModuleType", -+ "heaterShakerModuleType" -+ ] -+ }, -+ { -+ "id": "2", -+ "position": [164.0, 0.0, 0.0], -+ "matingSurfaceUnitVector": [-1, 1, -1], -+ "boundingBox": { -+ "xDimension": 128.0, -+ "yDimension": 86.0, -+ "zDimension": 0 -+ }, -+ "displayName": "Slot D2", -+ "compatibleModuleTypes": [ -+ "magneticModuleType", -+ "temperatureModuleType", -+ "heaterShakerModuleType" -+ ] -+ }, -+ { -+ "id": "3", -+ "position": [328.0, 0.0, 0.0], -+ "matingSurfaceUnitVector": [-1, 1, -1], -+ "boundingBox": { -+ "xDimension": 128.0, -+ "yDimension": 86.0, -+ "zDimension": 0 -+ }, -+ "displayName": "Slot D3", -+ "compatibleModuleTypes": [ -+ "magneticModuleType", -+ "temperatureModuleType", -+ "heaterShakerModuleType" -+ ] -+ }, -+ { -+ "id": "4", -+ "position": [0.0, 107, 0.0], -+ "matingSurfaceUnitVector": [-1, 1, -1], -+ "boundingBox": { -+ "xDimension": 128.0, -+ "yDimension": 86.0, -+ "zDimension": 0 -+ }, -+ "displayName": "Slot C1", -+ "compatibleModuleTypes": [ -+ "magneticModuleType", -+ "temperatureModuleType", -+ "heaterShakerModuleType" -+ ] -+ }, -+ { -+ "id": "5", -+ "position": [164.0, 107, 0.0], -+ "matingSurfaceUnitVector": [-1, 1, -1], -+ "boundingBox": { -+ "xDimension": 128.0, -+ "yDimension": 86.0, -+ "zDimension": 0 -+ }, -+ "displayName": "Slot C2", -+ "compatibleModuleTypes": [ -+ "magneticModuleType", -+ "temperatureModuleType", -+ "heaterShakerModuleType" -+ ] -+ }, -+ { -+ "id": "6", -+ "position": [328.0, 107, 0.0], -+ "matingSurfaceUnitVector": [-1, 1, -1], -+ "boundingBox": { -+ "xDimension": 128.0, -+ "yDimension": 86.0, -+ "zDimension": 0 -+ }, -+ "displayName": "Slot C3", -+ "compatibleModuleTypes": [ -+ "magneticModuleType", -+ "temperatureModuleType", -+ "heaterShakerModuleType" -+ ] -+ }, -+ { -+ "id": "7", -+ "position": [0.0, 214.0, 0.0], -+ "matingSurfaceUnitVector": [-1, 1, -1], -+ "boundingBox": { -+ "xDimension": 128.0, -+ "yDimension": 86.0, -+ "zDimension": 0 -+ }, -+ "displayName": "Slot B1", -+ "compatibleModuleTypes": [ -+ "magneticModuleType", -+ "temperatureModuleType", -+ "thermocyclerModuleType", -+ "heaterShakerModuleType" -+ ] -+ }, -+ { -+ "id": "8", -+ "position": [164.0, 214.0, 0.0], -+ "matingSurfaceUnitVector": [-1, 1, -1], -+ "boundingBox": { -+ "xDimension": 128.0, -+ "yDimension": 86.0, -+ "zDimension": 0 -+ }, -+ "displayName": "Slot B2", -+ "compatibleModuleTypes": [ -+ "magneticModuleType", -+ "temperatureModuleType", -+ "heaterShakerModuleType" -+ ] -+ }, -+ { -+ "id": "9", -+ "position": [328.0, 214.0, 0.0], -+ "matingSurfaceUnitVector": [-1, 1, -1], -+ "boundingBox": { -+ "xDimension": 128.0, -+ "yDimension": 86.0, -+ "zDimension": 0 -+ }, -+ "displayName": "Slot B3", -+ "compatibleModuleTypes": [ -+ "magneticModuleType", -+ "temperatureModuleType", -+ "heaterShakerModuleType" -+ ] -+ }, -+ { -+ "id": "10", -+ "position": [0.0, 321.0, 0.0], -+ "matingSurfaceUnitVector": [-1, 1, -1], -+ "boundingBox": { -+ "xDimension": 128.0, -+ "yDimension": 86.0, -+ "zDimension": 0 -+ }, -+ "displayName": "Slot A1", -+ "compatibleModuleTypes": [ -+ "magneticModuleType", -+ "temperatureModuleType", -+ "heaterShakerModuleType" -+ ] -+ }, -+ { -+ "id": "11", -+ "position": [164.0, 321.0, 0.0], -+ "matingSurfaceUnitVector": [-1, 1, -1], -+ "boundingBox": { -+ "xDimension": 128.0, -+ "yDimension": 86.0, -+ "zDimension": 0 -+ }, -+ "displayName": "Slot A2", -+ "compatibleModuleTypes": [ -+ "magneticModuleType", -+ "temperatureModuleType", -+ "heaterShakerModuleType" -+ ] -+ }, -+ { -+ "id": "12", -+ "position": [328.0, 321.0, 0.0], -+ "boundingBox": { -+ "xDimension": 128.0, -+ "yDimension": 86.0, -+ "zDimension": 0 -+ }, -+ "displayName": "Slot A3", -+ "compatibleModuleTypes": [] -+ } -+ ], -+ "calibrationPoints": [], -+ "fixtures": [ -+ { -+ "id": "fixedTrash", -+ "slot": "12", -+ "labware": "opentrons_1_trash_3200ml_fixed", -+ "displayName": "Fixed Trash" -+ } -+ ] -+ }, -+ "layers": [ -+ { -+ "name": "style", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "type": "text/css" -+ }, -+ "children": [ -+ { -+ "name": "", -+ "type": "text", -+ "value": "\n.st0{fill:#CCCCCC;}\n.st1{fill:none;stroke:#16212D;stroke-width:3.2047;stroke-opacity:0.7;}\n.st2{fill:none;stroke:#16212D;stroke-width:3.156;stroke-opacity:0.7;}\n", -+ "attributes": {}, -+ "children": [] -+ } -+ ] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "id": "SLOT_A1_EXPANSION", -+ "class": "st0", -+ "d": "M-97.8,496.6h239c2.3,0,4.2-1.9,4.2-4.2v-70c0-2.3-1.9-4.2-4.2-4.2h-239c-2.3,0-4.2,1.9-4.2,4.2v70\nC-102,494.7-100.1,496.6-97.8,496.6z" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "id": "SLOT_BASE_A1", -+ "class": "st0", -+ "d": "M-97.7,417.1h238.8c2.4,0,4.3-1.9,4.3-4.3v-97.4c0-2.4-1.9-4.3-4.3-4.3H-97.7c-2.4,0-4.3,1.9-4.3,4.3v97.4\nC-102,415.1-100.1,417.1-97.7,417.1z" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "id": "SLOT_BASE_A2", -+ "class": "st0", -+ "d": "M150.8,417.1h154.3c2.4,0,4.3-1.9,4.3-4.3v-97.4c0-2.4-1.9-4.3-4.3-4.3H150.8c-2.4,0-4.3,1.9-4.3,4.3v97.4\nC146.5,415.1,148.4,417.1,150.8,417.1z" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "id": "SLOT_BASE_A3", -+ "class": "st0", -+ "d": "M314.8,417.1h238.9c2.4,0,4.3-1.9,4.3-4.3v-97.4c0-2.4-1.9-4.3-4.3-4.3H314.8c-2.4,0-4.3,1.9-4.3,4.3v97.4\nC310.5,415.1,312.4,417.1,314.8,417.1z" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "id": "SLOT_BASE_B1", -+ "class": "st0", -+ "d": "M-97.7,310h238.8c2.4,0,4.3-1.9,4.3-4.3v-97.2c0-2.4-1.9-4.3-4.3-4.3H-97.7c-2.4,0-4.3,1.9-4.3,4.3v97.2\nC-102,308.1-100.1,310-97.7,310z" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "id": "SLOT_BASE_B2", -+ "class": "st0", -+ "d": "M150.8,310h154.3c2.4,0,4.3-1.9,4.3-4.3v-97.2c0-2.4-1.9-4.3-4.3-4.3H150.8c-2.4,0-4.3,1.9-4.3,4.3v97.2\nC146.5,308.1,148.4,310,150.8,310z" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "id": "SLOT_BASE_B3", -+ "class": "st0", -+ "d": "M314.8,310h238.9c2.4,0,4.3-1.9,4.3-4.3v-97.2c0-2.4-1.9-4.3-4.3-4.3H314.8c-2.4,0-4.3,1.9-4.3,4.3v97.2\nC310.5,308.1,312.4,310,314.8,310z" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "id": "SLOT_BASE_C1", -+ "class": "st0", -+ "d": "M-97.7,203.1h238.8c2.4,0,4.3-1.9,4.3-4.3v-97.4c0-2.4-1.9-4.3-4.3-4.3H-97.7c-2.4,0-4.3,1.9-4.3,4.3v97.4\nC-102,201.2-100.1,203.1-97.7,203.1z" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "id": "SLOT_BASE_C2", -+ "class": "st0", -+ "d": "M150.8,203.1h154.3c2.4,0,4.3-1.9,4.3-4.3v-97.4c0-2.4-1.9-4.3-4.3-4.3H150.8c-2.4,0-4.3,1.9-4.3,4.3v97.4\nC146.5,201.2,148.4,203.1,150.8,203.1z" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "id": "SLOT_BASE_C3", -+ "class": "st0", -+ "d": "M314.8,203.1h238.9c2.4,0,4.3-1.9,4.3-4.3v-97.4c0-2.4-1.9-4.3-4.3-4.3H314.8c-2.4,0-4.3,1.9-4.3,4.3v97.4\nC310.5,201.2,312.4,203.1,314.8,203.1z" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "id": "SLOT_BASE_D1", -+ "class": "st0", -+ "d": "M-97.7,96.1h238.8c2.4,0,4.3-1.9,4.3-4.3V-5.6c0-2.4-1.9-4.3-4.3-4.3H-97.7c-2.4,0-4.3,1.9-4.3,4.3v97.4\nC-102,94.2-100.1,96.1-97.7,96.1z" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "id": "SLOT_BASE_D2", -+ "class": "st0", -+ "d": "M150.8,96.1h154.3c2.4,0,4.3-1.9,4.3-4.3V-5.6c0-2.4-1.9-4.3-4.3-4.3H150.8c-2.4,0-4.3,1.9-4.3,4.3v97.4\nC146.5,94.2,148.4,96.1,150.8,96.1z" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "id": "SLOT_BASE_D3", -+ "class": "st0", -+ "d": "M314.8,96.1h238.9c2.4,0,4.3-1.9,4.3-4.3V-5.6c0-2.4-1.9-4.3-4.3-4.3H314.8c-2.4,0-4.3,1.9-4.3,4.3v97.4\nC310.5,94.2,312.4,96.1,314.8,96.1z" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "g", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "id": "SLOT_CLIPS" -+ }, -+ "children": [ -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M-1.9,398.9V409H8.9" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st2", -+ "d": "M-1.9,329.8v-10.5H8.7" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M129.9,398.9V409h-10.8" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M129.9,329.8v-10.7h-10.8" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M162.1,398.9V409h10.8" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st2", -+ "d": "M162.1,329.8v-10.5h10.6" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M293.9,398.9V409h-10.8" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M293.9,329.8v-10.7h-10.8" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M326,398.9V409h10.8" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st2", -+ "d": "M326,329.8v-10.5h10.6" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M457.8,398.9V409H447" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M457.8,329.8v-10.7H447" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M-1.9,291.9V302H8.9" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st2", -+ "d": "M-1.9,222.8v-10.5H8.7" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M129.9,291.9V302h-10.8" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M129.9,222.8v-10.7h-10.8" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M162.1,291.9V302h10.8" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st2", -+ "d": "M162.1,222.8v-10.5h10.6" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M293.9,291.9V302h-10.8" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M293.9,222.8v-10.7h-10.8" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M326,291.9V302h10.8" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st2", -+ "d": "M326,222.8v-10.5h10.6" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M457.8,291.9V302H447" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M457.8,222.8v-10.7H447" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M-1.9,185v10.1H8.9" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st2", -+ "d": "M-1.9,115.8v-10.5H8.7" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M129.9,185v10.1h-10.8" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M129.9,115.8v-10.7h-10.8" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M162.1,185v10.1h10.8" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st2", -+ "d": "M162.1,115.8v-10.5h10.6" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M293.9,185v10.1h-10.8" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M293.9,115.8v-10.7h-10.8" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M326,185v10.1h10.8" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st2", -+ "d": "M326,115.8v-10.5h10.6" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M457.8,185v10.1H447" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M457.8,115.8v-10.7H447" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M-1.9,77.9V88H8.9" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st2", -+ "d": "M-1.9,8.8V-1.7H8.7" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M129.9,77.9V88h-10.8" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M129.9,8.8V-1.9h-10.8" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M162.1,77.9V88h10.8" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st2", -+ "d": "M162.1,8.8V-1.7h10.6" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M293.9,77.9V88h-10.8" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M293.9,8.8V-1.9h-10.8" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M326,77.9V88h10.8" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st2", -+ "d": "M326,8.8V-1.7h10.6" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M457.8,77.9V88H447" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M457.8,8.8V-1.9H447" -+ }, -+ "children": [] -+ } -+ ] -+ } -+ ] -+} +diff --git a/shared-data/pipette/definitions/2/liquid/single_channel/p50/default/3_5.json b/shared-data/pipette/definitions/2/liquid/single_channel/p50/default/3_5.json +index c798ce421a..14fc4a5b67 100644 +--- a/shared-data/pipette/definitions/2/liquid/single_channel/p50/default/3_5.json ++++ b/shared-data/pipette/definitions/2/liquid/single_channel/p50/default/3_5.json +@@ -20,50 +20,50 @@ + "aspirate": { + "default": { + "1": [ +- [0.462, 0.5646, 0.0415], +- [0.648, 0.3716, 0.1307], +- [1.032, 0.2742, 0.1938], +- [1.37, 0.1499, 0.3221], +- [2.014, 0.1044, 0.3845], +- [2.772, 0.0432, 0.5076], +- [3.05, -0.0809, 0.8517], +- [3.4, 0.0256, 0.5268], +- [3.962, 0.0612, 0.4057], +- [4.438, 0.0572, 0.4217], +- [5.164, 0.018, 0.5955], +- [5.966, 0.0095, 0.6393], +- [7.38, 0.0075, 0.6514], +- [9.128, 0.0049, 0.6705], +- [10.16, 0.0033, 0.6854], +- [13.812, 0.0024, 0.6948], +- [27.204, 0.0008, 0.7165], +- [50.614, 0.0002, 0.7328], +- [53.046, -0.0005, 0.7676] ++ [0.31, 0.591, 0.0197], ++ [0.39, 0.2586, 0.1227], ++ [0.86, 0.3697, 0.0794], ++ [1.29, 0.231, 0.1987], ++ [1.93, 0.1144, 0.3491], ++ [2.7, 0.0536, 0.4664], ++ [2.95, -0.1041, 0.8923], ++ [3.28, 0.0216, 0.5214], ++ [3.76, 0.048, 0.4349], ++ [4.38, 0.083, 0.3032], ++ [5.08, 0.0153, 0.5996], ++ [5.9, 0.0136, 0.6083], ++ [7.29, 0.007, 0.6474], ++ [9.04, 0.0059, 0.6551], ++ [10.08, 0.0045, 0.6682], ++ [13.74, 0.0029, 0.6842], ++ [27.15, 0.001, 0.7104], ++ [50.48, 0.0002, 0.7319], ++ [52.89, -0.0006, 0.7703] + ] + } + }, + "dispense": { + "default": { + "1": [ +- [0.462, 0.5646, 0.0415], +- [0.648, 0.3716, 0.1307], +- [1.032, 0.2742, 0.1938], +- [1.37, 0.1499, 0.3221], +- [2.014, 0.1044, 0.3845], +- [2.772, 0.0432, 0.5076], +- [3.05, -0.0809, 0.8517], +- [3.4, 0.0256, 0.5268], +- [3.962, 0.0612, 0.4057], +- [4.438, 0.0572, 0.4217], +- [5.164, 0.018, 0.5955], +- [5.966, 0.0095, 0.6393], +- [7.38, 0.0075, 0.6514], +- [9.128, 0.0049, 0.6705], +- [10.16, 0.0033, 0.6854], +- [13.812, 0.0024, 0.6948], +- [27.204, 0.0008, 0.7165], +- [50.614, 0.0002, 0.7328], +- [53.046, -0.0005, 0.7676] ++ [0.31, 0.591, 0.0197], ++ [0.39, 0.2586, 0.1227], ++ [0.86, 0.3697, 0.0794], ++ [1.29, 0.231, 0.1987], ++ [1.93, 0.1144, 0.3491], ++ [2.7, 0.0536, 0.4664], ++ [2.95, -0.1041, 0.8923], ++ [3.28, 0.0216, 0.5214], ++ [3.76, 0.048, 0.4349], ++ [4.38, 0.083, 0.3032], ++ [5.08, 0.0153, 0.5996], ++ [5.9, 0.0136, 0.6083], ++ [7.29, 0.007, 0.6474], ++ [9.04, 0.0059, 0.6551], ++ [10.08, 0.0045, 0.6682], ++ [13.74, 0.0029, 0.6842], ++ [27.15, 0.001, 0.7104], ++ [50.48, 0.0002, 0.7319], ++ [52.89, -0.0006, 0.7703] + ] + } + }, +diff --git a/shared-data/pipette/definitions/2/liquid/single_channel/p50/lowVolumeDefault/3_5.json b/shared-data/pipette/definitions/2/liquid/single_channel/p50/lowVolumeDefault/3_5.json +index 644d93354e..4eba92a089 100644 +--- a/shared-data/pipette/definitions/2/liquid/single_channel/p50/lowVolumeDefault/3_5.json ++++ b/shared-data/pipette/definitions/2/liquid/single_channel/p50/lowVolumeDefault/3_5.json +@@ -20,46 +20,48 @@ + "aspirate": { + "default": { + "1": [ +- [0.11, 0.207815, 0.040201], +- [0.65, 0.43933, 0.014735], +- [1.04, 0.256666, 0.133466], +- [1.67, 0.147126, 0.247388], +- [2.45, 0.078774, 0.361536], +- [2.89, 0.042387, 0.450684], +- [3.2, 0.014781, 0.530464], +- [3.79, 0.071819, 0.347944], +- [4.22, 0.051592, 0.424605], +- [4.93, 0.021219, 0.552775], +- [5.81, 0.023461, 0.541725], +- [7.21, 0.008959, 0.625982], +- [8.93, 0.005456, 0.651235], +- [10.0, 0.007108, 0.636489], +- [13.61, 0.002591, 0.681656], +- [26.99, 0.001163, 0.701094], +- [45.25, 0.000207, 0.726887] ++ [0.3, 0.459, 0.0586], ++ [0.47, 0.43, 0.0674], ++ [0.9, 0.3404, 0.1095], ++ [1.26, 0.1925, 0.2425], ++ [1.95, 0.1314, 0.3195], ++ [2.76, 0.0604, 0.458], ++ [2.95, -0.2085, 1.2002], ++ [3.33, 0.0425, 0.4597], ++ [3.87, 0.0592, 0.404], ++ [4.31, 0.0518, 0.4327], ++ [5.07, 0.0264, 0.5424], ++ [5.93, 0.0186, 0.5818], ++ [7.34, 0.0078, 0.6458], ++ [9.08, 0.005, 0.6664], ++ [10.09, 0.0022, 0.6918], ++ [13.74, 0.0027, 0.6868], ++ [27.13, 0.0009, 0.7109], ++ [45.43, -0.0038, 0.8391] + ] + } + }, + "dispense": { + "default": { + "1": [ +- [0.11, 0.207815, 0.040201], +- [0.65, 0.43933, 0.014735], +- [1.04, 0.256666, 0.133466], +- [1.67, 0.147126, 0.247388], +- [2.45, 0.078774, 0.361536], +- [2.89, 0.042387, 0.450684], +- [3.2, 0.014781, 0.530464], +- [3.79, 0.071819, 0.347944], +- [4.22, 0.051592, 0.424605], +- [4.93, 0.021219, 0.552775], +- [5.81, 0.023461, 0.541725], +- [7.21, 0.008959, 0.625982], +- [8.93, 0.005456, 0.651235], +- [10.0, 0.007108, 0.636489], +- [13.61, 0.002591, 0.681656], +- [26.99, 0.001163, 0.701094], +- [45.25, 0.000207, 0.726887] ++ [0.3, 0.459, 0.0586], ++ [0.47, 0.43, 0.0674], ++ [0.9, 0.3404, 0.1095], ++ [1.26, 0.1925, 0.2425], ++ [1.95, 0.1314, 0.3195], ++ [2.76, 0.0604, 0.458], ++ [2.95, -0.2085, 1.2002], ++ [3.33, 0.0425, 0.4597], ++ [3.87, 0.0592, 0.404], ++ [4.31, 0.0518, 0.4327], ++ [5.07, 0.0264, 0.5424], ++ [5.93, 0.0186, 0.5818], ++ [7.34, 0.0078, 0.6458], ++ [9.08, 0.005, 0.6664], ++ [10.09, 0.0022, 0.6918], ++ [13.74, 0.0027, 0.6868], ++ [27.13, 0.0009, 0.7109], ++ [45.43, -0.0038, 0.8391] + ] + } + }, diff --git a/hardware-testing/hardware_testing/gravimetric/tips.py b/hardware-testing/hardware_testing/gravimetric/tips.py index 8edf66a5797..7e72c6884a2 100644 --- a/hardware-testing/hardware_testing/gravimetric/tips.py +++ b/hardware-testing/hardware_testing/gravimetric/tips.py @@ -60,18 +60,18 @@ 7: "A", } CHANNEL_TO_TIP_ROW_LOOKUP_BY_SLOT = { - "1": CHANNEL_TO_TIP_ROW_LOOKUP, - "2": CHANNEL_TO_TIP_ROW_LOOKUP, - "3": CHANNEL_TO_TIP_ROW_LOOKUP, - "4": CHANNEL_TO_TIP_ROW_LOOKUP, - "5": CHANNEL_TO_TIP_ROW_LOOKUP, - "6": CHANNEL_TO_TIP_ROW_LOOKUP, - "7": CHANNEL_TO_TIP_ROW_LOOKUP, - "8": CHANNEL_TO_TIP_ROW_LOOKUP, - "9": CHANNEL_TO_TIP_ROW_LOOKUP, - "10": CHANNEL_TO_TIP_ROW_LOOKUP_BACK, - "11": CHANNEL_TO_TIP_ROW_LOOKUP_BACK, - "12": CHANNEL_TO_TIP_ROW_LOOKUP_BACK, + "D1": CHANNEL_TO_TIP_ROW_LOOKUP, + "D2": CHANNEL_TO_TIP_ROW_LOOKUP, + "D3": CHANNEL_TO_TIP_ROW_LOOKUP, + "C1": CHANNEL_TO_TIP_ROW_LOOKUP, + "C2": CHANNEL_TO_TIP_ROW_LOOKUP, + "C3": CHANNEL_TO_TIP_ROW_LOOKUP, + "B1": CHANNEL_TO_TIP_ROW_LOOKUP, + "B2": CHANNEL_TO_TIP_ROW_LOOKUP, + "B3": CHANNEL_TO_TIP_ROW_LOOKUP, + "A1": CHANNEL_TO_TIP_ROW_LOOKUP_BACK, + "A2": CHANNEL_TO_TIP_ROW_LOOKUP_BACK, + "A3": CHANNEL_TO_TIP_ROW_LOOKUP_BACK, } REAR_CHANNELS = [0, 1, 2, 3] FRONT_CHANNELS = [4, 5, 6, 7] diff --git a/hardware-testing/hardware_testing/gravimetric/workarounds.py b/hardware-testing/hardware_testing/gravimetric/workarounds.py index 0d2c425d830..7c182ddd079 100644 --- a/hardware-testing/hardware_testing/gravimetric/workarounds.py +++ b/hardware-testing/hardware_testing/gravimetric/workarounds.py @@ -12,6 +12,8 @@ from hardware_testing.opentrons_api.helpers_ot3 import start_server_ot3, stop_server_ot3 from hardware_testing.opentrons_api.types import Point +from opentrons.protocol_engine.types import LabwareOffset + def is_running_in_app() -> bool: """Is running in App.""" @@ -33,7 +35,7 @@ def force_prepare_for_aspirate(pipette: InstrumentContext) -> None: pipette.dispense() -def http_get_all_labware_offsets() -> List[dict]: +def http_get_all_labware_offsets() -> List[LabwareOffset]: """Request (HTTP GET) from the local robot-server all runs information.""" req = Request("http://localhost:31950/runs") req.add_header("Opentrons-Version", "2") @@ -46,7 +48,18 @@ def http_get_all_labware_offsets() -> List[dict]: runs_json = json_loads(runs_response_data) protocols_list = runs_json["data"] - return [offset for p in protocols_list for offset in p["labwareOffsets"]] + offset_dict = [offset for p in protocols_list for offset in p["labwareOffsets"]] + offsets: List[LabwareOffset] = [] + for offset_data in offset_dict: + new_offset = LabwareOffset( + id=offset_data["id"], + createdAt=offset_data["createdAt"], + definitionUri=offset_data["definitionUri"], + location=offset_data["location"], + vector=offset_data["vector"], + ) + offsets.append(new_offset) + return offsets def _old_slot_to_ot3_slot(old_api_slot: str) -> str: diff --git a/hardware-testing/hardware_testing/labware/opentrons_flex_96_tiprack_1000ul_adp/1.json b/hardware-testing/hardware_testing/labware/opentrons_flex_96_tiprack_1000ul_adp/1.json deleted file mode 100644 index 2307f25d876..00000000000 --- a/hardware-testing/hardware_testing/labware/opentrons_flex_96_tiprack_1000ul_adp/1.json +++ /dev/null @@ -1,1017 +0,0 @@ -{ - "ordering": [ - ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"], - ["A2", "B2", "C2", "D2", "E2", "F2", "G2", "H2"], - ["A3", "B3", "C3", "D3", "E3", "F3", "G3", "H3"], - ["A4", "B4", "C4", "D4", "E4", "F4", "G4", "H4"], - ["A5", "B5", "C5", "D5", "E5", "F5", "G5", "H5"], - ["A6", "B6", "C6", "D6", "E6", "F6", "G6", "H6"], - ["A7", "B7", "C7", "D7", "E7", "F7", "G7", "H7"], - ["A8", "B8", "C8", "D8", "E8", "F8", "G8", "H8"], - ["A9", "B9", "C9", "D9", "E9", "F9", "G9", "H9"], - ["A10", "B10", "C10", "D10", "E10", "F10", "G10", "H10"], - ["A11", "B11", "C11", "D11", "E11", "F11", "G11", "H11"], - ["A12", "B12", "C12", "D12", "E12", "F12", "G12", "H12"] - ], - "brand": { - "brand": "ryantrons OT-3", - "brandId": [] - }, - "metadata": { - "displayName": "Opentrons Flex 96 Tip Rack 1000 µL with adapter", - "displayCategory": "tipRack", - "displayVolumeUnits": "µL", - "tags": [] - }, - "dimensions": { - "xDimension": 127.76, - "yDimension": 85.48, - "zDimension": 132 - }, - "wells": { - "A1": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 14.38, - "y": 74.1, - "z": 36.4 - }, - "B1": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 14.38, - "y": 65.1, - "z": 36.4 - }, - "C1": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 14.38, - "y": 56.1, - "z": 36.4 - }, - "D1": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 14.38, - "y": 47.1, - "z": 36.4 - }, - "E1": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 14.38, - "y": 38.1, - "z": 36.4 - }, - "F1": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 14.38, - "y": 29.1, - "z": 36.4 - }, - "G1": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 14.38, - "y": 20.1, - "z": 36.4 - }, - "H1": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 14.38, - "y": 11.1, - "z": 36.4 - }, - "A2": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 23.38, - "y": 74.1, - "z": 36.4 - }, - "B2": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 23.38, - "y": 65.1, - "z": 36.4 - }, - "C2": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 23.38, - "y": 56.1, - "z": 36.4 - }, - "D2": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 23.38, - "y": 47.1, - "z": 36.4 - }, - "E2": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 23.38, - "y": 38.1, - "z": 36.4 - }, - "F2": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 23.38, - "y": 29.1, - "z": 36.4 - }, - "G2": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 23.38, - "y": 20.1, - "z": 36.4 - }, - "H2": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 23.38, - "y": 11.1, - "z": 36.4 - }, - "A3": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 32.38, - "y": 74.1, - "z": 36.4 - }, - "B3": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 32.38, - "y": 65.1, - "z": 36.4 - }, - "C3": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 32.38, - "y": 56.1, - "z": 36.4 - }, - "D3": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 32.38, - "y": 47.1, - "z": 36.4 - }, - "E3": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 32.38, - "y": 38.1, - "z": 36.4 - }, - "F3": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 32.38, - "y": 29.1, - "z": 36.4 - }, - "G3": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 32.38, - "y": 20.1, - "z": 36.4 - }, - "H3": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 32.38, - "y": 11.1, - "z": 36.4 - }, - "A4": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 41.38, - "y": 74.1, - "z": 36.4 - }, - "B4": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 41.38, - "y": 65.1, - "z": 36.4 - }, - "C4": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 41.38, - "y": 56.1, - "z": 36.4 - }, - "D4": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 41.38, - "y": 47.1, - "z": 36.4 - }, - "E4": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 41.38, - "y": 38.1, - "z": 36.4 - }, - "F4": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 41.38, - "y": 29.1, - "z": 36.4 - }, - "G4": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 41.38, - "y": 20.1, - "z": 36.4 - }, - "H4": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 41.38, - "y": 11.1, - "z": 36.4 - }, - "A5": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 50.38, - "y": 74.1, - "z": 36.4 - }, - "B5": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 50.38, - "y": 65.1, - "z": 36.4 - }, - "C5": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 50.38, - "y": 56.1, - "z": 36.4 - }, - "D5": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 50.38, - "y": 47.1, - "z": 36.4 - }, - "E5": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 50.38, - "y": 38.1, - "z": 36.4 - }, - "F5": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 50.38, - "y": 29.1, - "z": 36.4 - }, - "G5": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 50.38, - "y": 20.1, - "z": 36.4 - }, - "H5": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 50.38, - "y": 11.1, - "z": 36.4 - }, - "A6": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 59.38, - "y": 74.1, - "z": 36.4 - }, - "B6": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 59.38, - "y": 65.1, - "z": 36.4 - }, - "C6": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 59.38, - "y": 56.1, - "z": 36.4 - }, - "D6": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 59.38, - "y": 47.1, - "z": 36.4 - }, - "E6": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 59.38, - "y": 38.1, - "z": 36.4 - }, - "F6": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 59.38, - "y": 29.1, - "z": 36.4 - }, - "G6": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 59.38, - "y": 20.1, - "z": 36.4 - }, - "H6": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 59.38, - "y": 11.1, - "z": 36.4 - }, - "A7": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 68.38, - "y": 74.1, - "z": 36.4 - }, - "B7": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 68.38, - "y": 65.1, - "z": 36.4 - }, - "C7": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 68.38, - "y": 56.1, - "z": 36.4 - }, - "D7": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 68.38, - "y": 47.1, - "z": 36.4 - }, - "E7": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 68.38, - "y": 38.1, - "z": 36.4 - }, - "F7": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 68.38, - "y": 29.1, - "z": 36.4 - }, - "G7": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 68.38, - "y": 20.1, - "z": 36.4 - }, - "H7": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 68.38, - "y": 11.1, - "z": 36.4 - }, - "A8": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 77.38, - "y": 74.1, - "z": 36.4 - }, - "B8": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 77.38, - "y": 65.1, - "z": 36.4 - }, - "C8": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 77.38, - "y": 56.1, - "z": 36.4 - }, - "D8": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 77.38, - "y": 47.1, - "z": 36.4 - }, - "E8": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 77.38, - "y": 38.1, - "z": 36.4 - }, - "F8": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 77.38, - "y": 29.1, - "z": 36.4 - }, - "G8": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 77.38, - "y": 20.1, - "z": 36.4 - }, - "H8": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 77.38, - "y": 11.1, - "z": 36.4 - }, - "A9": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 86.38, - "y": 74.1, - "z": 36.4 - }, - "B9": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 86.38, - "y": 65.1, - "z": 36.4 - }, - "C9": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 86.38, - "y": 56.1, - "z": 36.4 - }, - "D9": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 86.38, - "y": 47.1, - "z": 36.4 - }, - "E9": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 86.38, - "y": 38.1, - "z": 36.4 - }, - "F9": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 86.38, - "y": 29.1, - "z": 36.4 - }, - "G9": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 86.38, - "y": 20.1, - "z": 36.4 - }, - "H9": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 86.38, - "y": 11.1, - "z": 36.4 - }, - "A10": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 95.38, - "y": 74.1, - "z": 36.4 - }, - "B10": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 95.38, - "y": 65.1, - "z": 36.4 - }, - "C10": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 95.38, - "y": 56.1, - "z": 36.4 - }, - "D10": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 95.38, - "y": 47.1, - "z": 36.4 - }, - "E10": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 95.38, - "y": 38.1, - "z": 36.4 - }, - "F10": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 95.38, - "y": 29.1, - "z": 36.4 - }, - "G10": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 95.38, - "y": 20.1, - "z": 36.4 - }, - "H10": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 95.38, - "y": 11.1, - "z": 36.4 - }, - "A11": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 104.38, - "y": 74.1, - "z": 36.4 - }, - "B11": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 104.38, - "y": 65.1, - "z": 36.4 - }, - "C11": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 104.38, - "y": 56.1, - "z": 36.4 - }, - "D11": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 104.38, - "y": 47.1, - "z": 36.4 - }, - "E11": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 104.38, - "y": 38.1, - "z": 36.4 - }, - "F11": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 104.38, - "y": 29.1, - "z": 36.4 - }, - "G11": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 104.38, - "y": 20.1, - "z": 36.4 - }, - "H11": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 104.38, - "y": 11.1, - "z": 36.4 - }, - "A12": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 113.38, - "y": 74.1, - "z": 36.4 - }, - "B12": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 113.38, - "y": 65.1, - "z": 36.4 - }, - "C12": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 113.38, - "y": 56.1, - "z": 36.4 - }, - "D12": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 113.38, - "y": 47.1, - "z": 36.4 - }, - "E12": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 113.38, - "y": 38.1, - "z": 36.4 - }, - "F12": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 113.38, - "y": 29.1, - "z": 36.4 - }, - "G12": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 113.38, - "y": 20.1, - "z": 36.4 - }, - "H12": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 113.38, - "y": 11.1, - "z": 36.4 - } - }, - "groups": [ - { - "metadata": {}, - "wells": [ - "A1", - "B1", - "C1", - "D1", - "E1", - "F1", - "G1", - "H1", - "A2", - "B2", - "C2", - "D2", - "E2", - "F2", - "G2", - "H2", - "A3", - "B3", - "C3", - "D3", - "E3", - "F3", - "G3", - "H3", - "A4", - "B4", - "C4", - "D4", - "E4", - "F4", - "G4", - "H4", - "A5", - "B5", - "C5", - "D5", - "E5", - "F5", - "G5", - "H5", - "A6", - "B6", - "C6", - "D6", - "E6", - "F6", - "G6", - "H6", - "A7", - "B7", - "C7", - "D7", - "E7", - "F7", - "G7", - "H7", - "A8", - "B8", - "C8", - "D8", - "E8", - "F8", - "G8", - "H8", - "A9", - "B9", - "C9", - "D9", - "E9", - "F9", - "G9", - "H9", - "A10", - "B10", - "C10", - "D10", - "E10", - "F10", - "G10", - "H10", - "A11", - "B11", - "C11", - "D11", - "E11", - "F11", - "G11", - "H11", - "A12", - "B12", - "C12", - "D12", - "E12", - "F12", - "G12", - "H12" - ] - } - ], - "parameters": { - "format": "96Standard", - "quirks": [], - "isTiprack": true, - "tipLength": 95.6, - "tipOverlap": 10.5, - "isMagneticModuleCompatible": false, - "loadName": "opentrons_flex_96_tiprack_1000ul_adp" - }, - "namespace": "custom_beta", - "version": 1, - "schemaVersion": 2, - "cornerOffsetFromSlot": { - "x": 0, - "y": 0, - "z": 0 - } -} diff --git a/hardware-testing/hardware_testing/labware/opentrons_flex_96_tiprack_200ul_adp/1.json b/hardware-testing/hardware_testing/labware/opentrons_flex_96_tiprack_200ul_adp/1.json deleted file mode 100644 index 439479d5c76..00000000000 --- a/hardware-testing/hardware_testing/labware/opentrons_flex_96_tiprack_200ul_adp/1.json +++ /dev/null @@ -1,1017 +0,0 @@ -{ - "ordering": [ - ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"], - ["A2", "B2", "C2", "D2", "E2", "F2", "G2", "H2"], - ["A3", "B3", "C3", "D3", "E3", "F3", "G3", "H3"], - ["A4", "B4", "C4", "D4", "E4", "F4", "G4", "H4"], - ["A5", "B5", "C5", "D5", "E5", "F5", "G5", "H5"], - ["A6", "B6", "C6", "D6", "E6", "F6", "G6", "H6"], - ["A7", "B7", "C7", "D7", "E7", "F7", "G7", "H7"], - ["A8", "B8", "C8", "D8", "E8", "F8", "G8", "H8"], - ["A9", "B9", "C9", "D9", "E9", "F9", "G9", "H9"], - ["A10", "B10", "C10", "D10", "E10", "F10", "G10", "H10"], - ["A11", "B11", "C11", "D11", "E11", "F11", "G11", "H11"], - ["A12", "B12", "C12", "D12", "E12", "F12", "G12", "H12"] - ], - "brand": { - "brand": "ryantrons OT-3", - "brandId": [] - }, - "metadata": { - "displayName": "Opentrons Flex 96 Tip Rack 200 µL with adapter", - "displayCategory": "tipRack", - "displayVolumeUnits": "µL", - "tags": [] - }, - "dimensions": { - "xDimension": 127.76, - "yDimension": 85.48, - "zDimension": 132 - }, - "wells": { - "A1": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 14.38, - "y": 74.1, - "z": 73.65 - }, - "B1": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 14.38, - "y": 65.1, - "z": 73.65 - }, - "C1": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 14.38, - "y": 56.1, - "z": 73.65 - }, - "D1": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 14.38, - "y": 47.1, - "z": 73.65 - }, - "E1": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 14.38, - "y": 38.1, - "z": 73.65 - }, - "F1": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 14.38, - "y": 29.1, - "z": 73.65 - }, - "G1": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 14.38, - "y": 20.1, - "z": 73.65 - }, - "H1": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 14.38, - "y": 11.1, - "z": 73.65 - }, - "A2": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 23.38, - "y": 74.1, - "z": 73.65 - }, - "B2": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 23.38, - "y": 65.1, - "z": 73.65 - }, - "C2": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 23.38, - "y": 56.1, - "z": 73.65 - }, - "D2": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 23.38, - "y": 47.1, - "z": 73.65 - }, - "E2": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 23.38, - "y": 38.1, - "z": 73.65 - }, - "F2": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 23.38, - "y": 29.1, - "z": 73.65 - }, - "G2": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 23.38, - "y": 20.1, - "z": 73.65 - }, - "H2": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 23.38, - "y": 11.1, - "z": 73.65 - }, - "A3": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 32.38, - "y": 74.1, - "z": 73.65 - }, - "B3": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 32.38, - "y": 65.1, - "z": 73.65 - }, - "C3": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 32.38, - "y": 56.1, - "z": 73.65 - }, - "D3": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 32.38, - "y": 47.1, - "z": 73.65 - }, - "E3": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 32.38, - "y": 38.1, - "z": 73.65 - }, - "F3": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 32.38, - "y": 29.1, - "z": 73.65 - }, - "G3": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 32.38, - "y": 20.1, - "z": 73.65 - }, - "H3": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 32.38, - "y": 11.1, - "z": 73.65 - }, - "A4": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 41.38, - "y": 74.1, - "z": 73.65 - }, - "B4": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 41.38, - "y": 65.1, - "z": 73.65 - }, - "C4": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 41.38, - "y": 56.1, - "z": 73.65 - }, - "D4": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 41.38, - "y": 47.1, - "z": 73.65 - }, - "E4": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 41.38, - "y": 38.1, - "z": 73.65 - }, - "F4": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 41.38, - "y": 29.1, - "z": 73.65 - }, - "G4": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 41.38, - "y": 20.1, - "z": 73.65 - }, - "H4": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 41.38, - "y": 11.1, - "z": 73.65 - }, - "A5": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 50.38, - "y": 74.1, - "z": 73.65 - }, - "B5": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 50.38, - "y": 65.1, - "z": 73.65 - }, - "C5": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 50.38, - "y": 56.1, - "z": 73.65 - }, - "D5": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 50.38, - "y": 47.1, - "z": 73.65 - }, - "E5": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 50.38, - "y": 38.1, - "z": 73.65 - }, - "F5": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 50.38, - "y": 29.1, - "z": 73.65 - }, - "G5": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 50.38, - "y": 20.1, - "z": 73.65 - }, - "H5": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 50.38, - "y": 11.1, - "z": 73.65 - }, - "A6": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 59.38, - "y": 74.1, - "z": 73.65 - }, - "B6": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 59.38, - "y": 65.1, - "z": 73.65 - }, - "C6": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 59.38, - "y": 56.1, - "z": 73.65 - }, - "D6": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 59.38, - "y": 47.1, - "z": 73.65 - }, - "E6": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 59.38, - "y": 38.1, - "z": 73.65 - }, - "F6": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 59.38, - "y": 29.1, - "z": 73.65 - }, - "G6": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 59.38, - "y": 20.1, - "z": 73.65 - }, - "H6": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 59.38, - "y": 11.1, - "z": 73.65 - }, - "A7": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 68.38, - "y": 74.1, - "z": 73.65 - }, - "B7": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 68.38, - "y": 65.1, - "z": 73.65 - }, - "C7": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 68.38, - "y": 56.1, - "z": 73.65 - }, - "D7": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 68.38, - "y": 47.1, - "z": 73.65 - }, - "E7": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 68.38, - "y": 38.1, - "z": 73.65 - }, - "F7": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 68.38, - "y": 29.1, - "z": 73.65 - }, - "G7": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 68.38, - "y": 20.1, - "z": 73.65 - }, - "H7": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 68.38, - "y": 11.1, - "z": 73.65 - }, - "A8": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 77.38, - "y": 74.1, - "z": 73.65 - }, - "B8": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 77.38, - "y": 65.1, - "z": 73.65 - }, - "C8": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 77.38, - "y": 56.1, - "z": 73.65 - }, - "D8": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 77.38, - "y": 47.1, - "z": 73.65 - }, - "E8": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 77.38, - "y": 38.1, - "z": 73.65 - }, - "F8": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 77.38, - "y": 29.1, - "z": 73.65 - }, - "G8": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 77.38, - "y": 20.1, - "z": 73.65 - }, - "H8": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 77.38, - "y": 11.1, - "z": 73.65 - }, - "A9": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 86.38, - "y": 74.1, - "z": 73.65 - }, - "B9": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 86.38, - "y": 65.1, - "z": 73.65 - }, - "C9": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 86.38, - "y": 56.1, - "z": 73.65 - }, - "D9": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 86.38, - "y": 47.1, - "z": 73.65 - }, - "E9": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 86.38, - "y": 38.1, - "z": 73.65 - }, - "F9": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 86.38, - "y": 29.1, - "z": 73.65 - }, - "G9": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 86.38, - "y": 20.1, - "z": 73.65 - }, - "H9": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 86.38, - "y": 11.1, - "z": 73.65 - }, - "A10": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 95.38, - "y": 74.1, - "z": 73.65 - }, - "B10": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 95.38, - "y": 65.1, - "z": 73.65 - }, - "C10": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 95.38, - "y": 56.1, - "z": 73.65 - }, - "D10": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 95.38, - "y": 47.1, - "z": 73.65 - }, - "E10": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 95.38, - "y": 38.1, - "z": 73.65 - }, - "F10": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 95.38, - "y": 29.1, - "z": 73.65 - }, - "G10": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 95.38, - "y": 20.1, - "z": 73.65 - }, - "H10": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 95.38, - "y": 11.1, - "z": 73.65 - }, - "A11": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 104.38, - "y": 74.1, - "z": 73.65 - }, - "B11": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 104.38, - "y": 65.1, - "z": 73.65 - }, - "C11": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 104.38, - "y": 56.1, - "z": 73.65 - }, - "D11": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 104.38, - "y": 47.1, - "z": 73.65 - }, - "E11": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 104.38, - "y": 38.1, - "z": 73.65 - }, - "F11": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 104.38, - "y": 29.1, - "z": 73.65 - }, - "G11": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 104.38, - "y": 20.1, - "z": 73.65 - }, - "H11": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 104.38, - "y": 11.1, - "z": 73.65 - }, - "A12": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 113.38, - "y": 74.1, - "z": 73.65 - }, - "B12": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 113.38, - "y": 65.1, - "z": 73.65 - }, - "C12": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 113.38, - "y": 56.1, - "z": 73.65 - }, - "D12": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 113.38, - "y": 47.1, - "z": 73.65 - }, - "E12": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 113.38, - "y": 38.1, - "z": 73.65 - }, - "F12": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 113.38, - "y": 29.1, - "z": 73.65 - }, - "G12": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 113.38, - "y": 20.1, - "z": 73.65 - }, - "H12": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 113.38, - "y": 11.1, - "z": 73.65 - } - }, - "groups": [ - { - "metadata": {}, - "wells": [ - "A1", - "B1", - "C1", - "D1", - "E1", - "F1", - "G1", - "H1", - "A2", - "B2", - "C2", - "D2", - "E2", - "F2", - "G2", - "H2", - "A3", - "B3", - "C3", - "D3", - "E3", - "F3", - "G3", - "H3", - "A4", - "B4", - "C4", - "D4", - "E4", - "F4", - "G4", - "H4", - "A5", - "B5", - "C5", - "D5", - "E5", - "F5", - "G5", - "H5", - "A6", - "B6", - "C6", - "D6", - "E6", - "F6", - "G6", - "H6", - "A7", - "B7", - "C7", - "D7", - "E7", - "F7", - "G7", - "H7", - "A8", - "B8", - "C8", - "D8", - "E8", - "F8", - "G8", - "H8", - "A9", - "B9", - "C9", - "D9", - "E9", - "F9", - "G9", - "H9", - "A10", - "B10", - "C10", - "D10", - "E10", - "F10", - "G10", - "H10", - "A11", - "B11", - "C11", - "D11", - "E11", - "F11", - "G11", - "H11", - "A12", - "B12", - "C12", - "D12", - "E12", - "F12", - "G12", - "H12" - ] - } - ], - "parameters": { - "format": "96Standard", - "quirks": [], - "isTiprack": true, - "tipLength": 58.35, - "tipOverlap": 10.5, - "isMagneticModuleCompatible": false, - "loadName": "opentrons_flex_96_tiprack_200ul_adp" - }, - "namespace": "custom_beta", - "version": 1, - "schemaVersion": 2, - "cornerOffsetFromSlot": { - "x": 0, - "y": 0, - "z": 0 - } -} diff --git a/hardware-testing/hardware_testing/labware/opentrons_flex_96_tiprack_50ul_adp/1.json b/hardware-testing/hardware_testing/labware/opentrons_flex_96_tiprack_50ul_adp/1.json deleted file mode 100644 index a4d1b339097..00000000000 --- a/hardware-testing/hardware_testing/labware/opentrons_flex_96_tiprack_50ul_adp/1.json +++ /dev/null @@ -1,1017 +0,0 @@ -{ - "ordering": [ - ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"], - ["A2", "B2", "C2", "D2", "E2", "F2", "G2", "H2"], - ["A3", "B3", "C3", "D3", "E3", "F3", "G3", "H3"], - ["A4", "B4", "C4", "D4", "E4", "F4", "G4", "H4"], - ["A5", "B5", "C5", "D5", "E5", "F5", "G5", "H5"], - ["A6", "B6", "C6", "D6", "E6", "F6", "G6", "H6"], - ["A7", "B7", "C7", "D7", "E7", "F7", "G7", "H7"], - ["A8", "B8", "C8", "D8", "E8", "F8", "G8", "H8"], - ["A9", "B9", "C9", "D9", "E9", "F9", "G9", "H9"], - ["A10", "B10", "C10", "D10", "E10", "F10", "G10", "H10"], - ["A11", "B11", "C11", "D11", "E11", "F11", "G11", "H11"], - ["A12", "B12", "C12", "D12", "E12", "F12", "G12", "H12"] - ], - "brand": { - "brand": "ryantrons OT-3", - "brandId": [] - }, - "metadata": { - "displayName": "Opentrons Flex 96 Tip Rack 50 µL with adapter", - "displayCategory": "tipRack", - "displayVolumeUnits": "µL", - "tags": [] - }, - "dimensions": { - "xDimension": 127.76, - "yDimension": 85.48, - "zDimension": 132 - }, - "wells": { - "A1": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 14.38, - "y": 74.1, - "z": 74.1 - }, - "B1": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 14.38, - "y": 65.1, - "z": 74.1 - }, - "C1": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 14.38, - "y": 56.1, - "z": 74.1 - }, - "D1": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 14.38, - "y": 47.1, - "z": 74.1 - }, - "E1": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 14.38, - "y": 38.1, - "z": 74.1 - }, - "F1": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 14.38, - "y": 29.1, - "z": 74.1 - }, - "G1": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 14.38, - "y": 20.1, - "z": 74.1 - }, - "H1": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 14.38, - "y": 11.1, - "z": 74.1 - }, - "A2": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 23.38, - "y": 74.1, - "z": 74.1 - }, - "B2": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 23.38, - "y": 65.1, - "z": 74.1 - }, - "C2": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 23.38, - "y": 56.1, - "z": 74.1 - }, - "D2": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 23.38, - "y": 47.1, - "z": 74.1 - }, - "E2": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 23.38, - "y": 38.1, - "z": 74.1 - }, - "F2": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 23.38, - "y": 29.1, - "z": 74.1 - }, - "G2": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 23.38, - "y": 20.1, - "z": 74.1 - }, - "H2": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 23.38, - "y": 11.1, - "z": 74.1 - }, - "A3": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 32.38, - "y": 74.1, - "z": 74.1 - }, - "B3": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 32.38, - "y": 65.1, - "z": 74.1 - }, - "C3": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 32.38, - "y": 56.1, - "z": 74.1 - }, - "D3": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 32.38, - "y": 47.1, - "z": 74.1 - }, - "E3": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 32.38, - "y": 38.1, - "z": 74.1 - }, - "F3": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 32.38, - "y": 29.1, - "z": 74.1 - }, - "G3": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 32.38, - "y": 20.1, - "z": 74.1 - }, - "H3": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 32.38, - "y": 11.1, - "z": 74.1 - }, - "A4": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 41.38, - "y": 74.1, - "z": 74.1 - }, - "B4": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 41.38, - "y": 65.1, - "z": 74.1 - }, - "C4": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 41.38, - "y": 56.1, - "z": 74.1 - }, - "D4": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 41.38, - "y": 47.1, - "z": 74.1 - }, - "E4": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 41.38, - "y": 38.1, - "z": 74.1 - }, - "F4": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 41.38, - "y": 29.1, - "z": 74.1 - }, - "G4": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 41.38, - "y": 20.1, - "z": 74.1 - }, - "H4": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 41.38, - "y": 11.1, - "z": 74.1 - }, - "A5": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 50.38, - "y": 74.1, - "z": 74.1 - }, - "B5": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 50.38, - "y": 65.1, - "z": 74.1 - }, - "C5": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 50.38, - "y": 56.1, - "z": 74.1 - }, - "D5": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 50.38, - "y": 47.1, - "z": 74.1 - }, - "E5": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 50.38, - "y": 38.1, - "z": 74.1 - }, - "F5": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 50.38, - "y": 29.1, - "z": 74.1 - }, - "G5": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 50.38, - "y": 20.1, - "z": 74.1 - }, - "H5": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 50.38, - "y": 11.1, - "z": 74.1 - }, - "A6": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 59.38, - "y": 74.1, - "z": 74.1 - }, - "B6": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 59.38, - "y": 65.1, - "z": 74.1 - }, - "C6": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 59.38, - "y": 56.1, - "z": 74.1 - }, - "D6": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 59.38, - "y": 47.1, - "z": 74.1 - }, - "E6": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 59.38, - "y": 38.1, - "z": 74.1 - }, - "F6": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 59.38, - "y": 29.1, - "z": 74.1 - }, - "G6": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 59.38, - "y": 20.1, - "z": 74.1 - }, - "H6": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 59.38, - "y": 11.1, - "z": 74.1 - }, - "A7": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 68.38, - "y": 74.1, - "z": 74.1 - }, - "B7": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 68.38, - "y": 65.1, - "z": 74.1 - }, - "C7": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 68.38, - "y": 56.1, - "z": 74.1 - }, - "D7": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 68.38, - "y": 47.1, - "z": 74.1 - }, - "E7": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 68.38, - "y": 38.1, - "z": 74.1 - }, - "F7": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 68.38, - "y": 29.1, - "z": 74.1 - }, - "G7": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 68.38, - "y": 20.1, - "z": 74.1 - }, - "H7": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 68.38, - "y": 11.1, - "z": 74.1 - }, - "A8": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 77.38, - "y": 74.1, - "z": 74.1 - }, - "B8": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 77.38, - "y": 65.1, - "z": 74.1 - }, - "C8": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 77.38, - "y": 56.1, - "z": 74.1 - }, - "D8": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 77.38, - "y": 47.1, - "z": 74.1 - }, - "E8": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 77.38, - "y": 38.1, - "z": 74.1 - }, - "F8": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 77.38, - "y": 29.1, - "z": 74.1 - }, - "G8": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 77.38, - "y": 20.1, - "z": 74.1 - }, - "H8": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 77.38, - "y": 11.1, - "z": 74.1 - }, - "A9": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 86.38, - "y": 74.1, - "z": 74.1 - }, - "B9": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 86.38, - "y": 65.1, - "z": 74.1 - }, - "C9": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 86.38, - "y": 56.1, - "z": 74.1 - }, - "D9": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 86.38, - "y": 47.1, - "z": 74.1 - }, - "E9": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 86.38, - "y": 38.1, - "z": 74.1 - }, - "F9": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 86.38, - "y": 29.1, - "z": 74.1 - }, - "G9": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 86.38, - "y": 20.1, - "z": 74.1 - }, - "H9": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 86.38, - "y": 11.1, - "z": 74.1 - }, - "A10": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 95.38, - "y": 74.1, - "z": 74.1 - }, - "B10": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 95.38, - "y": 65.1, - "z": 74.1 - }, - "C10": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 95.38, - "y": 56.1, - "z": 74.1 - }, - "D10": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 95.38, - "y": 47.1, - "z": 74.1 - }, - "E10": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 95.38, - "y": 38.1, - "z": 74.1 - }, - "F10": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 95.38, - "y": 29.1, - "z": 74.1 - }, - "G10": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 95.38, - "y": 20.1, - "z": 74.1 - }, - "H10": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 95.38, - "y": 11.1, - "z": 74.1 - }, - "A11": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 104.38, - "y": 74.1, - "z": 74.1 - }, - "B11": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 104.38, - "y": 65.1, - "z": 74.1 - }, - "C11": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 104.38, - "y": 56.1, - "z": 74.1 - }, - "D11": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 104.38, - "y": 47.1, - "z": 74.1 - }, - "E11": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 104.38, - "y": 38.1, - "z": 74.1 - }, - "F11": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 104.38, - "y": 29.1, - "z": 74.1 - }, - "G11": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 104.38, - "y": 20.1, - "z": 74.1 - }, - "H11": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 104.38, - "y": 11.1, - "z": 74.1 - }, - "A12": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 113.38, - "y": 74.1, - "z": 74.1 - }, - "B12": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 113.38, - "y": 65.1, - "z": 74.1 - }, - "C12": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 113.38, - "y": 56.1, - "z": 74.1 - }, - "D12": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 113.38, - "y": 47.1, - "z": 74.1 - }, - "E12": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 113.38, - "y": 38.1, - "z": 74.1 - }, - "F12": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 113.38, - "y": 29.1, - "z": 74.1 - }, - "G12": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 113.38, - "y": 20.1, - "z": 74.1 - }, - "H12": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 113.38, - "y": 11.1, - "z": 74.1 - } - }, - "groups": [ - { - "metadata": {}, - "wells": [ - "A1", - "B1", - "C1", - "D1", - "E1", - "F1", - "G1", - "H1", - "A2", - "B2", - "C2", - "D2", - "E2", - "F2", - "G2", - "H2", - "A3", - "B3", - "C3", - "D3", - "E3", - "F3", - "G3", - "H3", - "A4", - "B4", - "C4", - "D4", - "E4", - "F4", - "G4", - "H4", - "A5", - "B5", - "C5", - "D5", - "E5", - "F5", - "G5", - "H5", - "A6", - "B6", - "C6", - "D6", - "E6", - "F6", - "G6", - "H6", - "A7", - "B7", - "C7", - "D7", - "E7", - "F7", - "G7", - "H7", - "A8", - "B8", - "C8", - "D8", - "E8", - "F8", - "G8", - "H8", - "A9", - "B9", - "C9", - "D9", - "E9", - "F9", - "G9", - "H9", - "A10", - "B10", - "C10", - "D10", - "E10", - "F10", - "G10", - "H10", - "A11", - "B11", - "C11", - "D11", - "E11", - "F11", - "G11", - "H11", - "A12", - "B12", - "C12", - "D12", - "E12", - "F12", - "G12", - "H12" - ] - } - ], - "parameters": { - "format": "96Standard", - "quirks": [], - "isTiprack": true, - "tipLength": 57.9, - "tipOverlap": 10.5, - "isMagneticModuleCompatible": false, - "loadName": "opentrons_flex_96_tiprack_50ul_adp" - }, - "namespace": "custom_beta", - "version": 1, - "schemaVersion": 2, - "cornerOffsetFromSlot": { - "x": 0, - "y": 0, - "z": 0 - } -} diff --git a/hardware-testing/hardware_testing/opentrons_api/helpers_ot3.py b/hardware-testing/hardware_testing/opentrons_api/helpers_ot3.py index 4beae74bdd9..f277ff93f76 100644 --- a/hardware-testing/hardware_testing/opentrons_api/helpers_ot3.py +++ b/hardware-testing/hardware_testing/opentrons_api/helpers_ot3.py @@ -113,7 +113,7 @@ def _create_fake_pipette_id(mount: OT3Mount, model: Optional[str]) -> Optional[s assert len(items) == 3 size = "P1K" if items[0] == "p1000" else "P50" channels = "S" if items[1] == "single" else "M" - version = items[2].upper().replace(".", "") + version = 35 # model names don't have a version so just fake a 3.5 version date = datetime.now().strftime("%y%m%d") unique_number = 1 if mount == OT3Mount.LEFT else 2 return f"{size}{channels}{version}{date}A0{unique_number}" diff --git a/hardware-testing/hardware_testing/protocols/gravimetric_lpc/gravimetric/gravimetric_ot3_p1000_96.py b/hardware-testing/hardware_testing/protocols/gravimetric_lpc/gravimetric/gravimetric_ot3_p1000_96.py index e4901928a34..6fe882f5370 100644 --- a/hardware-testing/hardware_testing/protocols/gravimetric_lpc/gravimetric/gravimetric_ot3_p1000_96.py +++ b/hardware-testing/hardware_testing/protocols/gravimetric_lpc/gravimetric/gravimetric_ot3_p1000_96.py @@ -1,5 +1,6 @@ """Photometric OT3 P1000.""" from opentrons.protocol_api import ProtocolContext +from opentrons.protocol_api._types import OffDeckType metadata = {"protocolName": "gravimetric-ot3-p1000-96"} requirements = {"robotType": "Flex", "apiLevel": "2.15"} @@ -8,24 +9,34 @@ SLOTS_TIPRACK = { # TODO: add slot 12 when tipracks are disposable 50: [2, 3, 5, 6, 7, 8, 9, 10, 11], - 200: [2, 3, 5, 6, 7, 8, 9, 10, 11], # NOTE: ignored during calibration - 1000: [2, 3, 5, 6, 7, 8, 9, 10, 11], # NOTE: ignored during calibration + 200: [2, 3, 5, 6, 7, 8, 9, 10, 11], + 1000: [2, 3, 5, 6, 7, 8, 9, 10, 11], } LABWARE_ON_SCALE = "nest_1_reservoir_195ml" def run(ctx: ProtocolContext) -> None: """Run.""" - tipracks = [ - ctx.load_labware(f"opentrons_flex_96_tiprack_{size}uL_adp", slot) - for size, slots in SLOTS_TIPRACK.items() - for slot in slots - if size == 50 # only calibrate 50ul tip-racks - ] scale_labware = ctx.load_labware(LABWARE_ON_SCALE, SLOT_SCALE) pipette = ctx.load_instrument("flex_96channel_1000", "left") - for rack in tipracks: - pipette.pick_up_tip(rack["A1"]) - pipette.aspirate(10, scale_labware["A1"].top()) - pipette.dispense(10, scale_labware["A1"].top()) - pipette.drop_tip(home_after=False) + adapters = [ + ctx.load_adapter("opentrons_flex_96_tiprack_adapter", slot) + for slot in SLOTS_TIPRACK[50] + ] + for tip_size in SLOTS_TIPRACK.keys(): + tipracks = [ + adapter.load_labware(f"opentrons_flex_96_tiprack_{tip_size}uL") + for adapter in adapters + ] + for rack in tipracks: + pipette.pick_up_tip(rack) + pipette.aspirate(10, scale_labware["A1"].top()) + pipette.dispense(10, scale_labware["A1"].top()) + pipette.drop_tip(home_after=False) + + for rack in tipracks: + ctx.move_labware( + rack, + new_location=OffDeckType.OFF_DECK, + use_gripper=False, + ) diff --git a/hardware-testing/hardware_testing/protocols/gravimetric_lpc/photometric/photometric_ot3_p1000_96.py b/hardware-testing/hardware_testing/protocols/gravimetric_lpc/photometric/photometric_ot3_p1000_96.py index 4be97d86289..2cb4dcc1daf 100644 --- a/hardware-testing/hardware_testing/protocols/gravimetric_lpc/photometric/photometric_ot3_p1000_96.py +++ b/hardware-testing/hardware_testing/protocols/gravimetric_lpc/photometric/photometric_ot3_p1000_96.py @@ -1,12 +1,13 @@ """Photometric OT3 P1000.""" from opentrons.protocol_api import ProtocolContext +from opentrons.protocol_api._types import OffDeckType metadata = {"protocolName": "photometric-ot3-p1000-96"} requirements = {"robotType": "Flex", "apiLevel": "2.15"} SLOTS_TIPRACK = { 50: [5, 6, 8, 9, 11], - 200: [5, 6, 8, 9, 11], # NOTE: ignoring this tip-rack during run() method + 200: [5, 6, 8, 9, 11], } SLOT_PLATE = 3 SLOT_RESERVOIR = 2 @@ -17,20 +18,27 @@ def run(ctx: ProtocolContext) -> None: """Run.""" - tipracks = [ - # FIXME: use official tip-racks once available - ctx.load_labware( - f"opentrons_flex_96_tiprack_{size}uL_adp", slot, namespace="custom_beta" - ) - for size, slots in SLOTS_TIPRACK.items() - for slot in slots - if size == 50 # only calibrate 50ul tips for 96ch test - ] reservoir = ctx.load_labware(RESERVOIR_LABWARE, SLOT_RESERVOIR) plate = ctx.load_labware(PHOTOPLATE_LABWARE, SLOT_PLATE) pipette = ctx.load_instrument("flex_96channel_1000", "left") - for rack in tipracks: - pipette.pick_up_tip(rack["A1"]) - pipette.aspirate(10, reservoir["A1"].top()) - pipette.dispense(10, plate["A1"].top()) - pipette.drop_tip(home_after=False) + adapters = [ + ctx.load_adapter("opentrons_flex_96_tiprack_adapter", slot) + for slot in SLOTS_TIPRACK[50] + ] + for tip_size in SLOTS_TIPRACK.keys(): + tipracks = [ + adapter.load_labware(f"opentrons_flex_96_tiprack_{tip_size}uL") + for adapter in adapters + ] + for rack in tipracks: + pipette.pick_up_tip(rack) + pipette.aspirate(10, reservoir["A1"].top()) + pipette.dispense(10, plate["A1"].top()) + pipette.drop_tip(home_after=False) + + for rack in tipracks: + ctx.move_labware( + rack, + new_location=OffDeckType.OFF_DECK, + use_gripper=False, + ) diff --git a/hardware-testing/tests/hardware_testing/liquid/test_heights.py b/hardware-testing/tests/hardware_testing/liquid/test_heights.py index ab73b54618c..39efb419e65 100644 --- a/hardware-testing/tests/hardware_testing/liquid/test_heights.py +++ b/hardware-testing/tests/hardware_testing/liquid/test_heights.py @@ -17,7 +17,7 @@ def _create_context() -> ProtocolContext: - return get_api_context(api_level="2.13", is_simulating=True) + return get_api_context(api_level="2.16", is_simulating=True) def _load_labware(ctx: ProtocolContext) -> Tuple[Labware, Labware, Labware, Labware]: From f3c6b0d7b051cc5c68d975876e810b27baad55b1 Mon Sep 17 00:00:00 2001 From: Derek Maggio Date: Thu, 4 Apr 2024 09:45:45 -0700 Subject: [PATCH 044/194] test: Fix 2.17 smoke test (#14801) # Overview I think my logic behind making the 2.17 smoke test blank was wrong. I think it just needs to be a straight copy of 2.16 smoke test with the api version updated to 2.17 Pulled from protocol ``` # This protocol is exactly the same as 2.16 Smoke Test V3 # The only difference is the API version in the metadata # There were no new positive test cases for 2.17 # The negative test cases are captured in the 2.17 dispense changes protcol ``` --- ...T2_P300M_P20S_TC_HS_TM_2_17_SmokeTestV3.py | 363 +++++++++++++++++- 1 file changed, 355 insertions(+), 8 deletions(-) diff --git a/app-testing/files/protocols/py/OT2_P300M_P20S_TC_HS_TM_2_17_SmokeTestV3.py b/app-testing/files/protocols/py/OT2_P300M_P20S_TC_HS_TM_2_17_SmokeTestV3.py index d9f4f62970a..fdb7c172256 100644 --- a/app-testing/files/protocols/py/OT2_P300M_P20S_TC_HS_TM_2_17_SmokeTestV3.py +++ b/app-testing/files/protocols/py/OT2_P300M_P20S_TC_HS_TM_2_17_SmokeTestV3.py @@ -3,23 +3,370 @@ from opentrons import protocol_api metadata = { - "protocolName": "🛠️ 2.17 Smoke Test", + "protocolName": "🛠️ 2.17 Smoke Test V3 🪄", "author": "Opentrons Engineering ", "source": "Software Testing Team", - "description": ("Placeholder - 2.17 Smoke Test is the same a 2.16 Smoke Test."), + "description": ("Description of the protocol that is longish \n has \n returns and \n emoji 😊 ⬆️ "), } -requirements = {"robotType": "OT-2", "apiLevel": "2.16"} +requirements = {"robotType": "OT-2", "apiLevel": "2.17"} + + +######################### +#### LOOK AT THIS ####### +######################### + +# This protocol is exactly the same as 2.16 Smoke Test V3 +# The only difference is the API version in the metadata +# There were no new positive test cases for 2.17 +# The negative test cases are captured in the 2.17 dispense changes protcol + +######################### +#### LOOK AT THIS ####### +######################### def run(ctx: protocol_api.ProtocolContext) -> None: """This method is run by the protocol engine.""" - # The only change in api version 2.17 is an error is thrown when you try to dispense more than the current volume of liquid in the pipette. - # Since the smoke test protocol should be able to be ran through without any errors, the test for the dispense error should not be added to the smoke test protocol. + ctx.set_rail_lights(True) + ctx.comment(f"Let there be light! {ctx.rail_lights_on} 🌠🌠🌠") + ctx.comment(f"Is the door is closed? {ctx.door_closed} 🚪🚪🚪") + ctx.comment(f"Is this a simulation? {ctx.is_simulating()} 🔮🔮🔮") + ctx.comment(f"Running against API Version: {ctx.api_version}") + + # deck positions + tips_300ul_position = "5" + tips_20ul_position = "4" + dye_source_position = "3" + logo_position = "2" + temperature_position = "9" + custom_lw_position = "6" + hs_position = "1" + + # Thermocycler has a default position that covers Slots 7, 8, 10, and 11. + # This is the only valid location for the Thermocycler on the OT-2 deck. + # This position is a default parameter when declaring the TC so you do not need to specify. + + # 300ul tips + tips_300ul = [ + ctx.load_labware( + load_name="opentrons_96_tiprack_300ul", + location=tips_300ul_position, + label="300ul tips", + ) + ] + + # 20ul tips + tips_20ul = [ + ctx.load_labware( + load_name="opentrons_96_tiprack_20ul", + location=tips_20ul_position, + label="20ul tips", + ) + ] + + # pipettes + pipette_left = ctx.load_instrument(instrument_name="p300_multi_gen2", mount="left", tip_racks=tips_300ul) + + pipette_right = ctx.load_instrument(instrument_name="p20_single_gen2", mount="right", tip_racks=tips_20ul) + + # modules https://docs.opentrons.com/v2/new_modules.html#available-modules + hs_module = ctx.load_module("heaterShakerModuleV1", hs_position) + temperature_module = ctx.load_module("temperature module gen2", temperature_position) + thermocycler_module = ctx.load_module("thermocycler module gen2") + + # module labware + temp_adapter = temperature_module.load_adapter("opentrons_96_well_aluminum_block") + temp_plate = temp_adapter.load_labware( + "nest_96_wellplate_100ul_pcr_full_skirt", + label="Temperature-Controlled plate", + ) + hs_plate = hs_module.load_labware(name="nest_96_wellplate_100ul_pcr_full_skirt", adapter="opentrons_96_pcr_adapter") + tc_plate = thermocycler_module.load_labware("nest_96_wellplate_100ul_pcr_full_skirt") + + # A 2.14 difference, no params specified, still should find it. + custom_labware = ctx.load_labware( + "cpx_4_tuberack_100ul", + custom_lw_position, + label="4 custom tubes", + ) + + # create plates and pattern list + logo_destination_plate = ctx.load_labware( + load_name="nest_96_wellplate_100ul_pcr_full_skirt", + location=logo_position, + label="logo destination", + ) + + dye_container = ctx.load_labware( + load_name="nest_12_reservoir_15ml", + location=dye_source_position, + label="dye container", + ) + + dye_source = dye_container.wells_by_name()["A2"] + + # Well Location set-up + dye_destination_wells = [ + logo_destination_plate.wells_by_name()["C7"], + logo_destination_plate.wells_by_name()["D6"], + logo_destination_plate.wells_by_name()["D7"], + logo_destination_plate.wells_by_name()["D8"], + logo_destination_plate.wells_by_name()["E5"], + ] + + # >= 2.14 define_liquid and load_liquid + water = ctx.define_liquid( + name="water", description="H₂O", display_color="#42AB2D" + ) # subscript 2 https://www.compart.com/en/unicode/U+2082 + + acetone = ctx.define_liquid( + name="acetone", description="C₃H₆O", display_color="#38588a" + ) # subscript 3 https://www.compart.com/en/unicode/U+2083 + # subscript 6 https://www.compart.com/en/unicode/U+2086 + + dye_container.wells_by_name()["A1"].load_liquid(liquid=water, volume=4000) + dye_container.wells_by_name()["A2"].load_liquid(liquid=water, volume=2000) + dye_container.wells_by_name()["A5"].load_liquid(liquid=acetone, volume=555.55555) + + # 2 different liquids in the same well + dye_container.wells_by_name()["A8"].load_liquid(liquid=water, volume=900.00) + dye_container.wells_by_name()["A8"].load_liquid(liquid=acetone, volume=1001.11) + + hs_module.close_labware_latch() + + pipette_right.pick_up_tip() + + ################################## + # Manual Deck State Modification # + ################################## + + # -------------------------- # + # Added in API version: 2.15 # + # -------------------------- # + + # Putting steps for this at beginning of protocol so you can do the manual stuff + # then walk away to let the rest of the protocol execute + + # The test flow is as follows: + # 1. Remove the existing PCR plate from slot 2 + # 2. Move the reservoir from slot 3 to slot 2 + # 3. Pickup P20 tip, move pipette to reservoir A1 in slot 2 + # 4. Pause and ask user to validate that the tip is in the middle of reservoir A1 in slot 2 + # 5. Move the reservoir back to slot 3 from slot 2 + # 6. Move pipette to reservoir A1 in slot 3 + # 7. Pause and ask user to validate that the tip is in the middle of reservoir A1 in slot 3 + # 8. Move custom labware from slot 6 to slot 2 + # 9. Move pipette to well A1 in slot 2 + # 10. Pause and ask user to validate that the tip is in the middle of well A1 in slot 2 + # 11. Move the custom labware back to slot 6 from slot 2 + # 12. Move pipette to well A1 in slot 6 + # 13. Pause and ask user to validate that the tip is in the middle of well A1 in slot 6 + # 14. Move the offdeck PCR plate back to slot 2 + # 15. Move pipette to well A1 in slot 2 + # 16. Pause and ask user to validate that the tip is in the middle of well A1 in slot 2 + + # In effect, nothing will actually change to the protocol, + # but we will be able to test that the UI responds appropriately. + + # Note: + # logo_destination_plate is a nest_96_wellplate_100ul_pcr_full_skirt - starting position is slot 2 + # dye_container is a nest_12_reservoir_15ml - starting position is slot 3 + + # Step 1 + ctx.move_labware( + labware=logo_destination_plate, + new_location=protocol_api.OFF_DECK, + ) + + # Step 2 + ctx.move_labware(labware=dye_container, new_location="2") + + # Step 3 + pipette_right.move_to(location=dye_container.wells_by_name()["A1"].top()) + + # Step 4 + ctx.pause("Is the pipette tip in the middle of reservoir A1 in slot 2?") + + # Step 5 + ctx.move_labware(labware=dye_container, new_location="3") + + # Step 6 + pipette_right.move_to(location=dye_container.wells_by_name()["A1"].top()) + + # Step 7 + ctx.pause("Is the pipette tip in the middle of reservoir A1 in slot 3?") + + # Step 8 + ctx.move_labware(labware=custom_labware, new_location="2") + + # Step 9 + pipette_right.move_to(location=custom_labware.wells_by_name()["A1"].top()) + + # Step 10 + ctx.pause("Is the pipette tip in the middle of custom labware A1 in slot 2?") + + # Step 11 + ctx.move_labware(labware=custom_labware, new_location="6") + + # Step 12 + pipette_right.move_to(location=custom_labware.wells_by_name()["A1"].top()) + + # Step 13 + ctx.pause("Is the pipette tip in the middle of custom labware A1 in slot 6?") + + # Step 14 + ctx.move_labware(labware=logo_destination_plate, new_location="2") + + # Step 15 + pipette_right.move_to(location=logo_destination_plate.wells_by_name()["A1"].top()) + + # Step 16 + ctx.pause("Is the pipette tip in the middle of well A1 in slot 2?") + + ####################### + # prepare_to_aspirate # + ####################### + + # -------------------------- # + # Added in API version: 2.16 # + # -------------------------- # + + pipette_right.prepare_to_aspirate() + pipette_right.move_to(dye_container.wells_by_name()["A1"].bottom(z=2)) + ctx.pause( + "Testing prepare_to_aspirate - watch pipette until next pause.\n The pipette should only move up out of the well after it has aspirated." + ) + pipette_right.aspirate(10, dye_container.wells_by_name()["A1"].bottom(z=2)) + ctx.pause("Did the pipette move up out of the well, only once, after aspirating?") + pipette_right.dispense(10, dye_container.wells_by_name()["A1"].bottom(z=2)) + + ######################################### + # protocol_context.fixed_trash property # + ######################################### + + # ---------------------------- # + # Changed in API version: 2.16 # + # ---------------------------- # + + pipette_right.move_to(ctx.fixed_trash) + ctx.pause("Is the pipette over the trash? Pipette will home after this pause.") + ctx.home() + + ############################################### + # instrument_context.trash_container property # + ############################################### + + # ---------------------------- # + # Changed in API version: 2.16 # + # ---------------------------- # + + pipette_right.move_to(pipette_right.trash_container) + ctx.pause("Is the pipette over the trash?") + + # Distribute dye + pipette_right.distribute( + volume=18, + source=dye_source, + dest=dye_destination_wells, + new_tip="never", + ) + pipette_right.drop_tip() + + # transfer + transfer_destinations = [ + logo_destination_plate.wells_by_name()["A11"], + logo_destination_plate.wells_by_name()["B11"], + logo_destination_plate.wells_by_name()["C11"], + ] + pipette_right.pick_up_tip() + pipette_right.transfer( + volume=60, + source=dye_container.wells_by_name()["A2"], + dest=transfer_destinations, + new_tip="never", + touch_tip=True, + blow_out=True, + blowout_location="destination well", + mix_before=(3, 20), + mix_after=(1, 20), + mix_touch_tip=True, + ) + + # consolidate + pipette_right.consolidate( + volume=20, + source=transfer_destinations, + dest=dye_container.wells_by_name()["A5"], + new_tip="never", + touch_tip=False, + blow_out=True, + blowout_location="destination well", + mix_before=(3, 20), + ) + + # well to well + pipette_right.return_tip() + pipette_right.pick_up_tip() + pipette_right.aspirate(volume=5, location=logo_destination_plate.wells_by_name()["A11"]) + pipette_right.air_gap(volume=10) + ctx.delay(seconds=3) + pipette_right.dispense(volume=5, location=logo_destination_plate.wells_by_name()["H11"]) + + # move to + pipette_right.move_to(logo_destination_plate.wells_by_name()["E12"].top()) + pipette_right.move_to(logo_destination_plate.wells_by_name()["E11"].bottom()) + pipette_right.blow_out() + # touch tip + # pipette ends in the middle of the well as of 6.3.0 in all touch_tip + pipette_right.touch_tip(location=logo_destination_plate.wells_by_name()["H1"]) + ctx.pause("Is the pipette tip in the middle of the well?") + pipette_right.return_tip() + + # Play with the modules + temperature_module.await_temperature(25) + + hs_module.set_and_wait_for_shake_speed(466) + ctx.delay(seconds=5) + + hs_module.set_and_wait_for_temperature(38) + + thermocycler_module.open_lid() + thermocycler_module.close_lid() + thermocycler_module.set_lid_temperature(38) # 37 is the minimum + thermocycler_module.set_block_temperature(temperature=28, hold_time_seconds=5) + thermocycler_module.deactivate_block() + thermocycler_module.deactivate_lid() + thermocycler_module.open_lid() + + hs_module.deactivate_shaker() + + # dispense to modules + + # to temperature module + pipette_right.pick_up_tip() + pipette_right.aspirate(volume=15, location=dye_source) + pipette_right.dispense(volume=15, location=temp_plate.well(0)) + pipette_right.drop_tip() - # Instead it should be added to a separate test protocol - OT2_P300M_P20S_TC_HS_TM_2_17_dispense_changes.py + # to heater shaker + pipette_left.pick_up_tip() + pipette_left.aspirate(volume=50, location=dye_source) + pipette_left.dispense(volume=50, location=hs_plate.well(0)) + hs_module.set_and_wait_for_shake_speed(350) + ctx.delay(seconds=5) + hs_module.deactivate_shaker() - # Therefore the 2.17 smoke test protocol is the same as the 2.16 smoke test protocol. Instead of copying and pasting the 2.16 smoke test protocol, we will noop this protocol and add a comment to explain the situation. + # to custom labware + # This labware does not EXIST!!!! so... + # Use tip rack lid to catch dye on wet run + pipette_right.pick_up_tip() + pipette_right.aspirate(volume=10, location=dye_source, rate=2.0) + pipette_right.dispense(volume=10, location=custom_labware.well(3), rate=1.5) + pipette_right.drop_tip() - pass + # to thermocycler + pipette_left.aspirate(volume=75, location=dye_source) + pipette_left.dispense(volume=60, location=tc_plate.wells_by_name()["A6"]) + pipette_left.drop_tip() From e353a06e02365a1431adccbf2c962e2f0e9a20c5 Mon Sep 17 00:00:00 2001 From: Nick Diehl <47604184+ncdiehl11@users.noreply.github.com> Date: Thu, 4 Apr 2024 13:21:25 -0400 Subject: [PATCH 045/194] feat(app, components): fix ProtocolDetails ParametersTable (#14803) --- .../localization/en/protocol_details.json | 1 + .../__tests__/ProtocolParameters.test.tsx | 2 +- .../__tests__/ParametersTable.test.tsx | 11 ++- .../src/molecules/ParametersTable/index.tsx | 73 ++++++++++++++++--- 4 files changed, 73 insertions(+), 14 deletions(-) diff --git a/app/src/assets/localization/en/protocol_details.json b/app/src/assets/localization/en/protocol_details.json index d00d7e5f9f9..fafd2c98038 100644 --- a/app/src/assets/localization/en/protocol_details.json +++ b/app/src/assets/localization/en/protocol_details.json @@ -39,6 +39,7 @@ "not_connected": "not connected", "not_in_protocol": "no {{section}} is specified for this protocol", "num_choices": "{{num}} choices", + "num_options": "{{num}} options", "off": "Off", "on_off": "On, off", "on": "On", diff --git a/app/src/organisms/ProtocolDetails/ProtocolParameters/__tests__/ProtocolParameters.test.tsx b/app/src/organisms/ProtocolDetails/ProtocolParameters/__tests__/ProtocolParameters.test.tsx index a752d19c8a4..173a03f0c7a 100644 --- a/app/src/organisms/ProtocolDetails/ProtocolParameters/__tests__/ProtocolParameters.test.tsx +++ b/app/src/organisms/ProtocolDetails/ProtocolParameters/__tests__/ProtocolParameters.test.tsx @@ -126,7 +126,7 @@ describe('ProtocolParameters', () => { screen.getByText('Default Module Offsets') screen.getByText('No offsets') - screen.getByText('3 choices') + screen.getByText('3 options') screen.getByText('pipette mount') screen.getByText('Left') diff --git a/components/src/molecules/ParametersTable/__tests__/ParametersTable.test.tsx b/components/src/molecules/ParametersTable/__tests__/ParametersTable.test.tsx index 6a4fe44bff0..aee232ebf8c 100644 --- a/components/src/molecules/ParametersTable/__tests__/ParametersTable.test.tsx +++ b/components/src/molecules/ParametersTable/__tests__/ParametersTable.test.tsx @@ -74,7 +74,7 @@ const render = (props: React.ComponentProps) => { return renderWithProviders() } -describe('ParametersTabl', () => { +describe('ParametersTable', () => { let props: React.ComponentProps beforeEach(() => { @@ -100,10 +100,12 @@ describe('ParametersTabl', () => { screen.getByText('6.5 mL') screen.getByText('1.5-10') + // more than 2 options screen.getByText('Default Module Offsets') screen.getByText('No offsets') - screen.getByText('3 choices') + screen.getByText('3 options') + // 2 options screen.getByText('pipette mount') screen.getByText('Left') screen.getByText('Left, Right') @@ -116,4 +118,9 @@ describe('ParametersTabl', () => { screen.getByText('default_value') screen.getByText('range') }) + + it('should render a description icon if description is provided', () => { + render(props) + screen.getByTestId('Icon_0') + }) }) diff --git a/components/src/molecules/ParametersTable/index.tsx b/components/src/molecules/ParametersTable/index.tsx index 671646f19d0..03731f0e32f 100644 --- a/components/src/molecules/ParametersTable/index.tsx +++ b/components/src/molecules/ParametersTable/index.tsx @@ -1,9 +1,13 @@ import * as React from 'react' import styled from 'styled-components' import { formatRunTimeParameterDefaultValue } from '@opentrons/shared-data' -import { BORDERS } from '../../helix-design-system' +import { BORDERS, COLORS } from '../../helix-design-system' import { SPACING, TYPOGRAPHY } from '../../ui-style-constants/index' import { StyledText } from '../../atoms/StyledText' +import { Tooltip, useHoverTooltip } from '../../tooltips' +import { Icon } from '../../icons' +import { Flex } from '../../primitives' +import { ALIGN_CENTER } from '../../styles' import type { RunTimeParameter } from '@opentrons/shared-data' @@ -28,20 +32,23 @@ export function ParametersTable({ 'choices' in runTimeParameter ? runTimeParameter.choices : [] const count = choices.length + if (count > 0) { + return count > 2 + ? t != null + ? t('num_options', { num: count }) + : `${count} options` + : choices.map(choice => choice.displayName).join(', ') + } + switch (type) { case 'int': case 'float': return minMax case 'bool': return t != null ? t('on_off') : 'On, off' - case 'str': - if (count > 2) { - return t != null ? t('choices', { count }) : `${count} choices` - } else { - return choices.map(choice => choice.displayName).join(', ') - } + default: + return '' } - return '' } return ( @@ -64,9 +71,12 @@ export function ParametersTable({ isLast={index === runTimeParameters.length - 1} key={`runTimeParameter-${index}`} > - - {parameter.displayName} - + {formatRunTimeParameterDefaultValue(parameter, t)} @@ -85,6 +95,46 @@ export function ParametersTable({ ) } +interface ParameterNameProps { + displayName: string + description: string | null + isLast: boolean + index: number +} + +const ParameterName = (props: ParameterNameProps): JSX.Element => { + const { displayName, description, isLast, index } = props + const [targetProps, tooltipProps] = useHoverTooltip() + + return ( + + + {displayName} + {description != null ? ( + <> + + + + + {description} + + + ) : null} + + + ) +} + const StyledTable = styled.table` width: 100%; border-collapse: collapse; @@ -111,6 +161,7 @@ interface StyledTableCellProps { } const StyledTableCell = styled.td` + width: 33%; padding-left: ${SPACING.spacing8}; padding-top: ${SPACING.spacing12}; padding-bottom: ${props => (props.isLast ? 0 : SPACING.spacing12)}; From 75e798d99f4f3bdf170e72140fc2d48b4065f777 Mon Sep 17 00:00:00 2001 From: koji Date: Thu, 4 Apr 2024 13:23:07 -0400 Subject: [PATCH 046/194] fix(app): modify software keyboard styling (#14804) * fix(app): modify software keyboard styling --- .../AlphanumericKeyboard/index.css | 3 ++- .../NumericalKeyboard.stories.tsx | 2 +- .../NumericalKeyboard/index.css | 4 +++- app/src/atoms/SoftwareKeyboard/index.css | 22 ++++++------------- 4 files changed, 13 insertions(+), 18 deletions(-) diff --git a/app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/index.css b/app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/index.css index 8816853e595..da0f9670b63 100644 --- a/app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/index.css +++ b/app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/index.css @@ -32,9 +32,10 @@ background-color: #dedede; /* grey30 */ } +/* ToDo (kk:04/04/2024) this important will be removed when I refactor the entire css */ .hg-layout-default .hg-row .hg-button, .hg-layout-shift .hg-row .hg-button { - height: 62.3px; + height: 62.3px !important; } /* first row and second row */ diff --git a/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/NumericalKeyboard.stories.tsx b/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/NumericalKeyboard.stories.tsx index 3bd55835b85..21b7c4c761b 100644 --- a/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/NumericalKeyboard.stories.tsx +++ b/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/NumericalKeyboard.stories.tsx @@ -57,7 +57,7 @@ const Template: Story< position={POSITION_ABSOLUTE} top="20%" width="22.5rem" - height="21.25rem" + height="max-content" > {showKeyboard && ( Date: Thu, 4 Apr 2024 13:29:29 -0400 Subject: [PATCH 047/194] feat(api): Do not enqueue json commands on protocol load (#14759) # Overview closes https://opentrons.atlassian.net/browse/EXEC-352. first step towards fixit commands. do not enqueue json protocol commands. # Test Plan Tested with Json protocols and Postman: - Make sure loading a protocol and executing it are happening within order. - Make sure get run `/commands` returning the list properly with successful commands. - Make sure loading a failed protocol should fail the run and fail the command. - Make sure get run `/commands` for failed runs the list of commands, last command being the failed command. - Fixed e2e test to comply with these changes # Changelog - Do no enqueue commands in PE for Json command upon load. - Execute commands one by one when run get started - same way we do for python protocols. # Review requests Changes make sense? GET run` /commands` will not return the full list of commands if the run did not start - its a change we are doing to make json protocols run like python protocols. are we ok with this? # Risk assessment Medium. need to do smoke tests for Json protocols and make sure these changes do not affect anything. --------- Co-authored-by: Max Marrone --- .../protocol_runner/protocol_runner.py | 18 +- .../protocol_runner/test_protocol_runner.py | 119 +++++++++- .../test_json_v6_protocol_run.tavern.yaml | 218 ++---------------- .../runs/test_json_v6_run_failure.tavern.yaml | 22 +- .../test_json_v7_protocol_run.tavern.yaml | 206 ++--------------- .../runs/test_play_stop_papi.tavern.yaml | 128 ++++++++++ .../runs/test_play_stop_v6.tavern.yaml | 128 ++++++++++ .../protocols/wait_for_resume_stop_papi.py | 13 ++ .../protocols/wait_for_resume_stop_v6.json | 37 +++ 9 files changed, 469 insertions(+), 420 deletions(-) create mode 100644 robot-server/tests/integration/http_api/runs/test_play_stop_papi.tavern.yaml create mode 100644 robot-server/tests/integration/http_api/runs/test_play_stop_v6.tavern.yaml create mode 100644 robot-server/tests/integration/protocols/wait_for_resume_stop_papi.py create mode 100644 robot-server/tests/integration/protocols/wait_for_resume_stop_v6.json diff --git a/api/src/opentrons/protocol_runner/protocol_runner.py b/api/src/opentrons/protocol_runner/protocol_runner.py index 67ea3d15db4..a1e88969615 100644 --- a/api/src/opentrons/protocol_runner/protocol_runner.py +++ b/api/src/opentrons/protocol_runner/protocol_runner.py @@ -36,6 +36,7 @@ LegacyExecutor, LegacyLoadInfo, ) +from ..protocol_engine.errors import ProtocolCommandFailedError from ..protocol_engine.types import ( PostRunHardwareState, DeckConfigurationType, @@ -283,6 +284,7 @@ def __init__( ) self._hardware_api.should_taskify_movement_execution(taskify=False) + self._queued_commands: List[pe_commands.CommandCreate] = [] async def load(self, protocol_source: ProtocolSource) -> None: """Load a JSONv6+ ProtocolSource into managed ProtocolEngine.""" @@ -324,17 +326,16 @@ async def load(self, protocol_source: ProtocolSource) -> None: color=liquid.displayColor, ) await _yield() + initial_home_command = pe_commands.HomeCreate( params=pe_commands.HomeParams(axes=None) ) # this command homes all axes, including pipette plugner and gripper jaw self._protocol_engine.add_command(request=initial_home_command) - for command in commands: - self._protocol_engine.add_command(request=command) - await _yield() + self._queued_commands = commands - self._task_queue.set_run_func(func=self._protocol_engine.wait_until_complete) + self._task_queue.set_run_func(func=self._add_command_and_execute) async def run( # noqa: D102 self, @@ -355,6 +356,15 @@ async def run( # noqa: D102 commands = self._protocol_engine.state_view.commands.get_all() return RunResult(commands=commands, state_summary=run_data, parameters=[]) + async def _add_command_and_execute(self) -> None: + for command in self._queued_commands: + result = await self._protocol_engine.add_and_execute_command(command) + if result and result.error: + raise ProtocolCommandFailedError( + original_error=result.error, + message=f"{result.error.errorType}: {result.error.detail}", + ) + class LiveRunner(AbstractRunner): """Protocol runner implementation for live http protocols.""" diff --git a/api/tests/opentrons/protocol_runner/test_protocol_runner.py b/api/tests/opentrons/protocol_runner/test_protocol_runner.py index 4f3ca342359..5497e9e12ab 100644 --- a/api/tests/opentrons/protocol_runner/test_protocol_runner.py +++ b/api/tests/opentrons/protocol_runner/test_protocol_runner.py @@ -1,4 +1,6 @@ """Tests for the PythonAndLegacyRunner, JsonRunner & LiveRunner classes.""" +from datetime import datetime + import pytest from pytest_lazyfixture import lazy_fixture # type: ignore[import-untyped] from decoy import Decoy, matchers @@ -18,7 +20,12 @@ from opentrons.util.broker import Broker from opentrons import protocol_reader -from opentrons.protocol_engine import ProtocolEngine, Liquid, commands as pe_commands +from opentrons.protocol_engine import ( + ProtocolEngine, + Liquid, + commands as pe_commands, + errors as pe_errors, +) from opentrons.protocol_reader import ( ProtocolSource, JsonProtocolConfig, @@ -328,6 +335,96 @@ async def test_run_json_runner( ) +async def test_run_json_runner_stop_requested_stops_enquqing( + decoy: Decoy, + hardware_api: HardwareAPI, + protocol_engine: ProtocolEngine, + task_queue: TaskQueue, + json_runner_subject: JsonRunner, + json_file_reader: JsonFileReader, + json_translator: JsonTranslator, +) -> None: + """It should run a protocol to completion.""" + labware_definition = LabwareDefinition.construct() # type: ignore[call-arg] + json_protocol_source = ProtocolSource( + directory=Path("/dev/null"), + main_file=Path("/dev/null/abc.json"), + files=[], + metadata={}, + robot_type="OT-2 Standard", + config=JsonProtocolConfig(schema_version=6), + content_hash="abc123", + ) + + commands: List[pe_commands.CommandCreate] = [ + pe_commands.HomeCreate(params=pe_commands.HomeParams()), + pe_commands.WaitForDurationCreate( + params=pe_commands.WaitForDurationParams(seconds=10) + ), + pe_commands.LoadLiquidCreate( + params=pe_commands.LoadLiquidParams( + liquidId="water-id", labwareId="labware-id", volumeByWell={"A1": 30} + ) + ), + ] + + liquids: List[Liquid] = [ + Liquid(id="water-id", displayName="water", description="water desc") + ] + + json_protocol = ProtocolSchemaV6.construct() # type: ignore[call-arg] + + decoy.when( + await protocol_reader.extract_labware_definitions(json_protocol_source) + ).then_return([labware_definition]) + decoy.when(json_file_reader.read(json_protocol_source)).then_return(json_protocol) + decoy.when(json_translator.translate_commands(json_protocol)).then_return(commands) + decoy.when(json_translator.translate_liquids(json_protocol)).then_return(liquids) + decoy.when( + await protocol_engine.add_and_execute_command( + pe_commands.HomeCreate(params=pe_commands.HomeParams()), + ) + ).then_return( + pe_commands.Home.construct(status=pe_commands.CommandStatus.SUCCEEDED) # type: ignore[call-arg] + ) + decoy.when( + await protocol_engine.add_and_execute_command( + pe_commands.WaitForDurationCreate( + params=pe_commands.WaitForDurationParams(seconds=10) + ), + ) + ).then_return( + pe_commands.WaitForDuration.construct( # type: ignore[call-arg] + error=pe_errors.ErrorOccurrence.from_failed( + id="some-id", + createdAt=datetime(year=2021, month=1, day=1), + error=pe_errors.ProtocolEngineError(), + ) + ) + ) + + await json_runner_subject.load(json_protocol_source) + + run_func_captor = matchers.Captor() + + decoy.verify( + protocol_engine.add_labware_definition(labware_definition), + protocol_engine.add_liquid( + id="water-id", name="water", description="water desc", color=None + ), + protocol_engine.add_command( + request=pe_commands.HomeCreate(params=pe_commands.HomeParams(axes=None)) + ), + task_queue.set_run_func(func=run_func_captor), + ) + + # Verify that the run func calls the right things: + run_func = run_func_captor.value + + with pytest.raises(pe_errors.ProtocolEngineError): + await run_func() + + @pytest.mark.parametrize( "schema_version, json_protocol", [ @@ -385,6 +482,8 @@ async def test_load_json_runner( await json_runner_subject.load(json_protocol_source) + run_func_captor = matchers.Captor() + decoy.verify( protocol_engine.add_labware_definition(labware_definition), protocol_engine.add_liquid( @@ -393,24 +492,30 @@ async def test_load_json_runner( protocol_engine.add_command( request=pe_commands.HomeCreate(params=pe_commands.HomeParams(axes=None)) ), - protocol_engine.add_command( + task_queue.set_run_func(func=run_func_captor), + ) + + # Verify that the run func calls the right things: + run_func = run_func_captor.value + await run_func() + decoy.verify( + await protocol_engine.add_and_execute_command( request=pe_commands.WaitForResumeCreate( params=pe_commands.WaitForResumeParams(message="hello") - ) + ), ), - protocol_engine.add_command( + await protocol_engine.add_and_execute_command( request=pe_commands.WaitForResumeCreate( params=pe_commands.WaitForResumeParams(message="goodbye") - ) + ), ), - protocol_engine.add_command( + await protocol_engine.add_and_execute_command( request=pe_commands.LoadLiquidCreate( params=pe_commands.LoadLiquidParams( liquidId="water-id", labwareId="labware-id", volumeByWell={"A1": 30} ) ), ), - task_queue.set_run_func(func=protocol_engine.wait_until_complete), ) diff --git a/robot-server/tests/integration/http_api/runs/test_json_v6_protocol_run.tavern.yaml b/robot-server/tests/integration/http_api/runs/test_json_v6_protocol_run.tavern.yaml index 1e7d7e20be4..4ff631bf277 100644 --- a/robot-server/tests/integration/http_api/runs/test_json_v6_protocol_run.tavern.yaml +++ b/robot-server/tests/integration/http_api/runs/test_json_v6_protocol_run.tavern.yaml @@ -93,10 +93,10 @@ stages: commandId: '{setup_command_id}' key: '{setup_command_key}' createdAt: '{setup_command_created_at}' - index: 14 + index: 1 meta: cursor: 0 - totalLength: 15 + totalLength: 2 data: # Initial home - id: !anystr @@ -105,184 +105,6 @@ stages: createdAt: !anystr status: queued params: {} - - id: !anystr - key: !anystr - commandType: loadPipette - createdAt: !anystr - status: queued - params: - pipetteName: p10_single - mount: left - pipetteId: pipetteId - - id: !anystr - key: !anystr - commandType: loadModule - createdAt: !anystr - status: queued - params: - model: magneticModuleV1 - location: - slotName: '3' - moduleId: magneticModuleId - - id: !anystr - key: !anystr - commandType: loadModule - createdAt: !anystr - status: queued - params: - model: temperatureModuleV2 - location: - slotName: '1' - moduleId: temperatureModuleId - - id: !anystr - key: !anystr - commandType: loadLabware - createdAt: !anystr - status: queued - params: - location: - moduleId: temperatureModuleId - loadName: foo_8_plate_33ul - namespace: example - version: 1 - labwareId: sourcePlateId - displayName: Source Plate - - id: !anystr - key: !anystr - commandType: loadLabware - createdAt: !anystr - status: queued - params: - location: - moduleId: magneticModuleId - loadName: foo_8_plate_33ul - namespace: example - version: 1 - labwareId: destPlateId - displayName: Sample Collection Plate - - id: !anystr - key: !anystr - commandType: loadLabware - createdAt: !anystr - status: queued - params: - location: - slotName: '8' - loadName: opentrons_96_tiprack_10ul - namespace: opentrons - version: 1 - labwareId: tipRackId - displayName: Opentrons 96 Tip Rack 10 µL - - id: !anystr - createdAt: !anystr - commandType: loadLiquid - key: !anystr - status: queued - params: - liquidId: 'waterId' - labwareId: 'sourcePlateId' - volumeByWell: - A1: 100 - B1: 100 - - id: !anystr - key: !anystr - commandType: pickUpTip - createdAt: !anystr - status: queued - params: - pipetteId: pipetteId - labwareId: tipRackId - wellName: B1 - wellLocation: - origin: top - offset: - x: 0 - 'y': 0 - z: 0 - - id: !anystr - key: !anystr - commandType: aspirate - createdAt: !anystr - status: queued - params: - pipetteId: pipetteId - labwareId: sourcePlateId - wellName: A1 - wellLocation: - origin: bottom - offset: - x: 0 - 'y': 0 - z: 2 - volume: 5 - flowRate: 3 - - id: !anystr - key: !anystr - commandType: dispense - createdAt: !anystr - status: queued - params: - pipetteId: pipetteId - labwareId: destPlateId - wellName: B1 - wellLocation: - origin: bottom - offset: - x: 0 - 'y': 0 - z: 1 - volume: 4.5 - flowRate: 2.5 - - id: !anystr - key: !anystr - commandType: moveToWell - createdAt: !anystr - status: queued - params: - pipetteId: pipetteId - labwareId: destPlateId - wellName: B2 - wellLocation: - origin: top - offset: - x: 0 - 'y': 0 - z: 0 - forceDirect: false - - id: !anystr - key: !anystr - commandType: moveToWell - createdAt: !anystr - status: queued - params: - pipetteId: pipetteId - labwareId: destPlateId - wellName: B2 - wellLocation: - origin: bottom - offset: - x: 2 - y: 3 - z: 10 - minimumZHeight: 35 - forceDirect: true - speed: 12.3 - - id: !anystr - key: !anystr - commandType: dropTip - createdAt: !anystr - status: queued - params: - pipetteId: pipetteId - labwareId: fixedTrash - wellName: A1 - wellLocation: - origin: default - offset: - x: 0 - y: 0 - z: 0 - alternateDropLocation: false - id: '{setup_command_id}' key: '{setup_command_key}' intent: setup @@ -352,6 +174,16 @@ stages: params: {} startedAt: !anystr completedAt: !anystr + - id: '{setup_command_id}' + key: '{setup_command_key}' + intent: setup + commandType: home + createdAt: '{setup_command_created_at}' + startedAt: '{setup_command_started_at}' + completedAt: '{setup_command_completed_at}' + status: succeeded + params: { } + notes: [] - id: !anystr key: !anystr commandType: loadPipette @@ -569,16 +401,6 @@ stages: y: 0 z: 0 alternateDropLocation: false - - id: '{setup_command_id}' - key: '{setup_command_key}' - intent: setup - commandType: home - createdAt: '{setup_command_created_at}' - startedAt: '{setup_command_started_at}' - completedAt: '{setup_command_completed_at}' - status: succeeded - notes: [] - params: {} - name: Verify commands succeeded with pageLength and cursor request: @@ -610,12 +432,12 @@ stages: notes: [] params: location: - moduleId: magneticModuleId + moduleId: temperatureModuleId loadName: foo_8_plate_33ul namespace: example version: 1 - labwareId: destPlateId - displayName: Sample Collection Plate + labwareId: sourcePlateId + displayName: Source Plate - id: !anystr key: !anystr commandType: loadLabware @@ -626,9 +448,9 @@ stages: notes: [] params: location: - slotName: '8' - loadName: opentrons_96_tiprack_10ul - namespace: opentrons + moduleId: magneticModuleId + loadName: foo_8_plate_33ul + namespace: example version: 1 - labwareId: tipRackId - displayName: Opentrons 96 Tip Rack 10 µL + labwareId: destPlateId + displayName: Sample Collection Plate diff --git a/robot-server/tests/integration/http_api/runs/test_json_v6_run_failure.tavern.yaml b/robot-server/tests/integration/http_api/runs/test_json_v6_run_failure.tavern.yaml index 46eccbae280..80c7f1b2ef5 100644 --- a/robot-server/tests/integration/http_api/runs/test_json_v6_run_failure.tavern.yaml +++ b/robot-server/tests/integration/http_api/runs/test_json_v6_run_failure.tavern.yaml @@ -86,12 +86,12 @@ stages: meta: runId: !anystr commandId: !anystr - index: 4 + index: 3 key: !anystr createdAt: !anystr meta: cursor: 3 - totalLength: 5 + totalLength: 4 data: - id: !anystr key: !anystr @@ -120,20 +120,4 @@ stages: y: 0 z: 1 flowRate: 3.78 - volume: 100 - - id: !anystr - key: !anystr - commandType: pickUpTip - createdAt: !anystr - completedAt: !anystr - status: failed - params: - pipetteId: pipetteId - labwareId: tipRackId - wellName: A1 - wellLocation: - origin: top - offset: - x: 0 - y: 0 - z: 0 + volume: 100 \ No newline at end of file diff --git a/robot-server/tests/integration/http_api/runs/test_json_v7_protocol_run.tavern.yaml b/robot-server/tests/integration/http_api/runs/test_json_v7_protocol_run.tavern.yaml index 089b5f30c03..317d339fbbf 100644 --- a/robot-server/tests/integration/http_api/runs/test_json_v7_protocol_run.tavern.yaml +++ b/robot-server/tests/integration/http_api/runs/test_json_v7_protocol_run.tavern.yaml @@ -93,10 +93,10 @@ stages: commandId: '{setup_command_id}' key: '{setup_command_key}' createdAt: '{setup_command_created_at}' - index: 14 + index: 1 meta: cursor: 0 - totalLength: 15 + totalLength: 2 data: # Initial home - id: !anystr @@ -104,185 +104,7 @@ stages: commandType: home createdAt: !anystr status: queued - params: {} - - id: !anystr - key: !anystr - commandType: loadPipette - createdAt: !anystr - status: queued - params: - pipetteName: p10_single - mount: left - pipetteId: pipetteId - - id: !anystr - key: !anystr - commandType: loadModule - createdAt: !anystr - status: queued - params: - model: magneticModuleV1 - location: - slotName: '3' - moduleId: magneticModuleId - - id: !anystr - key: !anystr - commandType: loadModule - createdAt: !anystr - status: queued - params: - model: temperatureModuleV2 - location: - slotName: '1' - moduleId: temperatureModuleId - - id: !anystr - key: !anystr - commandType: loadLabware - createdAt: !anystr - status: queued - params: - location: - moduleId: temperatureModuleId - loadName: foo_8_plate_33ul - namespace: example - version: 1 - labwareId: sourcePlateId - displayName: Source Plate - - id: !anystr - key: !anystr - commandType: loadLabware - createdAt: !anystr - status: queued - params: - location: - moduleId: magneticModuleId - loadName: foo_8_plate_33ul - namespace: example - version: 1 - labwareId: destPlateId - displayName: Sample Collection Plate - - id: !anystr - key: !anystr - commandType: loadLabware - createdAt: !anystr - status: queued - params: - location: - slotName: '8' - loadName: opentrons_96_tiprack_10ul - namespace: opentrons - version: 1 - labwareId: tipRackId - displayName: Opentrons 96 Tip Rack 10 µL - - id: !anystr - createdAt: !anystr - commandType: loadLiquid - key: !anystr - status: queued - params: - liquidId: 'waterId' - labwareId: 'sourcePlateId' - volumeByWell: - A1: 100 - B1: 100 - - id: !anystr - key: !anystr - commandType: pickUpTip - createdAt: !anystr - status: queued - params: - pipetteId: pipetteId - labwareId: tipRackId - wellName: B1 - wellLocation: - origin: top - offset: - x: 0 - 'y': 0 - z: 0 - - id: !anystr - key: !anystr - commandType: aspirate - createdAt: !anystr - status: queued - params: - pipetteId: pipetteId - labwareId: sourcePlateId - wellName: A1 - wellLocation: - origin: bottom - offset: - x: 0 - 'y': 0 - z: 2 - volume: 5 - flowRate: 3 - - id: !anystr - key: !anystr - commandType: dispense - createdAt: !anystr - status: queued - params: - pipetteId: pipetteId - labwareId: destPlateId - wellName: B1 - wellLocation: - origin: bottom - offset: - x: 0 - 'y': 0 - z: 1 - volume: 4.5 - flowRate: 2.5 - - id: !anystr - key: !anystr - commandType: moveToWell - createdAt: !anystr - status: queued - params: - pipetteId: pipetteId - labwareId: destPlateId - wellName: B2 - wellLocation: - origin: top - offset: - x: 0 - 'y': 0 - z: 0 - forceDirect: false - - id: !anystr - key: !anystr - commandType: moveToWell - createdAt: !anystr - status: queued - params: - pipetteId: pipetteId - labwareId: destPlateId - wellName: B2 - wellLocation: - origin: bottom - offset: - x: 2 - y: 3 - z: 10 - minimumZHeight: 35 - forceDirect: true - speed: 12.3 - - id: !anystr - key: !anystr - commandType: dropTip - createdAt: !anystr - status: queued - params: - pipetteId: pipetteId - labwareId: fixedTrash - wellName: A1 - wellLocation: - origin: default - offset: - x: 0 - y: 0 - z: 0 - alternateDropLocation: false + params: { } - id: '{setup_command_id}' key: '{setup_command_key}' intent: setup @@ -350,8 +172,18 @@ stages: startedAt: !anystr completedAt: !anystr status: succeeded + params: { } + notes: [ ] + - id: '{setup_command_id}' + key: '{setup_command_key}' + intent: setup + commandType: home + createdAt: '{setup_command_created_at}' + startedAt: '{setup_command_started_at}' + completedAt: '{setup_command_completed_at}' + status: succeeded + params: { } notes: [] - params: {} - id: !anystr key: !anystr commandType: loadPipette @@ -569,13 +401,3 @@ stages: y: 0 z: 0 alternateDropLocation: false - - id: '{setup_command_id}' - key: '{setup_command_key}' - intent: setup - commandType: home - createdAt: '{setup_command_created_at}' - startedAt: '{setup_command_started_at}' - completedAt: '{setup_command_completed_at}' - status: succeeded - notes: [] - params: {} diff --git a/robot-server/tests/integration/http_api/runs/test_play_stop_papi.tavern.yaml b/robot-server/tests/integration/http_api/runs/test_play_stop_papi.tavern.yaml new file mode 100644 index 00000000000..d59b533ca67 --- /dev/null +++ b/robot-server/tests/integration/http_api/runs/test_play_stop_papi.tavern.yaml @@ -0,0 +1,128 @@ +test_name: Test python protocol run commands are failed when stopped. + +marks: + - usefixtures: + - ot2_server_base_url +stages: + - name: Upload a python protocol + request: + url: '{ot2_server_base_url}/protocols' + method: POST + files: + files: 'tests/integration/protocols/wait_for_resume_stop_papi.py' + response: + status_code: 201 + save: + json: + protocol_id: data.id + + - name: Create run from protocol + request: + url: '{ot2_server_base_url}/runs' + method: POST + json: + data: + protocolId: '{protocol_id}' + response: + status_code: 201 + save: + json: + run_id: data.id + + - name: Play the run + request: + url: '{ot2_server_base_url}/runs/{run_id}/actions' + method: POST + json: + data: + actionType: play + response: + status_code: 201 + + - name: Wait for the command to run + max_retries: 10 + delay_after: 0.2 + request: + url: '{ot2_server_base_url}/runs/{run_id}/commands' + method: GET + response: + status_code: 200 + strict: + - json:off + json: + data: + - commandType: waitForDuration + status: running + + - name: Stop the run + request: + url: '{ot2_server_base_url}/runs/{run_id}/actions' + method: POST + json: + data: + actionType: stop + response: + status_code: 201 + + - name: Wait for the run to complete + max_retries: 10 + delay_after: 0.2 + request: + url: '{ot2_server_base_url}/runs/{run_id}' + method: GET + response: + status_code: 200 + strict: + - json:off + json: + data: + status: stopped + + - name: Get run commands + request: + url: '{ot2_server_base_url}/runs/{run_id}/commands' + method: GET + response: + status_code: 200 + strict: + - json:off + json: + data: + - id: !anystr + key: !anystr + commandType: home + createdAt: !anystr + startedAt: !anystr + completedAt: !anystr + status: succeeded + params: {} + notes: [] + - id: !anystr + key: !anystr + commandType: home + createdAt: !anystr + startedAt: !anystr + completedAt: !anystr + status: succeeded + params: { } + notes: [ ] + - id: !anystr + key: !anystr + commandType: waitForDuration + createdAt: !anystr + startedAt: !anystr + completedAt: !anystr + status: failed + params: + seconds: 30 + notes: [ ] + error: + createdAt: !anystr + detail: 'Run was cancelled' + errorCode: '4000' + errorInfo: { } + errorType: 'RunStoppedError' + id: !anystr + wrappedErrors: [ ] + + diff --git a/robot-server/tests/integration/http_api/runs/test_play_stop_v6.tavern.yaml b/robot-server/tests/integration/http_api/runs/test_play_stop_v6.tavern.yaml new file mode 100644 index 00000000000..e3d6d5b659f --- /dev/null +++ b/robot-server/tests/integration/http_api/runs/test_play_stop_v6.tavern.yaml @@ -0,0 +1,128 @@ +test_name: Test a JSONv6 run can be paused and then cancelled. + +marks: + - usefixtures: + - ot2_server_base_url +stages: + - name: Upload a JSONv6 protocol + request: + url: '{ot2_server_base_url}/protocols' + method: POST + files: + files: 'tests/integration/protocols/wait_for_resume_stop_v6.json' + response: + status_code: 201 + save: + json: + protocol_id: data.id + + - name: Create run from protocol + request: + url: '{ot2_server_base_url}/runs' + method: POST + json: + data: + protocolId: '{protocol_id}' + response: + status_code: 201 + save: + json: + run_id: data.id + + - name: Play the run + request: + url: '{ot2_server_base_url}/runs/{run_id}/actions' + method: POST + json: + data: + actionType: play + response: + status_code: 201 + + - name: Wait for the command to run + max_retries: 10 + delay_after: 0.2 + request: + url: '{ot2_server_base_url}/runs/{run_id}/commands' + method: GET + response: + status_code: 200 + strict: + - json:off + json: + data: + - commandType: waitForDuration + status: running + + - name: Stop the run + request: + url: '{ot2_server_base_url}/runs/{run_id}/actions' + method: POST + json: + data: + actionType: stop + response: + status_code: 201 + + - name: Wait for the run to complete + max_retries: 10 + delay_after: 0.2 + request: + url: '{ot2_server_base_url}/runs/{run_id}' + method: GET + response: + status_code: 200 + strict: + - json:off + json: + data: + status: stopped + + - name: Get run commands + request: + url: '{ot2_server_base_url}/runs/{run_id}/commands' + method: GET + response: + status_code: 200 + strict: + - json:off + json: + data: + - id: !anystr + key: !anystr + commandType: home + createdAt: !anystr + startedAt: !anystr + completedAt: !anystr + status: succeeded + params: {} + notes: [] + - id: !anystr + key: !anystr + commandType: home + createdAt: !anystr + startedAt: !anystr + completedAt: !anystr + status: succeeded + params: { } + notes: [ ] + - id: !anystr + key: !anystr + commandType: waitForDuration + createdAt: !anystr + startedAt: !anystr + completedAt: !anystr + status: failed + params: + seconds: 30 + notes: [ ] + error: + createdAt: !anystr + detail: 'Run was cancelled' + errorCode: '4000' + errorInfo: { } + errorType: 'RunStoppedError' + id: !anystr + wrappedErrors: [ ] + + diff --git a/robot-server/tests/integration/protocols/wait_for_resume_stop_papi.py b/robot-server/tests/integration/protocols/wait_for_resume_stop_papi.py new file mode 100644 index 00000000000..227d65cd00b --- /dev/null +++ b/robot-server/tests/integration/protocols/wait_for_resume_stop_papi.py @@ -0,0 +1,13 @@ +from opentrons.protocol_api import ProtocolContext + +metadata = { + "protocolName": "stop while waiting test", + "author": "Opentrons ", + "apiLevel": "2.15", +} + + +def run(ctx: ProtocolContext) -> None: + ctx.home() + ctx.delay(seconds=30) + ctx.set_rail_lights(on=True) diff --git a/robot-server/tests/integration/protocols/wait_for_resume_stop_v6.json b/robot-server/tests/integration/protocols/wait_for_resume_stop_v6.json new file mode 100644 index 00000000000..05101595ee7 --- /dev/null +++ b/robot-server/tests/integration/protocols/wait_for_resume_stop_v6.json @@ -0,0 +1,37 @@ +{ + "$otSharedSchema": "#/protocol/schemas/6", + "schemaVersion": 6, + "metadata": { + "protocolName": "Simple test protocol", + "author": "engineering ", + "description": "A short test protocol", + "created": 1223131231, + "tags": ["unitTest"] + }, + "robot": { + "model": "OT-2 Standard", + "deckId": "ot2_standard" + }, + "pipettes": {}, + "modules": {}, + "labware": {}, + "liquids": {}, + "labwareDefinitions": {}, + "commands": [ + { + "commandType": "home", + "params": {} + }, + { + "commandType": "waitForDuration", + "params": { + "seconds": 30 + } + }, + { + "commandType": "home", + "params": {} + } + ], + "commandAnnotations": [] +} From e4797d208b3c6ca262bc5b42f8d261b95944946b Mon Sep 17 00:00:00 2001 From: Jethary Rader <66035149+jerader@users.noreply.github.com> Date: Thu, 4 Apr 2024 14:36:33 -0400 Subject: [PATCH 048/194] feat(protocol-designer): add alert if x/y position is too close to edge (#14802) closes AUTH-252 --- .../TipPositionInput.module.css | 2 +- .../TipPositionField/TipPositionModal.tsx | 47 +++++++++++++++---- .../__tests__/TipPositionModal.test.tsx | 24 ++++++++-- .../fields/TipPositionField/constants.ts | 1 + .../src/localization/en/modal.json | 1 + 5 files changed, 60 insertions(+), 15 deletions(-) diff --git a/protocol-designer/src/components/StepEditForm/fields/TipPositionField/TipPositionInput.module.css b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/TipPositionInput.module.css index d7e6344e1ea..ef185908342 100644 --- a/protocol-designer/src/components/StepEditForm/fields/TipPositionField/TipPositionInput.module.css +++ b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/TipPositionInput.module.css @@ -4,7 +4,7 @@ display: flex; flex-direction: column; justify-content: space-evenly; - height: 5rem; + height: 4rem; } .main_row { diff --git a/protocol-designer/src/components/StepEditForm/fields/TipPositionField/TipPositionModal.tsx b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/TipPositionModal.tsx index 0d79a39ae9a..2a303f92c2f 100644 --- a/protocol-designer/src/components/StepEditForm/fields/TipPositionField/TipPositionModal.tsx +++ b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/TipPositionModal.tsx @@ -12,13 +12,14 @@ import { StyledText, } from '@opentrons/components' import { getMainPagePortalEl } from '../../../portals/MainPageModalPortal' -import modalStyles from '../../../modals/modal.module.css' import { getIsTouchTipField } from '../../../../form-types' -import { TOO_MANY_DECIMALS } from './constants' +import { PDAlert } from '../../../alerts/PDAlert' +import { TOO_MANY_DECIMALS, PERCENT_RANGE_TO_SHOW_WARNING } from './constants' import { TipPositionAllViz } from './TipPositionAllViz' +import * as utils from './utils' import styles from './TipPositionInput.module.css' -import * as utils from './utils' +import modalStyles from '../../../modals/modal.module.css' import type { StepFieldName } from '../../../../form-types' @@ -57,7 +58,9 @@ export const TipPositionModal = ( const { t } = useTranslation(['modal', 'button']) if (zSpec == null || xSpec == null || ySpec == null) { - console.error('expected to find specs for the zPosition but could not') + console.error( + 'expected to find specs for one of the positions but could not' + ) } const defaultMmFromBottom = utils.getDefaultMmFromBottom({ @@ -135,9 +138,14 @@ export const TipPositionModal = ( return utils.getErrorText({ errors, minMm: min, maxMm: max, isPristine, t }) } + const roundedXMin = utils.roundValue(xMinWidth) + const roundedYMin = utils.roundValue(yMinWidth) + const roundedXMax = utils.roundValue(xMaxWidth) + const roundedYMax = utils.roundValue(yMaxWidth) + const zErrorText = createErrorText(zErrors, minMmFromBottom, maxMmFromBottom) - const xErrorText = createErrorText(xErrors, xMinWidth, xMaxWidth) - const yErrorText = createErrorText(yErrors, yMinWidth, yMaxWidth) + const xErrorText = createErrorText(xErrors, roundedXMin, roundedXMax) + const yErrorText = createErrorText(yErrors, roundedYMin, roundedYMax) const handleDone = (): void => { setPristine(false) @@ -218,6 +226,14 @@ export const TipPositionModal = ( ): void => { handleYChange(e.currentTarget.value) } + const isXValueNearEdge = + xValue != null && + (parseInt(xValue) > PERCENT_RANGE_TO_SHOW_WARNING * xMaxWidth || + parseInt(xValue) < PERCENT_RANGE_TO_SHOW_WARNING * xMinWidth) + const isYValueNearEdge = + yValue != null && + (parseInt(yValue) > PERCENT_RANGE_TO_SHOW_WARNING * yMaxWidth || + parseInt(yValue) < PERCENT_RANGE_TO_SHOW_WARNING * yMinWidth) const TipPositionInputField = !isDefault ? ( @@ -227,8 +243,8 @@ export const TipPositionModal = ( {t('tip_position.title')}

{t(`tip_position.body.${zSpec?.name}`)}

+ + {(isXValueNearEdge || isYValueNearEdge) && !isDefault ? ( + + + + ) : null} +
diff --git a/protocol-designer/src/components/StepEditForm/fields/TipPositionField/__tests__/TipPositionModal.test.tsx b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/__tests__/TipPositionModal.test.tsx index 5fccf40a480..6054bd2eb2d 100644 --- a/protocol-designer/src/components/StepEditForm/fields/TipPositionField/__tests__/TipPositionModal.test.tsx +++ b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/__tests__/TipPositionModal.test.tsx @@ -61,14 +61,30 @@ describe('TipPositionModal', () => { expect(mockUpdateYSpec).toHaveBeenCalled() expect(mockUpdateZSpec).toHaveBeenCalled() }) + it('renders the alert if the x/y position values are too close to the max/min for x value', () => { + props.specs.x.value = 9.7 + render(props) + screen.getByText('warning') + screen.getByText( + 'The X and/or Y position value is close to edge of the well and might collide with it' + ) + }) + it('renders the alert if the x/y position values are too close to the max/min for y value', () => { + props.specs.y.value = -9.7 + render(props) + screen.getByText('warning') + screen.getByText( + 'The X and/or Y position value is close to edge of the well and might collide with it' + ) + }) it('renders the custom options, captions, and visual', () => { render(props) fireEvent.click(screen.getByRole('radio', { name: 'Custom' })) expect(screen.getAllByRole('textbox', { name: '' })).toHaveLength(3) screen.getByText('X position') - screen.getByText('between -5.15 and 5.15') + screen.getByText('between -5.1 and 5.2') screen.getByText('Y position') - screen.getByText('between -5.25 and 5.25') + screen.getByText('between -5.2 and 5.3') screen.getByText('Z position') screen.getByText('between 0 and 100') screen.getByText('mock TipPositionViz') @@ -113,8 +129,8 @@ describe('TipPositionModal', () => { fireEvent.click(screen.getByText('done')) // display out of bounds error screen.getByText('accepted range is 0 to 100') - screen.getByText('accepted range is -5.25 to 5.25') - screen.getByText('accepted range is -5.15 to 5.15') + screen.getByText('accepted range is -5.2 to 5.3') + screen.getByText('accepted range is -5.1 to 5.2') const xInputField = screen.getAllByRole('textbox', { name: '' })[0] fireEvent.change(xInputField, { target: { value: 3.55555 } }) fireEvent.click(screen.getByText('done')) diff --git a/protocol-designer/src/components/StepEditForm/fields/TipPositionField/constants.ts b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/constants.ts index c790cb449cc..528d9a0262e 100644 --- a/protocol-designer/src/components/StepEditForm/fields/TipPositionField/constants.ts +++ b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/constants.ts @@ -2,3 +2,4 @@ export const DECIMALS_ALLOWED = 1 export const SMALL_STEP_MM = 1 export const LARGE_STEP_MM = 10 export const TOO_MANY_DECIMALS: 'TOO_MANY_DECIMALS' = 'TOO_MANY_DECIMALS' +export const PERCENT_RANGE_TO_SHOW_WARNING = 0.9 diff --git a/protocol-designer/src/localization/en/modal.json b/protocol-designer/src/localization/en/modal.json index 8fb81091dc4..a07cb3b1310 100644 --- a/protocol-designer/src/localization/en/modal.json +++ b/protocol-designer/src/localization/en/modal.json @@ -62,6 +62,7 @@ "tip_position": { "title": "Tip Positioning", "caption": "between {{min}} and {{max}}", + "warning": "The X and/or Y position value is close to edge of the well and might collide with it", "radio_button": { "default": "{{defaultMmFromBottom}} mm from the bottom center (default)", "blowout": "0 mm from the top center (default)", From af5eb1fc18599fe07d6ce75a5beca01034e7d5bd Mon Sep 17 00:00:00 2001 From: koji Date: Fri, 5 Apr 2024 10:20:23 -0400 Subject: [PATCH 049/194] refactor(app): update storybook of small button (#14812) * refactor(app): update storybook of small button --- app/src/atoms/buttons/SmallButton.stories.tsx | 93 ++++++++++--------- 1 file changed, 50 insertions(+), 43 deletions(-) diff --git a/app/src/atoms/buttons/SmallButton.stories.tsx b/app/src/atoms/buttons/SmallButton.stories.tsx index f587f7f4e13..6566847a62c 100644 --- a/app/src/atoms/buttons/SmallButton.stories.tsx +++ b/app/src/atoms/buttons/SmallButton.stories.tsx @@ -1,68 +1,75 @@ -import * as React from 'react' import { VIEWPORT } from '@opentrons/components' import { SmallButton } from './' -import type { Story, Meta } from '@storybook/react' +import type { Meta, StoryObj } from '@storybook/react' -export default { +const meta: Meta = { title: 'ODD/Atoms/Buttons/SmallButton', argTypes: { onClick: { action: 'clicked' } }, component: SmallButton, parameters: VIEWPORT.touchScreenViewport, -} as Meta +} + +export default meta -const Template: Story> = args => ( - -) +type Story = StoryObj -export const Primary = Template.bind({}) -Primary.args = { - buttonText: 'Button text', +export const Primary: Story = { + args: { + buttonText: 'Button text', + }, } -export const Alert = Template.bind({}) -Alert.args = { - buttonType: 'alert', - buttonText: 'Button text', +export const Alert: Story = { + args: { + buttonType: 'alert', + buttonText: 'Button text', + }, } -export const Secondary = Template.bind({}) -Secondary.args = { - buttonType: 'secondary', - buttonText: 'Button text', +export const Secondary: Story = { + args: { + buttonType: 'secondary', + buttonText: 'Button text', + }, } -export const TertiaryLowLight = Template.bind({}) -TertiaryLowLight.args = { - buttonType: 'tertiaryLowLight', - buttonText: 'Button text', +export const TertiaryLowLight: Story = { + args: { + buttonType: 'tertiaryLowLight', + buttonText: 'Button text', + }, } -export const TertiaryHighLight = Template.bind({}) -TertiaryHighLight.args = { - buttonType: 'tertiaryHighLight', - buttonText: 'Button text', +export const TertiaryHighLight: Story = { + args: { + buttonType: 'tertiaryHighLight', + buttonText: 'Button text', + }, } -export const StartIconPrimary = Template.bind({}) -StartIconPrimary.args = { - buttonType: 'primary', - buttonText: 'Button text', - iconPlacement: 'startIcon', - iconName: 'reset', +export const StartIconPrimary: Story = { + args: { + buttonType: 'primary', + buttonText: 'Button text', + iconPlacement: 'startIcon', + iconName: 'reset', + }, } -export const EndIconAlert = Template.bind({}) -EndIconAlert.args = { - buttonType: 'alert', - buttonText: 'Button text', - iconPlacement: 'endIcon', - iconName: 'play-round-corners', +export const EndIconAlert: Story = { + args: { + buttonType: 'alert', + buttonText: 'Button text', + iconPlacement: 'endIcon', + iconName: 'play-round-corners', + }, } -export const SecondaryRounded = Template.bind({}) -SecondaryRounded.args = { - buttonType: 'secondary', - buttonText: 'Button text', - buttonCategory: 'rounded', +export const SecondaryRounded: Story = { + args: { + buttonType: 'secondary', + buttonText: 'Button text', + buttonCategory: 'rounded', + }, } From 059c9e7e0a774194b94edd48a60215e24e9ea916 Mon Sep 17 00:00:00 2001 From: Jamey Huffnagle Date: Fri, 5 Apr 2024 11:30:49 -0400 Subject: [PATCH 050/194] refactor(robot-server): Remove engine polling from RunsPublisher (#14777) Closes EXEC-310 Removes polling logic from RunsPublisher, making use of the new protocol engine event bubbling via PublisherNotifier. --- app/src/organisms/RunTimeControl/hooks.ts | 2 +- app/src/resources/useNotifyService.ts | 6 +- .../robot_server/runs/dependencies.py | 3 +- .../robot_server/runs/run_data_manager.py | 4 +- robot-server/robot_server/runs/run_store.py | 7 - .../service/notifications/__init__.py | 2 + .../publishers/runs_publisher.py | 222 +++++++----------- .../tests/protocols/test_protocol_store.py | 2 +- robot-server/tests/runs/test_run_store.py | 1 - .../notifications/publishers/__init__.py | 0 .../test_maintenance_runs_publisher.py | 30 +++ .../publishers/test_runs_publisher.py | 145 ++++++++++++ 12 files changed, 270 insertions(+), 154 deletions(-) create mode 100644 robot-server/tests/service/notifications/publishers/__init__.py create mode 100644 robot-server/tests/service/notifications/publishers/test_maintenance_runs_publisher.py create mode 100644 robot-server/tests/service/notifications/publishers/test_runs_publisher.py diff --git a/app/src/organisms/RunTimeControl/hooks.ts b/app/src/organisms/RunTimeControl/hooks.ts index 1bed99157be..a6750326731 100644 --- a/app/src/organisms/RunTimeControl/hooks.ts +++ b/app/src/organisms/RunTimeControl/hooks.ts @@ -82,7 +82,7 @@ export function useRunStatus( !([ RUN_STATUS_FAILED, RUN_STATUS_SUCCEEDED, - RUN_STATUS_STOP_REQUESTED, + RUN_STATUS_STOPPED, ] as RunStatus[]).includes(lastRunStatus.current), onSuccess: data => (lastRunStatus.current = data?.data?.status ?? null), ...options, diff --git a/app/src/resources/useNotifyService.ts b/app/src/resources/useNotifyService.ts index ae0100a2103..19831dc9c62 100644 --- a/app/src/resources/useNotifyService.ts +++ b/app/src/resources/useNotifyService.ts @@ -42,7 +42,6 @@ export function useNotifyService({ const hostname = host?.hostname ?? null const doTrackEvent = useTrackEvent() const isFlex = useIsFlex(host?.robotName ?? '') - const hasUsedNotifyService = React.useRef(false) const seenHostname = React.useRef(null) const { enabled, staleTime, forceHttpPolling } = options @@ -62,16 +61,15 @@ export function useNotifyService({ callback: onDataEvent, }) dispatch(notifySubscribeAction(hostname, topic)) - hasUsedNotifyService.current = true seenHostname.current = hostname } else { setRefetch('always') } return () => { - if (hasUsedNotifyService.current) { + if (seenHostname.current != null) { appShellListener({ - hostname: seenHostname.current as string, + hostname: seenHostname.current, topic, callback: onDataEvent, isDismounting: true, diff --git a/robot-server/robot_server/runs/dependencies.py b/robot-server/robot_server/runs/dependencies.py index 20b8d087b66..f66ec9fdf1c 100644 --- a/robot-server/robot_server/runs/dependencies.py +++ b/robot-server/robot_server/runs/dependencies.py @@ -43,13 +43,12 @@ async def get_run_store( app_state: AppState = Depends(get_app_state), sql_engine: SQLEngine = Depends(get_sql_engine), - runs_publisher: RunsPublisher = Depends(get_runs_publisher), ) -> RunStore: """Get a singleton RunStore to keep track of created runs.""" run_store = _run_store_accessor.get_from(app_state) if run_store is None: - run_store = RunStore(sql_engine=sql_engine, runs_publisher=runs_publisher) + run_store = RunStore(sql_engine=sql_engine) _run_store_accessor.set_on(app_state, run_store) return run_store diff --git a/robot-server/robot_server/runs/run_data_manager.py b/robot-server/robot_server/runs/run_data_manager.py index 5c57a14ecda..570537a135c 100644 --- a/robot-server/robot_server/runs/run_data_manager.py +++ b/robot-server/robot_server/runs/run_data_manager.py @@ -186,7 +186,7 @@ async def create( created_at=created_at, protocol_id=protocol.protocol_id if protocol is not None else None, ) - await self._runs_publisher.begin_polling_engine_store( + await self._runs_publisher.initialize( get_current_command=self.get_current_command, get_state_summary=self._get_good_state_summary, run_id=run_id, @@ -277,7 +277,7 @@ async def delete(self, run_id: str) -> None: """ if run_id == self._engine_store.current_run_id: await self._engine_store.clear() - await self._runs_publisher.stop_polling_engine_store() + await self._runs_publisher.clean_up_current_run() self._run_store.remove(run_id=run_id) diff --git a/robot-server/robot_server/runs/run_store.py b/robot-server/robot_server/runs/run_store.py index 6178e180470..5aa6dbae96b 100644 --- a/robot-server/robot_server/runs/run_store.py +++ b/robot-server/robot_server/runs/run_store.py @@ -27,7 +27,6 @@ ) from robot_server.persistence.pydantic import json_to_pydantic, pydantic_to_json from robot_server.protocols.protocol_store import ProtocolNotFoundError -from robot_server.service.notifications import RunsPublisher from .action_models import RunAction, RunActionType from .run_models import RunNotFoundError @@ -94,11 +93,9 @@ class RunStore: def __init__( self, sql_engine: sqlalchemy.engine.Engine, - runs_publisher: RunsPublisher, ) -> None: """Initialize a RunStore with sql engine and notification client.""" self._sql_engine = sql_engine - self._runs_publisher = runs_publisher def update_run_state( self, @@ -166,7 +163,6 @@ def update_run_state( action_rows = transaction.execute(select_actions).all() self._clear_caches() - self._runs_publisher.publish_runs_advise_refetch(run_id=run_id) maybe_run_resource = _convert_row_to_run(row=run_row, action_rows=action_rows) if not maybe_run_resource.ok: raise maybe_run_resource.error @@ -192,7 +188,6 @@ def insert_action(self, run_id: str, action: RunAction) -> None: transaction.execute(insert) self._clear_caches() - self._runs_publisher.publish_runs_advise_refetch(run_id=run_id) def insert( self, @@ -235,7 +230,6 @@ def insert( raise ProtocolNotFoundError(protocol_id=run.protocol_id) self._clear_caches() - self._runs_publisher.publish_runs_advise_refetch(run_id=run_id) return run @lru_cache(maxsize=_CACHE_ENTRIES) @@ -467,7 +461,6 @@ def remove(self, run_id: str) -> None: raise RunNotFoundError(run_id) self._clear_caches() - self._runs_publisher.publish_runs_advise_unsubscribe(run_id=run_id) def _run_exists( self, run_id: str, connection: sqlalchemy.engine.Connection diff --git a/robot-server/robot_server/service/notifications/__init__.py b/robot-server/robot_server/service/notifications/__init__.py index 7a71a61298d..7fd648f32aa 100644 --- a/robot-server/robot_server/service/notifications/__init__.py +++ b/robot-server/robot_server/service/notifications/__init__.py @@ -14,6 +14,7 @@ get_runs_publisher, ) from .change_notifier import ChangeNotifier +from .topics import Topics __all__ = [ # main export @@ -32,4 +33,5 @@ # for testing "PublisherNotifier", "ChangeNotifier", + "Topics", ] diff --git a/robot-server/robot_server/service/notifications/publishers/runs_publisher.py b/robot-server/robot_server/service/notifications/publishers/runs_publisher.py index 94aed694e8f..b6744fbc90a 100644 --- a/robot-server/robot_server/service/notifications/publishers/runs_publisher.py +++ b/robot-server/robot_server/service/notifications/publishers/runs_publisher.py @@ -1,7 +1,7 @@ -from fastapi import Depends import asyncio -import logging -from typing import Union, Callable, Optional +from fastapi import Depends +from dataclasses import dataclass +from typing import Callable, Optional from opentrons.protocol_engine import CurrentCommand, StateSummary, EngineStatus @@ -11,173 +11,120 @@ get_app_state, ) from ..notification_client import NotificationClient, get_notification_client +from ..publisher_notifier import PublisherNotifier, get_publisher_notifier from ..topics import Topics -log: logging.Logger = logging.getLogger(__name__) +@dataclass +class RunHooks: + """Generated during a protocol run. Utilized by RunsPublisher.""" + + run_id: str + get_current_command: Callable[[str], Optional[CurrentCommand]] + get_state_summary: Callable[[str], Optional[StateSummary]] + + +@dataclass +class EngineStateSlice: + """Protocol Engine state relevant to RunsPublisher.""" -POLL_INTERVAL = 1 + current_command: Optional[CurrentCommand] = None + state_summary_status: Optional[EngineStatus] = None class RunsPublisher: """Publishes protocol runs topics.""" - def __init__(self, client: NotificationClient) -> None: + def __init__( + self, client: NotificationClient, publisher_notifier: PublisherNotifier + ) -> None: """Returns a configured Runs Publisher.""" self._client = client + self._publisher_notifier = publisher_notifier self._run_data_manager_polling = asyncio.Event() - self._previous_current_command: Union[CurrentCommand, None] = None - self._previous_state_summary_status: Union[EngineStatus, None] = None self._poller: Optional[asyncio.Task[None]] = None + # Variables and callbacks related to PE state changes. + self._run_hooks: Optional[RunHooks] = None + self._engine_state_slice: Optional[EngineStateSlice] = None - # TODO(jh, 2023-02-02): Instead of polling, emit current_commands directly from PE. - async def begin_polling_engine_store( - self, - get_current_command: Callable[[str], Optional[CurrentCommand]], - get_state_summary: Callable[[str], Optional[StateSummary]], - run_id: str, - ) -> None: - """Continuously poll the engine store for the current_command. - - Args: - get_current_command: Callback to get the currently executing command, if any. - get_state_summary: Callback to get the current run's state summary, if any. - run_id: ID of the current run. - """ - if self._poller is None: - self._poller = asyncio.create_task( - self._poll_engine_store( - get_current_command=get_current_command, - run_id=run_id, - get_state_summary=get_state_summary, - ) - ) - else: - await self.stop_polling_engine_store() - self._poller = asyncio.create_task( - self._poll_engine_store( - get_current_command=get_current_command, - run_id=run_id, - get_state_summary=get_state_summary, - ) - ) - - async def stop_polling_engine_store(self) -> None: - """Stops polling the engine store. Run-related topics will publish as the poller is cancelled.""" - if self._poller is not None: - self._run_data_manager_polling.set() - self._poller.cancel() - - def publish_runs_advise_refetch(self, run_id: str) -> None: - """Publishes the equivalent of GET /runs and GET /runs/:runId. - - Args: - run_id: ID of the current run. - """ - self._client.publish_advise_refetch(topic=Topics.RUNS) - self._client.publish_advise_refetch(topic=f"{Topics.RUNS}/{run_id}") - - def publish_runs_advise_unsubscribe(self, run_id: str) -> None: - """Publishes the equivalent of GET /runs and GET /runs/:runId. - - Args: - run_id: ID of the current run. - """ - self._client.publish_advise_unsubscribe(topic=Topics.RUNS) - self._client.publish_advise_unsubscribe(topic=f"{Topics.RUNS}/{run_id}") + self._publisher_notifier.register_publish_callbacks( + [self._handle_current_command_change, self._handle_engine_status_change] + ) - async def _poll_engine_store( + async def initialize( self, - get_current_command: Callable[[str], Optional[CurrentCommand]], - get_state_summary: Callable[[str], Optional[StateSummary]], run_id: str, - ) -> None: - """Asynchronously publish new current commands. - - Args: - get_current_command: Retrieves the engine store's current command. - get_state_summary: Retrieves the engine store's state summary. - run_id: ID of the current run. - """ - try: - await self._poll_for_run_id_info( - get_current_command=get_current_command, - get_state_summary=get_state_summary, - run_id=run_id, - ) - except asyncio.CancelledError: - self._clean_up_poller() - await self._publish_runs_advise_unsubscribe_async(run_id=run_id) - await self._client.publish_advise_refetch_async( - topic=Topics.RUNS_CURRENT_COMMAND - ) - except Exception as e: - log.error(f"Error within run data manager poller: {e}") - - async def _poll_for_run_id_info( - self, get_current_command: Callable[[str], Optional[CurrentCommand]], get_state_summary: Callable[[str], Optional[StateSummary]], - run_id: str, - ): - """Poll the engine store for a specific run's state while the poll is active. + ) -> None: + """Initialize RunsPublisher with necessary information derived from the current run. Args: - get_current_command: Retrieves the engine store's current command. - get_state_summary: Retrieves the engine store's state summary. run_id: ID of the current run. + get_current_command: Callback to get the currently executing command, if any. + get_state_summary: Callback to get the current run's state summary, if any. """ + self._run_hooks = RunHooks( + run_id=run_id, + get_current_command=get_current_command, + get_state_summary=get_state_summary, + ) + self._engine_state_slice = EngineStateSlice() - while not self._run_data_manager_polling.is_set(): - current_command = get_current_command(run_id) - current_state_summary = get_state_summary(run_id) - current_state_summary_status = ( - current_state_summary.status if current_state_summary else None - ) - - if self._previous_current_command != current_command: - await self._publish_current_command() - self._previous_current_command = current_command + await self._publish_runs_advise_refetch_async() - if self._previous_state_summary_status != current_state_summary_status: - await self._publish_runs_advise_refetch_async(run_id=run_id) - self._previous_state_summary_status = current_state_summary_status - await asyncio.sleep(POLL_INTERVAL) + async def clean_up_current_run(self) -> None: + """Publish final refetch and unsubscribe flags.""" + await self._publish_runs_advise_refetch_async() + await self._publish_runs_advise_unsubscribe_async() - async def _publish_current_command( - self, - ) -> None: + async def _publish_current_command(self) -> None: """Publishes the equivalent of GET /runs/:runId/commands?cursor=null&pageLength=1.""" await self._client.publish_advise_refetch_async( topic=Topics.RUNS_CURRENT_COMMAND ) - async def _publish_runs_advise_refetch_async(self, run_id: str) -> None: - """Asynchronously publishes the equivalent of GET /runs and GET /runs/:runId via a refetch message. + async def _publish_runs_advise_refetch_async(self) -> None: + """Publish a refetch flag for relevant runs topics.""" + if self._run_hooks is not None: + await self._client.publish_advise_refetch_async(topic=Topics.RUNS) + await self._client.publish_advise_refetch_async( + topic=f"{Topics.RUNS}/{self._run_hooks.run_id}" + ) - Args: - run_id: ID of the current run. - """ - await self._client.publish_advise_refetch_async(topic=Topics.RUNS) - await self._client.publish_advise_refetch_async(topic=f"{Topics.RUNS}/{run_id}") + async def _publish_runs_advise_unsubscribe_async(self) -> None: + """Publish an unsubscribe flag for relevant runs topics.""" + if self._run_hooks is not None: + await self._client.publish_advise_unsubscribe_async( + topic=f"{Topics.RUNS}/{self._run_hooks.run_id}" + ) - async def _publish_runs_advise_unsubscribe_async(self, run_id: str) -> None: - """Asynchronously publishes the equivalent of GET /runs and GET /runs/:runId via an unsubscribe message. + async def _handle_current_command_change(self) -> None: + """Publish a refetch flag if the current command has changed.""" + if self._run_hooks is not None and self._engine_state_slice is not None: + current_command = self._run_hooks.get_current_command( + self._run_hooks.run_id + ) + if self._engine_state_slice.current_command != current_command: + await self._publish_current_command() + self._engine_state_slice.current_command = current_command - Args: - run_id: ID of the current run. - """ - await self._client.publish_advise_unsubscribe_async(topic=Topics.RUNS) - await self._client.publish_advise_unsubscribe_async( - topic=f"{Topics.RUNS}/{run_id}" - ) + async def _handle_engine_status_change(self) -> None: + """Publish a refetch flag if the engine status has changed.""" + if self._run_hooks is not None and self._engine_state_slice is not None: + current_state_summary = self._run_hooks.get_state_summary( + self._run_hooks.run_id + ) - def _clean_up_poller(self) -> None: - """Cleans up the runs data manager poller.""" - self._poller = None - self._run_data_manager_polling.clear() - self._previous_current_command = None - self._previous_state_summary_status = None + if ( + current_state_summary is not None + and self._engine_state_slice.state_summary_status + != current_state_summary.status + ): + await self._publish_runs_advise_refetch_async() + self._engine_state_slice.state_summary_status = ( + current_state_summary.status + ) _runs_publisher_accessor: AppStateAccessor[RunsPublisher] = AppStateAccessor[ @@ -188,12 +135,15 @@ def _clean_up_poller(self) -> None: async def get_runs_publisher( app_state: AppState = Depends(get_app_state), notification_client: NotificationClient = Depends(get_notification_client), + publisher_notifier: PublisherNotifier = Depends(get_publisher_notifier), ) -> RunsPublisher: """Get a singleton RunsPublisher to publish runs topics.""" runs_publisher = _runs_publisher_accessor.get_from(app_state) if runs_publisher is None: - runs_publisher = RunsPublisher(client=notification_client) + runs_publisher = RunsPublisher( + client=notification_client, publisher_notifier=publisher_notifier + ) _runs_publisher_accessor.set_on(app_state, runs_publisher) return runs_publisher diff --git a/robot-server/tests/protocols/test_protocol_store.py b/robot-server/tests/protocols/test_protocol_store.py index bd6655e4c10..d75212fd2fe 100644 --- a/robot-server/tests/protocols/test_protocol_store.py +++ b/robot-server/tests/protocols/test_protocol_store.py @@ -50,7 +50,7 @@ def mock_runs_publisher(decoy: Decoy) -> RunsPublisher: @pytest.fixture def run_store(sql_engine: SQLEngine, mock_runs_publisher: RunsPublisher) -> RunStore: """Get a RunStore linked to the same database as the subject ProtocolStore.""" - return RunStore(sql_engine=sql_engine, runs_publisher=mock_runs_publisher) + return RunStore(sql_engine=sql_engine) async def test_insert_and_get_protocol( diff --git a/robot-server/tests/runs/test_run_store.py b/robot-server/tests/runs/test_run_store.py index bb089d4b40a..31cabbe56bd 100644 --- a/robot-server/tests/runs/test_run_store.py +++ b/robot-server/tests/runs/test_run_store.py @@ -47,7 +47,6 @@ def subject( """Get a ProtocolStore test subject.""" return RunStore( sql_engine=sql_engine, - runs_publisher=mock_runs_publisher, ) diff --git a/robot-server/tests/service/notifications/publishers/__init__.py b/robot-server/tests/service/notifications/publishers/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/robot-server/tests/service/notifications/publishers/test_maintenance_runs_publisher.py b/robot-server/tests/service/notifications/publishers/test_maintenance_runs_publisher.py new file mode 100644 index 00000000000..8a0cb6a1832 --- /dev/null +++ b/robot-server/tests/service/notifications/publishers/test_maintenance_runs_publisher.py @@ -0,0 +1,30 @@ +"""Tests for the maintenance runs publisher.""" +import pytest +from unittest.mock import AsyncMock + +from robot_server.service.notifications import MaintenanceRunsPublisher, Topics + + +@pytest.fixture +def notification_client() -> AsyncMock: + """Mocked notification client.""" + return AsyncMock() + + +@pytest.fixture +def maintenance_runs_publisher( + notification_client: AsyncMock, +) -> MaintenanceRunsPublisher: + """Instantiate MaintenanceRunsPublisher.""" + return MaintenanceRunsPublisher(notification_client) + + +@pytest.mark.asyncio +async def test_publish_current_maintenance_run( + notification_client: AsyncMock, maintenance_runs_publisher: MaintenanceRunsPublisher +) -> None: + """It should publish a notify flag for maintenance runs.""" + await maintenance_runs_publisher.publish_current_maintenance_run() + notification_client.publish_advise_refetch_async.assert_awaited_once_with( + topic=Topics.MAINTENANCE_RUNS_CURRENT_RUN + ) diff --git a/robot-server/tests/service/notifications/publishers/test_runs_publisher.py b/robot-server/tests/service/notifications/publishers/test_runs_publisher.py new file mode 100644 index 00000000000..29797dbf83a --- /dev/null +++ b/robot-server/tests/service/notifications/publishers/test_runs_publisher.py @@ -0,0 +1,145 @@ +"""Tests for runs publisher.""" +import pytest +from datetime import datetime +from unittest.mock import MagicMock, AsyncMock + +from robot_server.service.notifications import RunsPublisher, Topics +from opentrons.protocol_engine import CurrentCommand, EngineStatus + + +def mock_curent_command(command_id: str) -> CurrentCommand: + """Create a mock CurrentCommand.""" + return CurrentCommand( + command_id=command_id, + command_key="1", + index=0, + created_at=datetime(year=2021, month=1, day=1), + ) + + +@pytest.fixture +def notification_client() -> AsyncMock: + """Mocked notification client.""" + return AsyncMock() + + +@pytest.fixture +def publisher_notifier() -> AsyncMock: + """Mocked publisher notifier.""" + return AsyncMock() + + +@pytest.fixture +async def runs_publisher( + notification_client: AsyncMock, publisher_notifier: AsyncMock +) -> RunsPublisher: + """Instantiate RunsPublisher.""" + return RunsPublisher( + client=notification_client, publisher_notifier=publisher_notifier + ) + + +@pytest.mark.asyncio +async def test_initialize( + runs_publisher: RunsPublisher, notification_client: AsyncMock +) -> None: + """It should initialize the runs_publisher with required parameters and callbacks.""" + run_id = "1234" + get_current_command = AsyncMock() + get_state_summary = AsyncMock() + + await runs_publisher.initialize(run_id, get_current_command, get_state_summary) + + assert runs_publisher._run_hooks + assert runs_publisher._run_hooks.run_id == run_id + assert runs_publisher._run_hooks.get_current_command == get_current_command + assert runs_publisher._run_hooks.get_state_summary == get_state_summary + assert runs_publisher._engine_state_slice + assert runs_publisher._engine_state_slice.current_command is None + assert runs_publisher._engine_state_slice.state_summary_status is None + + notification_client.publish_advise_refetch_async.assert_any_await(topic=Topics.RUNS) + notification_client.publish_advise_refetch_async.assert_any_await( + topic=f"{Topics.RUNS}/1234" + ) + + +@pytest.mark.asyncio +async def test_clean_up_current_run( + runs_publisher: RunsPublisher, notification_client: AsyncMock +) -> None: + """It should publish to appropriate topics at the end of a run.""" + await runs_publisher.initialize("1234", AsyncMock(), AsyncMock()) + + await runs_publisher.clean_up_current_run() + + notification_client.publish_advise_refetch_async.assert_any_await(topic=Topics.RUNS) + notification_client.publish_advise_refetch_async.assert_any_await( + topic=f"{Topics.RUNS}/1234" + ) + notification_client.publish_advise_unsubscribe_async.assert_any_await( + topic=f"{Topics.RUNS}/1234" + ) + + +@pytest.mark.asyncio +async def test_handle_current_command_change( + runs_publisher: RunsPublisher, notification_client: AsyncMock +) -> None: + """It should handle command changes appropriately.""" + await runs_publisher.initialize( + "1234", lambda _: mock_curent_command("command1"), AsyncMock() + ) + + assert runs_publisher._run_hooks + assert runs_publisher._engine_state_slice + + runs_publisher._engine_state_slice.current_command = mock_curent_command("command1") + + await runs_publisher._handle_current_command_change() + + assert notification_client.publish_advise_refetch_async.call_count == 2 + + runs_publisher._run_hooks.get_current_command = lambda _: mock_curent_command( + "command2" + ) + + await runs_publisher._handle_current_command_change() + + notification_client.publish_advise_refetch_async.assert_any_await( + topic=Topics.RUNS_CURRENT_COMMAND + ) + + +@pytest.mark.asyncio +async def test_handle_engine_status_change( + runs_publisher: RunsPublisher, notification_client: AsyncMock +) -> None: + """It should handle engine status changes appropriately.""" + await runs_publisher.initialize( + "1234", lambda _: mock_curent_command("command1"), AsyncMock() + ) + + assert runs_publisher._run_hooks + assert runs_publisher._engine_state_slice + + runs_publisher._run_hooks.run_id = "1234" + runs_publisher._run_hooks.get_state_summary = MagicMock( + return_value=MagicMock(status=EngineStatus.IDLE) + ) + runs_publisher._engine_state_slice.state_summary_status = EngineStatus.IDLE + + await runs_publisher._handle_engine_status_change() + + assert notification_client.publish_advise_refetch_async.call_count == 2 + + runs_publisher._run_hooks.get_state_summary.return_value = MagicMock( + status=EngineStatus.RUNNING + ) + + await runs_publisher._handle_engine_status_change() + + notification_client.publish_advise_refetch_async.assert_any_await(topic=Topics.RUNS) + notification_client.publish_advise_refetch_async.assert_any_await( + topic=f"{Topics.RUNS}/1234" + ) From 66b1a3bdb532cfea384b4f88db5c301d79c4405e Mon Sep 17 00:00:00 2001 From: Nick Diehl <47604184+ncdiehl11@users.noreply.github.com> Date: Fri, 5 Apr 2024 12:54:05 -0400 Subject: [PATCH 051/194] fix(app, components): fix parameter table styling (#14809) --- .../ProtocolRunRunTimeParameters.tsx | 87 ++++++++++--------- .../src/molecules/ParametersTable/index.tsx | 74 +++++++++------- 2 files changed, 92 insertions(+), 69 deletions(-) diff --git a/app/src/organisms/Devices/ProtocolRun/ProtocolRunRunTimeParameters.tsx b/app/src/organisms/Devices/ProtocolRun/ProtocolRunRunTimeParameters.tsx index 2769cfdc313..09511fe8862 100644 --- a/app/src/organisms/Devices/ProtocolRun/ProtocolRunRunTimeParameters.tsx +++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunRunTimeParameters.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' -import styled from 'styled-components' +import styled, { css } from 'styled-components' import { formatRunTimeParameterDefaultValue } from '@opentrons/shared-data' import { ALIGN_CENTER, @@ -9,6 +9,7 @@ import { COLORS, DIRECTION_COLUMN, DIRECTION_ROW, + DISPLAY_INLINE, Flex, Icon, InfoScreen, @@ -96,7 +97,7 @@ export function ProtocolRunRuntimeParameters({ key={`${index}_${parameter.variableName}`} parameter={parameter} index={index} - runTimeParametersLength={runTimeParameters.length} + isLast={index === runTimeParameters.length - 1} t={t} /> ) @@ -113,41 +114,48 @@ export function ProtocolRunRuntimeParameters({ interface StyledTableRowComponentProps { parameter: RunTimeParameter index: number - runTimeParametersLength: number + isLast: boolean t: any } const StyledTableRowComponent = ( props: StyledTableRowComponentProps ): JSX.Element => { - const { parameter, index, runTimeParametersLength, t } = props + const { parameter, index, isLast, t } = props const [targetProps, tooltipProps] = useHoverTooltip() return ( - - - - {parameter.displayName} - {parameter.description != null ? ( - <> - - - - - {parameter.description} - - - ) : null} - + + + + {parameter.displayName} + + {parameter.description != null ? ( + <> + + + + + {parameter.description} + + + ) : null} - + {formatRunTimeParameterDefaultValue(parameter, t)} @@ -173,14 +181,14 @@ const StyledTable = styled.table` ` const StyledTableHeaderContainer = styled.thead` display: grid; - grid-template-columns: 1fr 1fr; - grid-gap: 48px; + grid-template-columns: 0.35fr 0.35fr; + grid-gap: ${SPACING.spacing48}; border-bottom: ${BORDERS.lineBorder}; ` const StyledTableHeader = styled.th` ${TYPOGRAPHY.labelSemiBold} - padding: ${SPACING.spacing8}; + padding-bottom: ${SPACING.spacing8}; ` interface StyledTableRowProps { @@ -189,19 +197,20 @@ interface StyledTableRowProps { const StyledTableRow = styled.tr` display: grid; - grid-template-columns: 1fr 1fr; - grid-gap: 48px; - padding-top: ${SPACING.spacing8}; - padding-bottom: ${SPACING.spacing8}; + grid-template-columns: 0.35fr 0.35fr; + grid-gap: ${SPACING.spacing48}; border-bottom: ${props => (props.isLast ? 'none' : BORDERS.lineBorder)}; - align-items: ${ALIGN_CENTER}; ` interface StyledTableCellProps { - isLast: boolean + paddingRight?: string + display?: string } const StyledTableCell = styled.td` - padding-left: ${SPACING.spacing8}; - height: 1.25rem; + align-items: ${ALIGN_CENTER}; + display: ${props => (props.display != null ? props.display : 'table-cell')}; + padding: ${SPACING.spacing8} 0; + padding-right: ${props => + props.paddingRight != null ? props.paddingRight : SPACING.spacing16}; ` diff --git a/components/src/molecules/ParametersTable/index.tsx b/components/src/molecules/ParametersTable/index.tsx index 03731f0e32f..4ca8d8a2cb0 100644 --- a/components/src/molecules/ParametersTable/index.tsx +++ b/components/src/molecules/ParametersTable/index.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import styled from 'styled-components' +import styled, { css } from 'styled-components' import { formatRunTimeParameterDefaultValue } from '@opentrons/shared-data' import { BORDERS, COLORS } from '../../helix-design-system' import { SPACING, TYPOGRAPHY } from '../../ui-style-constants/index' @@ -7,7 +7,7 @@ import { StyledText } from '../../atoms/StyledText' import { Tooltip, useHoverTooltip } from '../../tooltips' import { Icon } from '../../icons' import { Flex } from '../../primitives' -import { ALIGN_CENTER } from '../../styles' +import { DISPLAY_INLINE } from '../../styles' import type { RunTimeParameter } from '@opentrons/shared-data' @@ -82,7 +82,10 @@ export function ParametersTable({ {formatRunTimeParameterDefaultValue(parameter, t)} - + {formatRange(parameter, `${min}-${max}`)} @@ -107,30 +110,36 @@ const ParameterName = (props: ParameterNameProps): JSX.Element => { const [targetProps, tooltipProps] = useHoverTooltip() return ( - - - {displayName} - {description != null ? ( - <> - - - - - {description} - - - ) : null} - + + + {displayName} + + {description != null ? ( + <> + + + + + {description} + + + ) : null} ) } @@ -143,7 +152,8 @@ const StyledTable = styled.table` const StyledTableHeader = styled.th` ${TYPOGRAPHY.labelSemiBold} - padding: ${SPACING.spacing8}; + grid-gap: ${SPACING.spacing16}; + padding-bottom: ${SPACING.spacing8}; border-bottom: ${BORDERS.lineBorder}; ` @@ -152,17 +162,21 @@ interface StyledTableRowProps { } const StyledTableRow = styled.tr` - padding: ${SPACING.spacing8}; + grid-gap: ${SPACING.spacing16}; border-bottom: ${props => (props.isLast ? 'none' : BORDERS.lineBorder)}; ` interface StyledTableCellProps { isLast: boolean + paddingRight?: string + display?: string } const StyledTableCell = styled.td` width: 33%; - padding-left: ${SPACING.spacing8}; + display: ${props => (props.display != null ? props.display : 'table-cell')}; padding-top: ${SPACING.spacing12}; padding-bottom: ${props => (props.isLast ? 0 : SPACING.spacing12)}; + padding-right: ${props => + props.paddingRight != null ? props.paddingRight : SPACING.spacing16}; ` From d759c8ab00398fad3d3fa7de73316fc50ab58dd9 Mon Sep 17 00:00:00 2001 From: Jamey Huffnagle Date: Fri, 5 Apr 2024 15:06:13 -0400 Subject: [PATCH 052/194] fix(app-shell): Fix isConnectingToBroker nullish coalescence (#14816) --- .../src/notifications/__tests__/store.test.ts | 348 ++++++++++++++++++ app-shell/src/notifications/store.ts | 3 +- 2 files changed, 350 insertions(+), 1 deletion(-) create mode 100644 app-shell/src/notifications/__tests__/store.test.ts diff --git a/app-shell/src/notifications/__tests__/store.test.ts b/app-shell/src/notifications/__tests__/store.test.ts new file mode 100644 index 00000000000..7192c8c2fa0 --- /dev/null +++ b/app-shell/src/notifications/__tests__/store.test.ts @@ -0,0 +1,348 @@ +import { describe, it, expect, beforeEach } from 'vitest' + +import { connectionStore } from '../store' + +const MOCK_IP = 'MOCK_IP' +const MOCK_ROBOT = 'MOCK_ROBOT' +const MOCK_WINDOW = {} as any +const MOCK_CLIENT = { connected: true } as any +const MOCK_TOPIC = 'MOCK_TOPIC' as any + +describe('ConnectionStore', () => { + beforeEach(() => { + connectionStore.clearStore() + }) + + describe('getBrowserWindow', () => { + it('should return the browser window', () => { + connectionStore.setBrowserWindow(MOCK_WINDOW) + expect(connectionStore.getBrowserWindow()).toBe(MOCK_WINDOW) + }) + }) + + describe('getAllBrokersInStore', () => { + it('should return an empty array if there are no brokers in the store', () => { + expect(connectionStore.getAllBrokersInStore()).toEqual([]) + }) + + it('should return an array of broker names in the store', async () => { + await connectionStore.setPendingConnection(MOCK_ROBOT) + await connectionStore.setPendingConnection('robot2') + expect(connectionStore.getAllBrokersInStore()).toEqual([ + MOCK_ROBOT, + 'robot2', + ]) + }) + }) + + describe('getClient', () => { + it('should return null if the given IP is not associated with a connection', () => { + expect(connectionStore.getClient(MOCK_IP)).toBeNull() + }) + + it('should return the client if the given IP is associated with a connection', async () => { + await connectionStore.setPendingConnection(MOCK_ROBOT) + connectionStore.associateIPWithRobotName(MOCK_IP, MOCK_ROBOT) + await connectionStore.setConnected(MOCK_ROBOT, MOCK_CLIENT) + expect(connectionStore.getClient(MOCK_IP)).toBe(MOCK_CLIENT) + }) + }) + + describe('setErrorStatus and getFailedConnectionStatus', () => { + it('should return null if the given IP is not associated with a connection', () => { + expect(connectionStore.getFailedConnectionStatus(MOCK_IP)).toBeNull() + }) + + it('should return the unreachable status for the given IP', async () => { + connectionStore.associateIPWithRobotName(MOCK_IP, MOCK_ROBOT) + await connectionStore.setPendingConnection(MOCK_ROBOT) + await connectionStore.setErrorStatus(MOCK_IP, 'ECONNFAILED') + expect(connectionStore.getFailedConnectionStatus(MOCK_IP)).toBe( + 'ECONNFAILED' + ) + }) + + it('should return "ECONNFAILED" if the unreachable status for the given IP is "ECONNREFUSED" after the first error status check', async () => { + connectionStore.associateIPWithRobotName(MOCK_IP, MOCK_ROBOT) + await connectionStore.setPendingConnection(MOCK_ROBOT) + await connectionStore.setErrorStatus(MOCK_IP, 'ECONNREFUSED') + expect(connectionStore.getFailedConnectionStatus(MOCK_IP)).toBe( + 'ECONNREFUSED' + ) + expect(connectionStore.getFailedConnectionStatus(MOCK_IP)).toBe( + 'ECONNFAILED' + ) + }) + + it('should throw an error if the given IP is not associated with a connection', async () => { + await expect( + connectionStore.setErrorStatus(MOCK_IP, 'Connection refused') + ).rejects.toThrowError('MOCK_IP is not associated with a connection') + }) + }) + + describe('getRobotNameByIP', () => { + it('should return null if the given IP is not associated with a connection', () => { + expect(connectionStore.getRobotNameByIP(MOCK_IP)).toBeNull() + }) + + it('should return the robot name associated with the given IP', () => { + connectionStore.associateIPWithRobotName(MOCK_IP, MOCK_ROBOT) + expect(connectionStore.getRobotNameByIP(MOCK_IP)).toBe(MOCK_ROBOT) + }) + }) + + describe('setBrowserWindow', () => { + it('should set the browser window', () => { + connectionStore.setBrowserWindow(MOCK_WINDOW) + expect(connectionStore.getBrowserWindow()).toBe(MOCK_WINDOW) + }) + }) + + describe('setPendingConnection', () => { + it('should create a new connection if there is no connection currently connecting', async () => { + await connectionStore.setPendingConnection(MOCK_ROBOT) + expect(connectionStore.getAllBrokersInStore()).toEqual([MOCK_ROBOT]) + }) + + it('should reject with an error if there is already a connection currently connecting', async () => { + await expect( + connectionStore.setPendingConnection(MOCK_ROBOT) + ).resolves.toBeUndefined() + await expect( + connectionStore.setPendingConnection(MOCK_ROBOT) + ).rejects.toThrowError( + 'Cannot create a new connection while currently connecting.' + ) + }) + }) + + describe('setConnected', () => { + it('should set the client for the given robot name', async () => { + connectionStore.setPendingConnection(MOCK_ROBOT) + await connectionStore.setConnected(MOCK_ROBOT, MOCK_CLIENT) + connectionStore.associateIPWithRobotName(MOCK_IP, MOCK_ROBOT) + expect(connectionStore.getClient(MOCK_IP)).toBe(MOCK_CLIENT) + }) + + it('should reject with an error if there is already a connection for the given robot name', async () => { + const MOCK_CLIENT_2 = {} as any + await connectionStore.setPendingConnection(MOCK_ROBOT) + await connectionStore.setConnected(MOCK_ROBOT, MOCK_CLIENT) + connectionStore.associateIPWithRobotName(MOCK_IP, MOCK_ROBOT) + await expect( + connectionStore.setConnected(MOCK_ROBOT, MOCK_CLIENT_2) + ).rejects.toThrowError('Connection already exists for MOCK_ROBOT') + }) + + it('should reject with an error if the given robot name is not associated with a connection', async () => { + await expect( + connectionStore.setConnected(MOCK_ROBOT, MOCK_CLIENT) + ).rejects.toThrowError('IP is not associated with a connection') + }) + }) + + describe('setSubStatus', () => { + it('should set the pending sub status for the given IP and topic', async () => { + connectionStore.associateIPWithRobotName(MOCK_IP, MOCK_ROBOT) + await connectionStore.setPendingConnection(MOCK_ROBOT) + await connectionStore.setConnected(MOCK_ROBOT, MOCK_CLIENT) + await connectionStore.setSubStatus(MOCK_IP, MOCK_TOPIC, 'pending') + expect(connectionStore.isPendingSub(MOCK_ROBOT, MOCK_TOPIC)).toBe(true) + }) + + it('should set the subscribed status for the given IP and topic', async () => { + connectionStore.associateIPWithRobotName(MOCK_IP, MOCK_ROBOT) + await connectionStore.setPendingConnection(MOCK_ROBOT) + await connectionStore.setConnected(MOCK_ROBOT, MOCK_CLIENT) + await connectionStore.setSubStatus(MOCK_IP, MOCK_TOPIC, 'subscribed') + expect(connectionStore.isActiveSub(MOCK_ROBOT, MOCK_TOPIC)).toBe(true) + expect(connectionStore.isPendingSub(MOCK_ROBOT, MOCK_TOPIC)).toBe(false) + }) + + it('should throw an error if the given IP is not associated with a connection', async () => { + await expect( + connectionStore.setSubStatus(MOCK_IP, MOCK_TOPIC, 'pending') + ).rejects.toThrowError('IP is not associated with a connection') + }) + }) + + describe('setUnsubStatus', () => { + it('should set the pending unsub status for the given IP and topic if it is currently subscribed', async () => { + connectionStore.associateIPWithRobotName(MOCK_IP, MOCK_ROBOT) + await connectionStore.setPendingConnection(MOCK_ROBOT) + await connectionStore.setConnected(MOCK_ROBOT, MOCK_CLIENT) + await connectionStore.setSubStatus(MOCK_IP, MOCK_TOPIC, 'subscribed') + await connectionStore.setUnsubStatus(MOCK_IP, MOCK_TOPIC, 'pending') + expect(connectionStore.isPendingUnsub(MOCK_IP, MOCK_TOPIC)).toBe(true) + expect(connectionStore.isActiveSub(MOCK_ROBOT, MOCK_TOPIC)).toBe(true) + }) + + it('should set the unsubscribed status for the given IP and topic if it is currently subscribed', async () => { + connectionStore.associateIPWithRobotName(MOCK_IP, MOCK_ROBOT) + await connectionStore.setPendingConnection(MOCK_ROBOT) + await connectionStore.setConnected(MOCK_ROBOT, MOCK_CLIENT) + await connectionStore.setSubStatus(MOCK_IP, MOCK_TOPIC, 'subscribed') + await connectionStore.setUnsubStatus(MOCK_IP, MOCK_TOPIC, 'unsubscribed') + expect(connectionStore.isActiveSub(MOCK_ROBOT, MOCK_TOPIC)).toBe(false) + expect(connectionStore.isPendingUnsub(MOCK_IP, MOCK_TOPIC)).toBe(false) + }) + + it('should not do anything if the given IP is not associated with a connection', async () => { + await expect( + connectionStore.setUnsubStatus(MOCK_IP, MOCK_TOPIC, 'pending') + ).rejects.toThrowError('IP is not associated with a connection') + }) + }) + + describe('associateIPWithRobotName', () => { + it('should associate the given IP with the given robot name', () => { + connectionStore.associateIPWithRobotName(MOCK_IP, MOCK_ROBOT) + expect(connectionStore.getRobotNameByIP(MOCK_IP)).toBe(MOCK_ROBOT) + }) + + it('should update the association if the IP is already associated with a different robot name', () => { + connectionStore.associateIPWithRobotName(MOCK_IP, MOCK_ROBOT) + connectionStore.associateIPWithRobotName(MOCK_IP, 'robot2') + expect(connectionStore.getRobotNameByIP(MOCK_IP)).toBe('robot2') + }) + }) + + describe('clearStore', () => { + it('should clear all connections and robot names', async () => { + await connectionStore.setPendingConnection(MOCK_ROBOT) + connectionStore.setBrowserWindow(MOCK_WINDOW) + expect(connectionStore.getAllBrokersInStore()).not.toEqual([]) + expect(connectionStore.getBrowserWindow()).not.toBeNull() + connectionStore.clearStore() + expect(connectionStore.getAllBrokersInStore()).toEqual([]) + expect(connectionStore.getBrowserWindow()).toBeNull() + }) + }) + + describe('isConnectedToBroker', () => { + it('should return false if the given robot name is not associated with a connection', () => { + expect(connectionStore.isConnectedToBroker(MOCK_ROBOT)).toBe(false) + }) + + it('should return false if the connection client is null', async () => { + await connectionStore.setPendingConnection(MOCK_ROBOT) + expect(connectionStore.isConnectedToBroker(MOCK_ROBOT)).toBe(false) + }) + + it('should return true if the connection client is not null', async () => { + await connectionStore.setPendingConnection(MOCK_ROBOT) + await connectionStore.setConnected(MOCK_ROBOT, MOCK_CLIENT) + expect(connectionStore.isConnectedToBroker(MOCK_ROBOT)).toBe(true) + }) + }) + + describe('isConnectingToBroker', () => { + it('should return false if the given robot name is not associated with a connection', () => { + expect(connectionStore.isConnectingToBroker(MOCK_ROBOT)).toBe(false) + }) + + it('should return false if the connection client is not null', () => { + connectionStore.setPendingConnection(MOCK_ROBOT) + connectionStore.setConnected(MOCK_ROBOT, MOCK_CLIENT) + expect(connectionStore.isConnectingToBroker(MOCK_ROBOT)).toBe(false) + }) + + it('should return true if the connection client is null and the connection is not terminated', () => { + connectionStore.setPendingConnection(MOCK_ROBOT) + expect(connectionStore.isConnectingToBroker(MOCK_ROBOT)).toBe(true) + }) + }) + + describe('isPendingSub', () => { + it('should return false if the given IP is not associated with a connection', () => { + expect(connectionStore.isPendingSub(MOCK_ROBOT, MOCK_TOPIC)).toBe(false) + }) + + it('should return false if the topic is not pending', () => { + connectionStore.associateIPWithRobotName(MOCK_IP, MOCK_ROBOT) + expect(connectionStore.isPendingSub(MOCK_ROBOT, MOCK_TOPIC)).toBe(false) + }) + + it('should return true if the topic is pending', async () => { + connectionStore.associateIPWithRobotName(MOCK_IP, MOCK_ROBOT) + await connectionStore.setPendingConnection(MOCK_ROBOT) + await connectionStore.setConnected(MOCK_ROBOT, MOCK_CLIENT) + connectionStore.setSubStatus(MOCK_IP, MOCK_TOPIC, 'pending') + expect(connectionStore.isPendingSub(MOCK_ROBOT, MOCK_TOPIC)).toBe(true) + }) + }) + + describe('isActiveSub', () => { + it('should return false if the given IP is not associated with a connection', () => { + expect(connectionStore.isActiveSub(MOCK_ROBOT, MOCK_TOPIC)).toBe(false) + }) + + it('should return false if the topic is not subscribed', () => { + connectionStore.associateIPWithRobotName(MOCK_IP, MOCK_ROBOT) + expect(connectionStore.isActiveSub(MOCK_ROBOT, MOCK_TOPIC)).toBe(false) + }) + + it('should return true if the topic is subscribed', async () => { + connectionStore.associateIPWithRobotName(MOCK_IP, MOCK_ROBOT) + await connectionStore.setPendingConnection(MOCK_ROBOT) + await connectionStore.setConnected(MOCK_ROBOT, MOCK_CLIENT) + await connectionStore.setSubStatus(MOCK_IP, MOCK_TOPIC, 'subscribed') + expect(connectionStore.isActiveSub(MOCK_ROBOT, MOCK_TOPIC)).toBe(true) + }) + }) + + describe('isPendingUnsub', () => { + it('should return false if the given IP is not associated with a connection', () => { + expect(connectionStore.isPendingUnsub(MOCK_IP, MOCK_TOPIC)).toBe(false) + }) + + it('should return false if the topic is not pending', () => { + connectionStore.associateIPWithRobotName(MOCK_IP, MOCK_ROBOT) + expect(connectionStore.isPendingUnsub(MOCK_IP, MOCK_TOPIC)).toBe(false) + }) + + it('should return true if the topic is pending', async () => { + connectionStore.associateIPWithRobotName(MOCK_IP, MOCK_ROBOT) + await connectionStore.setPendingConnection(MOCK_ROBOT) + await connectionStore.setConnected(MOCK_ROBOT, MOCK_CLIENT) + await connectionStore.setSubStatus(MOCK_IP, MOCK_TOPIC, 'subscribed') + await connectionStore.setUnsubStatus(MOCK_IP, MOCK_TOPIC, 'pending') + expect(connectionStore.isPendingUnsub(MOCK_IP, MOCK_TOPIC)).toBe(true) + }) + }) + + describe('isConnectionTerminated', () => { + it('should return true if the given robot name is not associated with a connection', () => { + expect(connectionStore.isConnectionTerminated(MOCK_ROBOT)).toBe(true) + }) + + it('should return true if the unreachable status is not null', async () => { + connectionStore.associateIPWithRobotName(MOCK_IP, MOCK_ROBOT) + await connectionStore.setPendingConnection(MOCK_ROBOT) + await connectionStore.setConnected(MOCK_ROBOT, MOCK_CLIENT) + await connectionStore.setErrorStatus(MOCK_IP, 'Connection refused') + expect(connectionStore.isConnectionTerminated(MOCK_ROBOT)).toBe(true) + }) + + it('should return false if the unreachable status is null', async () => { + connectionStore.associateIPWithRobotName(MOCK_IP, MOCK_ROBOT) + await connectionStore.setPendingConnection(MOCK_ROBOT) + await connectionStore.setConnected(MOCK_ROBOT, MOCK_CLIENT) + expect(connectionStore.isConnectionTerminated(MOCK_ROBOT)).toBe(false) + }) + }) + + describe('isKnownPortBlockedIP', () => { + it('should return false if the given IP is not in the known port blocked IPs set', () => { + expect(connectionStore.isKnownPortBlockedIP('MOCK_IP_2')).toBe(false) + }) + + it('should return true if the given IP is in the known port blocked IPs set', async () => { + connectionStore.associateIPWithRobotName(MOCK_IP, MOCK_ROBOT) + await connectionStore.setPendingConnection(MOCK_ROBOT) + connectionStore.setErrorStatus(MOCK_IP, 'ECONNREFUSED') + expect(connectionStore.isKnownPortBlockedIP(MOCK_IP)).toBe(true) + }) + }) +}) diff --git a/app-shell/src/notifications/store.ts b/app-shell/src/notifications/store.ts index 9968080258e..c9742ec6f90 100644 --- a/app-shell/src/notifications/store.ts +++ b/app-shell/src/notifications/store.ts @@ -207,7 +207,8 @@ class ConnectionStore { public isConnectingToBroker(robotName: string): boolean { return ( - (this.hostsByRobotName[robotName]?.client == null ?? false) && + robotName in this.hostsByRobotName && + this.hostsByRobotName[robotName].client == null && !this.isConnectionTerminated(robotName) ) } From b22e631547428ccb7bc064ed7a80da61ff9b887e Mon Sep 17 00:00:00 2001 From: Nick Diehl <47604184+ncdiehl11@users.noreply.github.com> Date: Fri, 5 Apr 2024 15:24:15 -0400 Subject: [PATCH 053/194] feat(app, api-client, react-api-client): add optional RTP tp /protocols endpoint (#14817) closes AUTH-286 --- api-client/src/protocols/createProtocol.ts | 9 ++++++++- .../useCreateRunFromProtocol.ts | 9 +++++++-- .../ProtocolRunRunTimeParameters.tsx | 4 ++-- .../useCreateProtocolMutation.test.tsx | 1 + .../src/protocols/useCreateProtocolMutation.ts | 18 +++++++++++++++--- 5 files changed, 33 insertions(+), 8 deletions(-) diff --git a/api-client/src/protocols/createProtocol.ts b/api-client/src/protocols/createProtocol.ts index 64593d1a953..2bcbefe6a7b 100644 --- a/api-client/src/protocols/createProtocol.ts +++ b/api-client/src/protocols/createProtocol.ts @@ -2,15 +2,22 @@ import { POST, request } from '../request' import type { ResponsePromise } from '../request' import type { HostConfig } from '../types' import type { Protocol } from './types' +import type { RunTimeParameterCreateData } from '../runs' export function createProtocol( config: HostConfig, files: File[], - protocolKey?: string + protocolKey?: string, + runTimeParameterValues?: RunTimeParameterCreateData ): ResponsePromise { const formData = new FormData() files.forEach(file => formData.append('files', file, file.name)) if (protocolKey != null) formData.append('key', protocolKey) + if (runTimeParameterValues != null) + formData.append( + 'runTimeParameterValues', + JSON.stringify(runTimeParameterValues) + ) return request(POST, '/protocols', formData, config) } diff --git a/app/src/organisms/ChooseRobotToRunProtocolSlideout/useCreateRunFromProtocol.ts b/app/src/organisms/ChooseRobotToRunProtocolSlideout/useCreateRunFromProtocol.ts index 209e886fc29..c649d2eb885 100644 --- a/app/src/organisms/ChooseRobotToRunProtocolSlideout/useCreateRunFromProtocol.ts +++ b/app/src/organisms/ChooseRobotToRunProtocolSlideout/useCreateRunFromProtocol.ts @@ -83,7 +83,8 @@ export function useCreateRunFromProtocol( }) }, }, - host + host, + runTimeParameterValues ) let error = @@ -107,7 +108,11 @@ export function useCreateRunFromProtocol( ) => { resetRunMutation() createProtocolRun( - { files: [...srcFiles, ...customLabwareFiles], protocolKey }, + { + files: [...srcFiles, ...customLabwareFiles], + protocolKey, + runTimeParameterValues, + }, ...args ) }, diff --git a/app/src/organisms/Devices/ProtocolRun/ProtocolRunRunTimeParameters.tsx b/app/src/organisms/Devices/ProtocolRun/ProtocolRunRunTimeParameters.tsx index 09511fe8862..ea7ec478415 100644 --- a/app/src/organisms/Devices/ProtocolRun/ProtocolRunRunTimeParameters.tsx +++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunRunTimeParameters.tsx @@ -1,7 +1,7 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' import styled, { css } from 'styled-components' -import { formatRunTimeParameterDefaultValue } from '@opentrons/shared-data' +import { formatRunTimeParameterValue } from '@opentrons/shared-data' import { ALIGN_CENTER, BORDERS, @@ -158,7 +158,7 @@ const StyledTableRowComponent = ( - {formatRunTimeParameterDefaultValue(parameter, t)} + {formatRunTimeParameterValue(parameter, t)} {parameter.value !== parameter.default ? ( { result.current.createProtocol({ files: createProtocolData, protocolKey: 'fakeProtocolKey', + runTimeParameterValues: { fakeParamName: 5.0 }, }) ) diff --git a/react-api-client/src/protocols/useCreateProtocolMutation.ts b/react-api-client/src/protocols/useCreateProtocolMutation.ts index 1474787b75e..2e36321e311 100644 --- a/react-api-client/src/protocols/useCreateProtocolMutation.ts +++ b/react-api-client/src/protocols/useCreateProtocolMutation.ts @@ -8,11 +8,17 @@ import { import { createProtocol } from '@opentrons/api-client' import { useHost } from '../api' import type { AxiosError } from 'axios' -import type { ErrorResponse, HostConfig, Protocol } from '@opentrons/api-client' +import type { + ErrorResponse, + HostConfig, + Protocol, + RunTimeParameterCreateData, +} from '@opentrons/api-client' export interface CreateProtocolVariables { files: File[] protocolKey?: string + runTimeParameterValues?: RunTimeParameterCreateData } export type UseCreateProtocolMutationResult = UseMutationResult< Protocol, @@ -34,7 +40,8 @@ export type UseCreateProtocolMutationOptions = UseMutationOptions< export function useCreateProtocolMutation( options: UseCreateProtocolMutationOptions = {}, - hostOverride?: HostConfig | null + hostOverride?: HostConfig | null, + runTimeParameterValues?: RunTimeParameterCreateData ): UseCreateProtocolMutationResult { const contextHost = useHost() const host = @@ -48,7 +55,12 @@ export function useCreateProtocolMutation( >( [host, 'protocols'], ({ files: protocolFiles, protocolKey }) => - createProtocol(host as HostConfig, protocolFiles, protocolKey) + createProtocol( + host as HostConfig, + protocolFiles, + protocolKey, + runTimeParameterValues + ) .then(response => { const protocolId = response.data.data.id queryClient From f3f9c28f1efda19c8255402e9256bbd8225f61a4 Mon Sep 17 00:00:00 2001 From: koji Date: Mon, 8 Apr 2024 08:03:50 -0400 Subject: [PATCH 054/194] refactor(app): update Divider stories (#14831) * refactor(app): update Divider stories --- app/src/atoms/structure/Divider.stories.tsx | 83 +++++++++++---------- 1 file changed, 43 insertions(+), 40 deletions(-) diff --git a/app/src/atoms/structure/Divider.stories.tsx b/app/src/atoms/structure/Divider.stories.tsx index 301e40debf9..021eb562020 100644 --- a/app/src/atoms/structure/Divider.stories.tsx +++ b/app/src/atoms/structure/Divider.stories.tsx @@ -8,49 +8,52 @@ import { StyledText, TYPOGRAPHY, } from '@opentrons/components' -import { Divider } from './index' -import type { Story, Meta } from '@storybook/react' +import { Divider as DividerComponent } from './index' +import type { Meta, StoryObj } from '@storybook/react' -export default { +const meta: Meta = { title: 'App/Atoms/Divider', - component: Divider, -} as Meta + component: DividerComponent, + decorators: [ + Story => ( + <> + + + + + {'About Calibration'} + -const Template: Story> = args => ( - <> - - - - - - {'About Calibration'} - - - - {'This section is about calibration.'} - + + {'This section is about calibration.'} + + + - - - - - - - - - {'Deck Calibration'} - - - - {'This section is for deck calibration.'} - + + + + + + {'Deck Calibration'} + + + {'This section is for deck calibration.'} + + + - - - -) - -export const Primary = Template.bind({}) -Primary.args = { - marginY: SPACING.spacing16, + + ), + ], } +export default meta +type Story = StoryObj +export const Divider: Story = {} From 754295db32d0b254ea2ab9395a6ce6e57b74e297 Mon Sep 17 00:00:00 2001 From: koji Date: Mon, 8 Apr 2024 08:04:47 -0400 Subject: [PATCH 055/194] refactor(components): update Flex stories (#14830) * refactor(components): update Flex stories --- components/src/primitives/Flex.stories.tsx | 74 +++++++++++++--------- 1 file changed, 45 insertions(+), 29 deletions(-) diff --git a/components/src/primitives/Flex.stories.tsx b/components/src/primitives/Flex.stories.tsx index f9773b5fc55..1335fa52919 100644 --- a/components/src/primitives/Flex.stories.tsx +++ b/components/src/primitives/Flex.stories.tsx @@ -1,35 +1,51 @@ import * as React from 'react' -import { Flex as FlexComponent } from './Flex' -import { - Box, - DIRECTION_COLUMN, - JUSTIFY_SPACE_AROUND, -} from '@opentrons/components' +import { BORDERS, COLORS } from '../helix-design-system' +import { SPACING } from '../ui-style-constants' +import { DIRECTION_COLUMN, JUSTIFY_SPACE_AROUND } from '../styles' +import { StyledText } from '../atoms/StyledText' +import { Box, Flex as FlexComponent } from '../primitives' -import type { Story, Meta } from '@storybook/react' +import type { Meta, StoryObj } from '@storybook/react' -export default { +const meta: Meta = { title: 'Library/Atoms/Flex', -} as Meta + component: FlexComponent, +} + +export default meta + +type Story = StoryObj -const Template: Story> = args => ( - -) -export const Flex = Template.bind({}) -Flex.args = { - children: [ - - This is a flex child - , - - This is a flex child - , - ], - flexDirection: DIRECTION_COLUMN, - justifyContent: JUSTIFY_SPACE_AROUND, - backgroundColor: 'grey', - border: '1px solid black', - padding: '1rem', - maxWidth: '20rem', - height: '10rem', +export const Flex: Story = { + args: { + children: [ + + + This is a flex child + + , + + + This is a flex child + + , + ], + flexDirection: DIRECTION_COLUMN, + justifyContent: JUSTIFY_SPACE_AROUND, + backgroundColor: 'grey', + border: '1px solid black', + padding: '1rem', + maxWidth: '20rem', + height: '10rem', + }, } From 467517143c82f6a12525d9f1b0326395dee86a53 Mon Sep 17 00:00:00 2001 From: koji Date: Mon, 8 Apr 2024 08:05:17 -0400 Subject: [PATCH 056/194] chore: update storybook addon (#14829) * chore: update storybook addon --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 67e9f909547..a38a11bdcd3 100755 --- a/package.json +++ b/package.json @@ -133,7 +133,7 @@ "shx": "^0.3.3", "simple-git": "^3.15.1", "storybook": "^7.6.16", - "storybook-addon-pseudo-states": "^1.15.5", + "storybook-addon-pseudo-states": "2.0.0", "style-loader": "^1.1.3", "stylelint": "^11.0.0", "stylelint-config-standard": "^19.0.0", diff --git a/yarn.lock b/yarn.lock index fe7459d12e4..c18f88ecf3a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18536,10 +18536,10 @@ store2@^2.14.2: resolved "https://registry.yarnpkg.com/store2/-/store2-2.14.3.tgz#24077d7ba110711864e4f691d2af941ec533deb5" integrity sha512-4QcZ+yx7nzEFiV4BMLnr/pRa5HYzNITX2ri0Zh6sT9EyQHbBHacC6YigllUPU9X3D0f/22QCgfokpKs52YRrUg== -storybook-addon-pseudo-states@^1.15.5: - version "1.15.5" - resolved "https://registry.yarnpkg.com/storybook-addon-pseudo-states/-/storybook-addon-pseudo-states-1.15.5.tgz#47d40391440dff235c05938c5b033aa655dda38e" - integrity sha512-DVngZ4121lJ6s42vKNfmLCBKhBMhh01D7sCV/LohP0rZoVW6Zws552g906Wan5R14gnArAlPCxQ+zbgm7QqxDA== +storybook-addon-pseudo-states@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/storybook-addon-pseudo-states/-/storybook-addon-pseudo-states-2.0.0.tgz#4fa251aaea04ebc6d17b7e57e5f09ea240f14583" + integrity sha512-tLuuwB1k+xFsX8C1fn4G/vJm5wX33jvSLeqTsJgWwI3/AKJUf6Thbg/kg14I2AwN8nqffjun2PzE05Iea23n0w== storybook@^7.6.16: version "7.6.17" From 80a834e73df3eb5edb45f39df63acc663c723704 Mon Sep 17 00:00:00 2001 From: koji Date: Mon, 8 Apr 2024 08:07:39 -0400 Subject: [PATCH 057/194] refactor(app): update large button stories (#14823) * refactor(app): update large button stories --- app/src/atoms/buttons/LargeButton.stories.tsx | 64 +++++++++++-------- 1 file changed, 38 insertions(+), 26 deletions(-) diff --git a/app/src/atoms/buttons/LargeButton.stories.tsx b/app/src/atoms/buttons/LargeButton.stories.tsx index f1f9427a4cf..fa3a5e9d2fb 100644 --- a/app/src/atoms/buttons/LargeButton.stories.tsx +++ b/app/src/atoms/buttons/LargeButton.stories.tsx @@ -1,35 +1,47 @@ -import * as React from 'react' -import { VIEWPORT } from '@opentrons/components' +import { ICON_DATA_BY_NAME, VIEWPORT } from '@opentrons/components' import { LargeButton } from './' -import type { Story, Meta } from '@storybook/react' -export default { +import type { Meta, StoryObj } from '@storybook/react' + +const meta: Meta = { title: 'ODD/Atoms/Buttons/LargeButton', - argTypes: { onClick: { action: 'clicked' } }, + component: LargeButton, + argTypes: { + onClick: { action: 'clicked' }, + iconName: { + control: { + type: 'select', + }, + options: Object.keys(ICON_DATA_BY_NAME), + }, + }, parameters: VIEWPORT.touchScreenViewport, -} as Meta +} + +export default meta -const LargeButtonTemplate: Story< - React.ComponentProps -> = args => +type Story = StoryObj -export const PrimaryLargeButton = LargeButtonTemplate.bind({}) -PrimaryLargeButton.args = { - buttonText: 'Button text', - disabled: false, - iconName: 'play-round-corners', +export const Primary: Story = { + args: { + buttonText: 'Button text', + disabled: false, + iconName: 'play-round-corners', + }, } -export const SecondaryLargeButton = LargeButtonTemplate.bind({}) -SecondaryLargeButton.args = { - buttonText: 'Button text', - buttonType: 'secondary', - disabled: false, - iconName: 'build', +export const Secondary: Story = { + args: { + buttonText: 'Button text', + buttonType: 'secondary', + disabled: false, + iconName: 'build', + }, } -export const AlertLargeButton = LargeButtonTemplate.bind({}) -AlertLargeButton.args = { - buttonText: 'Button text', - buttonType: 'alert', - disabled: false, - iconName: 'reset', +export const Alert: Story = { + args: { + buttonText: 'Button text', + buttonType: 'alert', + disabled: false, + iconName: 'reset', + }, } From 6bb0f309b97378ae5cf41cb7e75840c898d1000f Mon Sep 17 00:00:00 2001 From: koji Date: Mon, 8 Apr 2024 08:08:02 -0400 Subject: [PATCH 058/194] refactor(components): update Link stories (#14825) * refactor(components): update Link stories --- components/src/primitives/Link.stories.tsx | 35 ++++++++++++---------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/components/src/primitives/Link.stories.tsx b/components/src/primitives/Link.stories.tsx index 2f54b472920..1aa3890d293 100644 --- a/components/src/primitives/Link.stories.tsx +++ b/components/src/primitives/Link.stories.tsx @@ -1,24 +1,27 @@ -import * as React from 'react' import { Link } from './Link' -import type { Story, Meta } from '@storybook/react' +import type { Meta, StoryObj } from '@storybook/react' -export default { +const meta: Meta = { title: 'Library/Atoms/Link', -} as Meta + component: Link, +} + +export default meta + +type Story = StoryObj -const Template: Story> = args => ( - -) -export const Basic = Template.bind({}) -Basic.args = { - children: 'hello anchor', - href: '#', +export const Basic: Story = { + args: { + children: 'hello anchor', + href: '#', + }, } -export const External = Template.bind({}) -External.args = { - children: 'hello opentrons', - external: true, - href: 'https://www.opentrons.com', +export const External: Story = { + args: { + children: 'hello opentrons', + external: true, + href: 'https://www.opentrons.com', + }, } From 45725a588c87016126146b12f581a792e475a0dc Mon Sep 17 00:00:00 2001 From: koji Date: Mon, 8 Apr 2024 08:09:06 -0400 Subject: [PATCH 059/194] refactor(app): update Line stories (#14826) * refactor(app): update Line stories --- app/src/atoms/structure/Line.stories.tsx | 90 +++++++++++++----------- 1 file changed, 49 insertions(+), 41 deletions(-) diff --git a/app/src/atoms/structure/Line.stories.tsx b/app/src/atoms/structure/Line.stories.tsx index ed017ed95e1..46a90756c71 100644 --- a/app/src/atoms/structure/Line.stories.tsx +++ b/app/src/atoms/structure/Line.stories.tsx @@ -9,49 +9,57 @@ import { TYPOGRAPHY, } from '@opentrons/components' -import { Line } from './index' -import type { Story, Meta } from '@storybook/react' +import { Line as LineComponent } from './index' +import type { Meta, StoryObj } from '@storybook/react' -export default { +const meta: Meta = { title: 'App/Atoms/Line', - component: Line, -} as Meta - -const Template: Story> = args => ( - <> - - - - - - {'About Calibration'} - - - - {'This section is about calibration.'} - + component: LineComponent, + decorators: [ + Story => ( + <> + + + + + + {'About Calibration'} + + + + {'This section is about calibration.'} + + + - - - - - - - - - {'Deck Calibration'} - - - - {'This section is for deck calibration.'} - + + + + + + + {'Deck Calibration'} + + + + {'This section is for deck calibration.'} + + + - - - -) - -export const Primary = Template.bind({}) -Primary.args = { - marginY: SPACING.spacing8, + + ), + ], } + +export default meta + +type Story = StoryObj + +export const Line: Story = {} From 0669849daa5d04e8ed208eaa119d618ce1c327b9 Mon Sep 17 00:00:00 2001 From: koji Date: Mon, 8 Apr 2024 10:11:22 -0400 Subject: [PATCH 060/194] fix(app): software keyboard unresponsive issue (#14819) * fix(app): software keyboard unresponsive issue --- app/package.json | 2 +- .../AlphanumericKeyboard.stories.tsx | 22 +++++++++------ .../AlphanumericKeyboard/index.css | 6 ++-- .../AlphanumericKeyboard/index.tsx | 11 ++++++-- .../FullKeyboard/FullKeyboard.stories.tsx | 20 +++++++------ .../SoftwareKeyboard/FullKeyboard/index.css | 5 ---- .../SoftwareKeyboard/FullKeyboard/index.tsx | 11 ++++++-- .../IndividualKey/IndividualKey.stories.tsx | 22 +++++++++------ .../SoftwareKeyboard/IndividualKey/index.tsx | 11 ++++++-- .../NumericalKeyboard.stories.tsx | 28 +++++++++---------- .../NumericalKeyboard/index.tsx | 9 ++++-- app/src/atoms/SoftwareKeyboard/index.css | 1 - app/src/pages/NameRobot/index.tsx | 6 ++-- yarn.lock | 10 +++---- 14 files changed, 96 insertions(+), 68 deletions(-) diff --git a/app/package.json b/app/package.json index f72519e3f4a..5097851c9ff 100644 --- a/app/package.json +++ b/app/package.json @@ -52,7 +52,7 @@ "react-redux": "8.1.2", "react-router-dom": "5.3.4", "react-select": "5.4.0", - "react-simple-keyboard": "^3.4.187", + "react-simple-keyboard": "^3.7.0", "react-viewport-list": "6.3.0", "redux": "4.0.5", "redux-observable": "1.1.0", diff --git a/app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/AlphanumericKeyboard.stories.tsx b/app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/AlphanumericKeyboard.stories.tsx index 6d30005ad9e..a610d352caf 100644 --- a/app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/AlphanumericKeyboard.stories.tsx +++ b/app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/AlphanumericKeyboard.stories.tsx @@ -8,20 +8,20 @@ import { } from '@opentrons/components' import { InputField } from '../../InputField' import { AlphanumericKeyboard } from '.' -import '../index.css' -import './index.css' -import type { Story, Meta } from '@storybook/react' +import type { Meta, StoryObj } from '@storybook/react' -export default { +const meta: Meta = { title: 'ODD/Atoms/SoftwareKeyboard/AlphanumericKeyboard', component: AlphanumericKeyboard, parameters: VIEWPORT.touchScreenViewport, -} as Meta +} + +export default meta -const Template: Story< - React.ComponentProps -> = args => { +type Story = StoryObj + +const Keyboard = (): JSX.Element => { const [showKeyboard, setShowKeyboard] = React.useState(false) const [value, setValue] = React.useState('') const keyboardRef = React.useRef(null) @@ -32,12 +32,14 @@ const Template: Story< value={value} type="text" placeholder="When focusing, the keyboard shows up" + // eslint-disable-next-line @typescript-eslint/no-confusing-void-expression onFocus={() => setShowKeyboard(true)} /> {showKeyboard && ( e != null && setValue(String(e))} keyboardRef={keyboardRef} /> @@ -47,4 +49,6 @@ const Template: Story< ) } -export const AlphanumericSoftwareKeyboard = Template.bind({}) +export const AlphanumericSoftwareKeyboard: Story = { + render: () => , +} diff --git a/app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/index.css b/app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/index.css index da0f9670b63..1fa59e2230a 100644 --- a/app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/index.css +++ b/app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/index.css @@ -32,10 +32,8 @@ background-color: #dedede; /* grey30 */ } -/* ToDo (kk:04/04/2024) this important will be removed when I refactor the entire css */ -.hg-layout-default .hg-row .hg-button, -.hg-layout-shift .hg-row .hg-button { - height: 62.3px !important; +.alphanumericKeyboard .hg-button { + height: 62.3px; } /* first row and second row */ diff --git a/app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/index.tsx b/app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/index.tsx index af02f09b31f..5698e49f1e6 100644 --- a/app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/index.tsx +++ b/app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/index.tsx @@ -2,18 +2,23 @@ import * as React from 'react' import Keyboard from 'react-simple-keyboard' import { alphanumericKeyboardLayout, customDisplay } from '../constants' +import '../index.css' +import './index.css' + +// TODO (kk:04/05/2024) add debug to make debugging easy interface AlphanumericKeyboardProps { onChange: (input: string) => void keyboardRef: React.MutableRefObject + debug?: boolean } export function AlphanumericKeyboard({ onChange, keyboardRef, + debug = false, // If true, will input a \n }: AlphanumericKeyboardProps): JSX.Element { const [layoutName, setLayoutName] = React.useState('default') const onKeyPress = (button: string): void => { - console.log(button) if (button === '{ABC}') handleShift() if (button === '{numbers}') handleNumber() if (button === '{abc}') handleUnShift() @@ -36,16 +41,16 @@ export function AlphanumericKeyboard({ return ( (keyboardRef.current = r)} - theme={'hg-theme-default oddTheme1'} + theme={'hg-theme-default oddTheme1 alphanumericKeyboard'} onChange={onChange} onKeyPress={onKeyPress} layoutName={layoutName} layout={alphanumericKeyboardLayout} display={customDisplay} mergeDisplay={true} - autoUseTouchEvents={true} useButtonTag={true} width="100%" + debug={debug} // If true, will input a \n /> ) } diff --git a/app/src/atoms/SoftwareKeyboard/FullKeyboard/FullKeyboard.stories.tsx b/app/src/atoms/SoftwareKeyboard/FullKeyboard/FullKeyboard.stories.tsx index 3aaea8cb33d..417c922876d 100644 --- a/app/src/atoms/SoftwareKeyboard/FullKeyboard/FullKeyboard.stories.tsx +++ b/app/src/atoms/SoftwareKeyboard/FullKeyboard/FullKeyboard.stories.tsx @@ -9,18 +9,18 @@ import { import { InputField } from '../../InputField' import { FullKeyboard } from '.' -import '../index.css' -import './index.css' +import type { Meta, StoryObj } from '@storybook/react' -import type { Story, Meta } from '@storybook/react' - -export default { +const meta: Meta = { title: 'ODD/Atoms/SoftwareKeyboard/FullKeyboard', component: FullKeyboard, parameters: VIEWPORT.touchScreenViewport, -} as Meta +} +export default meta -const Template: Story> = args => { +type Story = StoryObj + +const Keyboard = (): JSX.Element => { const [showKeyboard, setShowKeyboard] = React.useState(false) const [value, setValue] = React.useState('') const keyboardRef = React.useRef(null) @@ -31,12 +31,14 @@ const Template: Story> = args => { value={value} type="text" placeholder="When focusing, the keyboard shows up" + // eslint-disable-next-line @typescript-eslint/no-confusing-void-expression onFocus={() => setShowKeyboard(true)} /> {showKeyboard && ( e != null && setValue(String(e))} keyboardRef={keyboardRef} /> @@ -46,4 +48,6 @@ const Template: Story> = args => { ) } -export const FullSoftwareKeyboard = Template.bind({}) +export const FullSoftwareKeyboard: Story = { + render: () => , +} diff --git a/app/src/atoms/SoftwareKeyboard/FullKeyboard/index.css b/app/src/atoms/SoftwareKeyboard/FullKeyboard/index.css index b54cde35e04..b3ff8968da4 100644 --- a/app/src/atoms/SoftwareKeyboard/FullKeyboard/index.css +++ b/app/src/atoms/SoftwareKeyboard/FullKeyboard/index.css @@ -24,10 +24,6 @@ grid-gap: 3px; } -.simple-keyboard.simple-keyboard.oddTheme1 .hg-button { - height: 44.75px; -} - .simple-keyboard.simple-keyboard.oddTheme1 .hg-button:not(:last-child) { margin-bottom: 3px; } @@ -42,7 +38,6 @@ .hg-layout-symbols .hg-row .hg-button, .hg-layout-numbers .hg-row .hg-button { color: #16212d; - height: 44.75px; font-size: 20px; font-style: normal; font-weight: 600; diff --git a/app/src/atoms/SoftwareKeyboard/FullKeyboard/index.tsx b/app/src/atoms/SoftwareKeyboard/FullKeyboard/index.tsx index 850ad689758..69c5c748d3a 100644 --- a/app/src/atoms/SoftwareKeyboard/FullKeyboard/index.tsx +++ b/app/src/atoms/SoftwareKeyboard/FullKeyboard/index.tsx @@ -1,15 +1,21 @@ import * as React from 'react' -import Keyboard from 'react-simple-keyboard' +import { KeyboardReact as Keyboard } from 'react-simple-keyboard' import { customDisplay, fullKeyboardLayout } from '../constants' +import '../index.css' +import './index.css' + +// TODO (kk:04/05/2024) add debug to make debugging easy interface FullKeyboardProps { onChange: (input: string) => void keyboardRef: React.MutableRefObject + debug?: boolean } export function FullKeyboard({ onChange, keyboardRef, + debug = false, }: FullKeyboardProps): JSX.Element { const [layoutName, setLayoutName] = React.useState('default') const handleShift = (button: string): void => { @@ -51,8 +57,9 @@ export function FullKeyboard({ layout={fullKeyboardLayout} display={customDisplay} mergeDisplay={true} - autoUseTouchEvents={true} useButtonTag={true} + debug={debug} // If true, will input a \n + baseClass="fullKeyboard" /> ) } diff --git a/app/src/atoms/SoftwareKeyboard/IndividualKey/IndividualKey.stories.tsx b/app/src/atoms/SoftwareKeyboard/IndividualKey/IndividualKey.stories.tsx index 3600dafc89a..3f91df121f6 100644 --- a/app/src/atoms/SoftwareKeyboard/IndividualKey/IndividualKey.stories.tsx +++ b/app/src/atoms/SoftwareKeyboard/IndividualKey/IndividualKey.stories.tsx @@ -8,18 +8,20 @@ import { } from '@opentrons/components' import { InputField } from '../../InputField' import { IndividualKey } from '.' -import '../index.css' -import './index.css' -import type { Story, Meta } from '@storybook/react' +import type { Meta, StoryObj } from '@storybook/react' -export default { +const meta: Meta = { title: 'ODD/Atoms/SoftwareKeyboard/IndividualKey', component: IndividualKey, parameters: VIEWPORT.touchScreenViewport, -} as Meta +} + +export default meta + +type Story = StoryObj -const Template: Story> = args => { +const Keyboard = ({ ...args }): JSX.Element => { const [showKeyboard, setShowKeyboard] = React.useState(false) const [value, setValue] = React.useState('') const keyboardRef = React.useRef(null) @@ -49,7 +51,9 @@ const Template: Story> = args => { ) } -export const Keyboard = Template.bind({}) -Keyboard.args = { - keyText: 'hello!', +export const IndividualKeySoftwareKeyboard: Story = args => ( + +) +IndividualKeySoftwareKeyboard.args = { + keyText: 'hello', } diff --git a/app/src/atoms/SoftwareKeyboard/IndividualKey/index.tsx b/app/src/atoms/SoftwareKeyboard/IndividualKey/index.tsx index c501b0eccc6..9ff8c278423 100644 --- a/app/src/atoms/SoftwareKeyboard/IndividualKey/index.tsx +++ b/app/src/atoms/SoftwareKeyboard/IndividualKey/index.tsx @@ -1,19 +1,26 @@ import * as React from 'react' -import Keyboard from 'react-simple-keyboard' +import { KeyboardReact as Keyboard } from 'react-simple-keyboard' + +import '../index.css' +import './index.css' const customDisplay = { '{backspace}': 'del', } + +// TODO (kk:04/05/2024) add debug to make debugging easy interface IndividualKeyProps { onChange: (input: string) => void keyboardRef: React.MutableRefObject keyText: string + debug?: boolean } export function IndividualKey({ onChange, keyboardRef, keyText, + debug = false, }: IndividualKeyProps): JSX.Element { const numericalKeyboard = { layout: { @@ -31,10 +38,10 @@ export function IndividualKey({ onChange={onChange} layoutName="default" display={customDisplay} - autoUseTouchEvents={true} useButtonTag={true} {...numericalKeyboard} width="100%" + debug={debug} // If true, will input a \n /> ) } diff --git a/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/NumericalKeyboard.stories.tsx b/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/NumericalKeyboard.stories.tsx index 21b7c4c761b..d7659866c6a 100644 --- a/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/NumericalKeyboard.stories.tsx +++ b/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/NumericalKeyboard.stories.tsx @@ -8,12 +8,10 @@ import { } from '@opentrons/components' import { InputField } from '../../InputField' import { NumericalKeyboard } from '.' -import '../index.css' -import './index.css' -import type { Story, Meta } from '@storybook/react' +import type { Meta, StoryObj } from '@storybook/react' -export default { +const meta: Meta = { title: 'ODD/Atoms/SoftwareKeyboard/NumericalKeyboard', component: NumericalKeyboard, parameters: VIEWPORT.touchScreenViewport, @@ -23,21 +21,23 @@ export default { type: 'boolean', options: [true, false], }, - defaultValue: false, }, hasHyphen: { control: { type: 'boolean', options: [true, false], }, - defaultValue: false, }, }, -} as Meta +} + +export default meta + +type Story = StoryObj -const Template: Story< - React.ComponentProps -> = args => { +const Keyboard = (args): JSX.Element => { + const { isDecimal, hasHyphen } = args + console.log(isDecimal, hasHyphen) const [showKeyboard, setShowKeyboard] = React.useState(false) const [value, setValue] = React.useState('') const keyboardRef = React.useRef(null) @@ -64,8 +64,8 @@ const Template: Story< // eslint-disable-next-line @typescript-eslint/no-confusing-void-expression onChange={e => e != null && setValue(String(e))} keyboardRef={keyboardRef} - isDecimal={args.isDecimal} - hasHyphen={args.hasHyphen} + isDecimal={isDecimal} + hasHyphen={hasHyphen} /> )} @@ -73,8 +73,8 @@ const Template: Story< ) } -export const Keyboard = Template.bind({}) -Keyboard.args = { +export const NumericalSoftwareKeyboard: Story = args => +NumericalSoftwareKeyboard.args = { isDecimal: false, hasHyphen: false, } diff --git a/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/index.tsx b/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/index.tsx index 85d1a0b8b43..9065bcce44f 100644 --- a/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/index.tsx +++ b/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/index.tsx @@ -1,12 +1,16 @@ import * as React from 'react' -import Keyboard from 'react-simple-keyboard' +import { KeyboardReact as Keyboard } from 'react-simple-keyboard' import { numericalKeyboardLayout, numericalCustom } from '../constants' +import '../index.css' +import './index.css' +// Note (kk:04/05/2024) add debug to make debugging easy interface NumericalKeyboardProps { onChange: (input: string) => void keyboardRef: React.MutableRefObject isDecimal?: boolean hasHyphen?: boolean + debug?: boolean } // the default keyboard layout intKeyboard that doesn't have decimal point and hyphen. @@ -15,6 +19,7 @@ export function NumericalKeyboard({ keyboardRef, isDecimal = false, hasHyphen = false, + debug = false, }: NumericalKeyboardProps): JSX.Element { const layoutName = `${isDecimal ? 'float' : 'int'}${ hasHyphen ? 'NegKeyboard' : 'Keyboard' @@ -30,10 +35,10 @@ export function NumericalKeyboard({ theme={'hg-theme-default oddTheme1 numerical-keyboard'} onChange={onChange} display={numericalCustom} - autoUseTouchEvents={true} useButtonTag={true} layoutName={layoutName} layout={numericalKeyboardLayout} + debug={debug} // If true, will input a \n /> ) } diff --git a/app/src/atoms/SoftwareKeyboard/index.css b/app/src/atoms/SoftwareKeyboard/index.css index b89c7c1d887..f19179f4366 100644 --- a/app/src/atoms/SoftwareKeyboard/index.css +++ b/app/src/atoms/SoftwareKeyboard/index.css @@ -60,7 +60,6 @@ box-sizing: border-box; cursor: pointer; display: flex; - height: 40px; justify-content: center; padding: 5px; -webkit-tap-highlight-color: rgba(0, 0, 0, 0); diff --git a/app/src/pages/NameRobot/index.tsx b/app/src/pages/NameRobot/index.tsx index 1bbf4099234..3823525ccb4 100644 --- a/app/src/pages/NameRobot/index.tsx +++ b/app/src/pages/NameRobot/index.tsx @@ -18,8 +18,8 @@ import { POSITION_FIXED, POSITION_RELATIVE, SPACING, - TYPOGRAPHY, StyledText, + TYPOGRAPHY, } from '@opentrons/components' import { useUpdateRobotNameMutation } from '@opentrons/react-api-client' @@ -121,7 +121,7 @@ export function NameRobot(): JSX.Element { defaultValues: { newRobotName: '', }, - resolver: resolver, + resolver, }) const newRobotName = watch('newRobotName') @@ -298,7 +298,7 @@ export function NameRobot(): JSX.Element { { field.onChange(input) - trigger('newRobotName') + void trigger('newRobotName') }} keyboardRef={keyboardRef} /> diff --git a/yarn.lock b/yarn.lock index c18f88ecf3a..9773a4fe6f3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2416,7 +2416,7 @@ react-redux "8.1.2" react-router-dom "5.3.4" react-select "5.4.0" - react-simple-keyboard "^3.4.187" + react-simple-keyboard "^3.7.0" react-viewport-list "6.3.0" redux "4.0.5" redux-observable "1.1.0" @@ -16763,10 +16763,10 @@ react-select@5.4.0: prop-types "^15.6.0" react-transition-group "^4.3.0" -react-simple-keyboard@^3.4.187: - version "3.7.93" - resolved "https://registry.yarnpkg.com/react-simple-keyboard/-/react-simple-keyboard-3.7.93.tgz#2343be2f96d59ab1f00ce8dcd0ed576eb9f59945" - integrity sha512-MJSwiBOiU0xMjyHfrHVJ6YJkH/TKga4S4DINfqL+MbNYglJ0qMhCyLxorjjlqs744X71/+InV5Dnc8dYK7YMYg== +react-simple-keyboard@^3.7.0: + version "3.7.107" + resolved "https://registry.yarnpkg.com/react-simple-keyboard/-/react-simple-keyboard-3.7.107.tgz#6e71f48950a1923486f2ca8edc5194cdbae0f332" + integrity sha512-r2emrLGoD6A37fl+GCEODFLxtUET1uXZsmFokb7cB6/3OlE7EV08wSzB+yTju+qwIibsc6EXLC6KoRf0FsVC1A== react-snap@^1.23.0: version "1.23.0" From 1175b4fd8714f036eb720053b5e2cb68d931d012 Mon Sep 17 00:00:00 2001 From: Jeremy Leon Date: Mon, 8 Apr 2024 10:17:17 -0400 Subject: [PATCH 061/194] refactor(api): be more permissive with accepting int literals for float parameters (#14820) Allow int literals to be used for float parameters in PAPI add_float arguments and robot server endpoints. --- .../protocol_api/_parameter_context.py | 8 +-- .../protocols/parameters/validation.py | 39 +++++++++++- .../protocol_api/test_parameter_context.py | 15 +++-- .../protocols/parameters/test_validation.py | 62 +++++++++++++++++++ 4 files changed, 115 insertions(+), 9 deletions(-) diff --git a/api/src/opentrons/protocol_api/_parameter_context.py b/api/src/opentrons/protocol_api/_parameter_context.py index e16273b2a33..7773653a9c5 100644 --- a/api/src/opentrons/protocol_api/_parameter_context.py +++ b/api/src/opentrons/protocol_api/_parameter_context.py @@ -91,10 +91,10 @@ def add_float( parameter = parameter_definition.create_float_parameter( display_name=display_name, variable_name=variable_name, - default=default, - minimum=minimum, - maximum=maximum, - choices=choices, + default=validation.ensure_float_value(default), + minimum=validation.ensure_optional_float_value(minimum), + maximum=validation.ensure_optional_float_value(maximum), + choices=validation.ensure_float_choices(choices), description=description, unit=unit, ) diff --git a/api/src/opentrons/protocols/parameters/validation.py b/api/src/opentrons/protocols/parameters/validation.py index 6e5c3b78a9f..166055df504 100644 --- a/api/src/opentrons/protocols/parameters/validation.py +++ b/api/src/opentrons/protocols/parameters/validation.py @@ -61,7 +61,8 @@ def ensure_value_type( This does not guarantee that the value will be the correct type for the given parameter, only that any data coming in is in the format that we expect. For now, the only transformation it is doing is converting integers represented - as floating points to integers, and bools represented as 1.0/0.0 to True/False. + as floating points to integers, and bools represented as 1.0/0.0 to True/False, and floating points represented as + ints to floats. If something is labelled as a type but does not get converted here, that will be caught when it is attempted to be set as the parameter value and will raise the appropriate error there. @@ -72,9 +73,45 @@ def ensure_value_type( validated_value = bool(value) elif parameter_type is int and value.is_integer(): validated_value = int(value) + elif ( + isinstance(value, int) + and not isinstance(value, bool) + and parameter_type is float + ): + validated_value = float(value) return validated_value +def ensure_float_value(value: Union[float, int]) -> float: + """Ensures that if we are expecting a float and receive an int, that will be converted to a float.""" + if not isinstance(value, bool) and isinstance(value, int): + return float(value) + return value + + +def ensure_optional_float_value(value: Optional[Union[float, int]]) -> Optional[float]: + """Ensures that if we are expecting an optional float and receive an int, that will be converted to a float.""" + if not isinstance(value, bool) and isinstance(value, int): + return float(value) + return value + + +def ensure_float_choices( + choices: Optional[List[ParameterChoice]], +) -> Optional[List[ParameterChoice]]: + """Ensures that if we are expecting float parameter choices and any are int types, those will be converted.""" + if choices is not None: + return [ + ParameterChoice( + display_name=choice["display_name"], + # Type ignore because if for some reason this is a str or bool, that will raise in `validate_options` + value=ensure_float_value(choice["value"]), # type: ignore[arg-type] + ) + for choice in choices + ] + return choices + + def convert_type_string_for_enum( parameter_type: type, ) -> Literal["int", "float", "str"]: diff --git a/api/tests/opentrons/protocol_api/test_parameter_context.py b/api/tests/opentrons/protocol_api/test_parameter_context.py index 8b98ae204ca..4d839d72667 100644 --- a/api/tests/opentrons/protocol_api/test_parameter_context.py +++ b/api/tests/opentrons/protocol_api/test_parameter_context.py @@ -77,14 +77,21 @@ def test_add_float(decoy: Decoy, subject: ParameterContext) -> None: """It should create and add a float parameter definition.""" param_def = decoy.mock(cls=mock_parameter_definition.ParameterDefinition) decoy.when(param_def.variable_name).then_return("my cooler variable") + decoy.when(mock_validation.ensure_float_value(12.3)).then_return(3.21) + decoy.when(mock_validation.ensure_optional_float_value(4.5)).then_return(5.4) + decoy.when(mock_validation.ensure_optional_float_value(67.8)).then_return(87.6) + decoy.when( + mock_validation.ensure_float_choices([{"display_name": "foo", "value": 4.2}]) + ).then_return([{"display_name": "bar", "value": 2.4}]) + decoy.when( mock_parameter_definition.create_float_parameter( display_name="abc", variable_name="xyz", - default=12.3, - minimum=4.5, - maximum=67.8, - choices=[{"display_name": "foo", "value": 4.2}], + default=3.21, + minimum=5.4, + maximum=87.6, + choices=[{"display_name": "bar", "value": 2.4}], description="blah blah blah", unit="lux", ) diff --git a/api/tests/opentrons/protocols/parameters/test_validation.py b/api/tests/opentrons/protocols/parameters/test_validation.py index f515da885ed..1f092a51c46 100644 --- a/api/tests/opentrons/protocols/parameters/test_validation.py +++ b/api/tests/opentrons/protocols/parameters/test_validation.py @@ -134,6 +134,7 @@ def test_validate_options_raises_name_error() -> None: [ (1.0, int, 1), (1.1, int, 1.1), + (2, float, 2.0), (2.0, float, 2.0), (2.2, float, 2.2), ("3.0", str, "3.0"), @@ -150,6 +151,67 @@ def test_ensure_value_type( assert result == subject.ensure_value_type(value, param_type) +@pytest.mark.parametrize( + ["value", "result"], + [ + (1, 1.0), + (2.0, 2.0), + (3.3, 3.3), + ], +) +def test_ensure_float_value(value: Union[float, int], result: float) -> None: + """It should ensure that if applicable, the value is coerced into a float.""" + assert result == subject.ensure_float_value(value) + + +@pytest.mark.parametrize( + ["value", "result"], + [ + (1, 1.0), + (2.0, 2.0), + (3.3, 3.3), + (None, None), + ], +) +def test_ensure_optional_float_value(value: Union[float, int], result: float) -> None: + """It should ensure that if applicable, the value is coerced into a float.""" + assert result == subject.ensure_optional_float_value(value) + + +@pytest.mark.parametrize( + ["choices", "result"], + [ + ([], []), + (None, None), + ( + [{"display_name": "foo", "value": 1}], + [{"display_name": "foo", "value": 1.0}], + ), + ( + [{"display_name": "foo", "value": 2.0}], + [{"display_name": "foo", "value": 2.0}], + ), + ( + [{"display_name": "foo", "value": 3.3}], + [{"display_name": "foo", "value": 3.3}], + ), + ( + [{"display_name": "foo", "value": "4"}], + [{"display_name": "foo", "value": "4"}], + ), + ( + [{"display_name": "foo", "value": True}], + [{"display_name": "foo", "value": True}], + ), + ], +) +def test_ensure_float_choices( + choices: Optional[List[ParameterChoice]], result: Optional[List[ParameterChoice]] +) -> None: + """It should ensure that if applicable, the value in a choice is coerced into a float.""" + assert result == subject.ensure_float_choices(choices) + + @pytest.mark.parametrize( ["param_type", "result"], [(int, "int"), (float, "float"), (str, "str")], From 49ab661963f39bfe901a1b59337f1d5d9f7482a0 Mon Sep 17 00:00:00 2001 From: Jethary Rader <66035149+jerader@users.noreply.github.com> Date: Mon, 8 Apr 2024 10:19:36 -0400 Subject: [PATCH 062/194] =?UTF-8?q?feat(protocol-designer):=20createFileWi?= =?UTF-8?q?zard=20now=20accommodates=20MoaM=20for=20F=E2=80=A6=20(#14818)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …lex temp closes AUTH-15 --- .../CreateFileWizard/EquipmentOption.tsx | 196 +++++++++++--- .../modals/CreateFileWizard/InputField.tsx | 5 - .../CreateFileWizard/ModulesAndOtherTile.tsx | 139 +++++++--- .../CreateFileWizard/StagingAreaTile.tsx | 23 +- .../__tests__/EquipmentOption.test.tsx | 21 +- .../__tests__/ModulesAndOtherTile.test.tsx | 6 +- .../CreateFileWizard/__tests__/utils.test.tsx | 242 +++++++++++++++-- .../modals/CreateFileWizard/utils.ts | 254 ++++++++++++------ .../src/localization/en/shared.json | 1 + .../src/localization/en/tooltip.json | 3 +- 10 files changed, 695 insertions(+), 195 deletions(-) diff --git a/protocol-designer/src/components/modals/CreateFileWizard/EquipmentOption.tsx b/protocol-designer/src/components/modals/CreateFileWizard/EquipmentOption.tsx index 97266b07252..76b97572b47 100644 --- a/protocol-designer/src/components/modals/CreateFileWizard/EquipmentOption.tsx +++ b/protocol-designer/src/components/modals/CreateFileWizard/EquipmentOption.tsx @@ -15,48 +15,39 @@ import { TYPOGRAPHY, useHoverTooltip, Tooltip, + DIRECTION_COLUMN, + Box, + StyledText, } from '@opentrons/components' import type { StyleProps } from '@opentrons/components' import type { RobotType } from '@opentrons/shared-data' -const EQUIPMENT_OPTION_STYLE = css` - background-color: ${COLORS.white}; - border-radius: ${BORDERS.borderRadius8}; - border: 1px ${BORDERS.styleSolid} ${COLORS.grey30}; - +const ARROW_STYLE = css` + color: ${COLORS.grey50}; + cursor: pointer; &:hover { - background-color: ${COLORS.grey10}; - border: 1px ${BORDERS.styleSolid} ${COLORS.grey35}; - } - - &:focus { - outline: 2px ${BORDERS.styleSolid} ${COLORS.blue50}; - outline-offset: 3px; + color: ${COLORS.black80}; } ` -const EQUIPMENT_OPTION_SELECTED_STYLE = css` - ${EQUIPMENT_OPTION_STYLE} - background-color: ${COLORS.blue10}; - border: 1px ${BORDERS.styleSolid} ${COLORS.blue50}; - +const ARROW_STYLE_ACTIVE = css` + color: ${COLORS.blue50}; + cursor: pointer; &:hover { - background-color: ${COLORS.blue10}; - border: 1px ${BORDERS.styleSolid} ${COLORS.blue50}; - box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.2); + color: ${COLORS.black80}; } ` -const EQUIPMENT_OPTION_DISABLED_STYLE = css` - ${EQUIPMENT_OPTION_STYLE} - background-color: ${COLORS.white}; - border: 1px ${BORDERS.styleSolid} ${COLORS.grey30}; - - &:hover { - background-color: ${COLORS.white}; - border: 1px ${BORDERS.styleSolid} ${COLORS.grey30}; - } +const ARROW_STYLE_DISABLED = css` + color: ${COLORS.grey50}; ` + +interface MultiplesProps { + numItems: number + maxItems: number + setValue: (num: number) => void + isDisabled: boolean +} interface EquipmentOptionProps extends StyleProps { onClick: React.MouseEventHandler isSelected: boolean @@ -65,6 +56,7 @@ interface EquipmentOptionProps extends StyleProps { image?: React.ReactNode showCheckbox?: boolean disabled?: boolean + multiples?: MultiplesProps } export function EquipmentOption(props: EquipmentOptionProps): JSX.Element { const { @@ -75,10 +67,51 @@ export function EquipmentOption(props: EquipmentOptionProps): JSX.Element { showCheckbox = false, disabled = false, robotType, + multiples, ...styleProps } = props - const { t } = useTranslation('tooltip') - const [targetProps, tooltipProps] = useHoverTooltip() + const { t } = useTranslation(['tooltip', 'shared']) + const [equipmentTargetProps, equipmentTooltipProps] = useHoverTooltip() + const [tempTargetProps, tempTooltipProps] = useHoverTooltip() + const [numMultiples, setNumMultiples] = React.useState(0) + + const EQUIPMENT_OPTION_STYLE = css` + background-color: ${COLORS.white}; + border-radius: ${BORDERS.borderRadius8}; + border: 1px ${BORDERS.styleSolid} ${COLORS.grey30}; + + &:hover { + background-color: ${multiples ? COLORS.white : COLORS.grey10}; + border: 1px ${BORDERS.styleSolid} + ${multiples ? COLORS.grey30 : COLORS.grey35}; + } + + &:focus { + outline: 2px ${BORDERS.styleSolid} ${COLORS.blue50}; + outline-offset: 3px; + } + ` + + const EQUIPMENT_OPTION_SELECTED_STYLE = css` + ${EQUIPMENT_OPTION_STYLE} + background-color: ${COLORS.blue10}; + border: 1px ${BORDERS.styleSolid} ${COLORS.blue50}; + + &:hover { + border: 1px ${BORDERS.styleSolid} ${COLORS.blue50}; + box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.2); + } + ` + + const EQUIPMENT_OPTION_DISABLED_STYLE = css` + ${EQUIPMENT_OPTION_STYLE} + background-color: ${COLORS.white}; + border: 1px ${BORDERS.styleSolid} ${COLORS.grey30}; + + &:hover { + border: 1px ${BORDERS.styleSolid} ${COLORS.grey30}; + } + ` let equipmentOptionStyle if (disabled) { @@ -102,6 +135,66 @@ export function EquipmentOption(props: EquipmentOptionProps): JSX.Element { ) } else if (showCheckbox && disabled) { iconInfo = + } else if (multiples != null) { + const { numItems, maxItems, isDisabled } = multiples + let upArrowStyle = ARROW_STYLE + if (isDisabled || numItems === maxItems) { + upArrowStyle = ARROW_STYLE_DISABLED + } else if (numItems > 0) { + upArrowStyle = ARROW_STYLE_ACTIVE + } + let downArrowStyle = ARROW_STYLE + if (numItems === 0) { + downArrowStyle = ARROW_STYLE_DISABLED + } else if (numItems > 0) { + downArrowStyle = ARROW_STYLE_ACTIVE + } + + iconInfo = ( + + { + multiples.setValue(numMultiples + 1) + setNumMultiples(prevNumMultiples => prevNumMultiples + 1) + } + } + > + + + { + multiples.setValue(numMultiples - 1) + setNumMultiples(prevNumMultiples => prevNumMultiples - 1) + } + } + > + + + {isDisabled || numMultiples === 7 ? ( + + {t('not_enough_space_for_temp')} + + ) : null} + + ) } return ( @@ -117,31 +210,52 @@ export function EquipmentOption(props: EquipmentOptionProps): JSX.Element { : BORDERS.lineBorder } borderRadius={BORDERS.borderRadius8} - cursor={disabled ? 'auto' : 'pointer'} + cursor={disabled || multiples != null ? 'auto' : 'pointer'} backgroundColor={disabled ? COLORS.grey30 : COLORS.transparent} onClick={disabled ? undefined : onClick} {...styleProps} - {...targetProps} + {...equipmentTargetProps} css={equipmentOptionStyle} > {iconInfo} {image} - - {text} - + + + {text} + + {multiples != null ? ( + <> + + + {t('shared:amount')} + {multiples.numItems} + + + ) : null} + {disabled ? ( - + {t( robotType === FLEX_ROBOT_TYPE ? 'disabled_no_space_additional_items' diff --git a/protocol-designer/src/components/modals/CreateFileWizard/InputField.tsx b/protocol-designer/src/components/modals/CreateFileWizard/InputField.tsx index 1140109b303..63a7903907e 100644 --- a/protocol-designer/src/components/modals/CreateFileWizard/InputField.tsx +++ b/protocol-designer/src/components/modals/CreateFileWizard/InputField.tsx @@ -8,7 +8,6 @@ import { COLORS, DIRECTION_COLUMN, Flex, - RESPONSIVENESS, SPACING, TYPOGRAPHY, DISPLAY_INLINE_BLOCK, @@ -60,10 +59,6 @@ function Input(props: InputFieldProps): JSX.Element { border: 1px ${BORDERS.styleSolid} ${error ? COLORS.red50 : COLORS.grey30}; font-size: ${TYPOGRAPHY.fontSizeP}; - @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { - padding: 0; - } - &:active { border: 1px ${BORDERS.styleSolid} ${COLORS.grey50}; } diff --git a/protocol-designer/src/components/modals/CreateFileWizard/ModulesAndOtherTile.tsx b/protocol-designer/src/components/modals/CreateFileWizard/ModulesAndOtherTile.tsx index 492b408ae5f..bcebf6313c3 100644 --- a/protocol-designer/src/components/modals/CreateFileWizard/ModulesAndOtherTile.tsx +++ b/protocol-designer/src/components/modals/CreateFileWizard/ModulesAndOtherTile.tsx @@ -30,11 +30,14 @@ import { getModuleDisplayName, getModuleType, FLEX_ROBOT_TYPE, + THERMOCYCLER_MODULE_TYPE, + MAGNETIC_BLOCK_TYPE, } from '@opentrons/shared-data' import { getIsCrashablePipetteSelected } from '../../../step-forms' import gripperImage from '../../../images/flex_gripper.png' import wasteChuteImage from '../../../images/waste_chute.png' import trashBinImage from '../../../images/flex_trash_bin.png' +import { getEnableMoam } from '../../../feature-flags/selectors' import { uuid } from '../../../utils' import { selectors as featureFlagSelectors } from '../../../feature-flags' import { CrashInfoBox, ModuleDiagram } from '../../modules' @@ -42,7 +45,8 @@ import { ModuleFields } from '../FilePipettesModal/ModuleFields' import { GoBack } from './GoBack' import { getCrashableModuleSelected, - getLastCheckedEquipment, + getDisabledEquipment, + getNextAvailableModuleSlot, getTrashBinOptionDisabled, } from './utils' import { EquipmentOption } from './EquipmentOption' @@ -50,6 +54,8 @@ import { HandleEnter } from './HandleEnter' import type { AdditionalEquipment, WizardTileProps } from './types' +const MAX_TEMPERATURE_MODULES = 7 + export const DEFAULT_SLOT_MAP: { [moduleModel in ModuleModel]?: string } = { [THERMOCYCLER_MODULE_V2]: 'B1', [HEATERSHAKER_MODULE_V1]: 'D1', @@ -186,13 +192,14 @@ export function ModulesAndOtherTile(props: WizardTileProps): JSX.Element { function FlexModuleFields(props: WizardTileProps): JSX.Element { const { watch, setValue } = props + const enableMoamFf = useSelector(getEnableMoam) const modules = watch('modules') const additionalEquipment = watch('additionalEquipment') const moduleTypesOnDeck = modules != null ? Object.values(modules).map(module => module.type) : [] const trashBinDisabled = getTrashBinOptionDisabled({ additionalEquipment, - moduleTypesOnDeck, + modules, }) const handleSetEquipmentOption = (equipment: AdditionalEquipment): void => { @@ -214,6 +221,82 @@ function FlexModuleFields(props: WizardTileProps): JSX.Element { {FLEX_SUPPORTED_MODULE_MODELS.map(moduleModel => { const moduleType = getModuleType(moduleModel) const moduleOnDeck = moduleTypesOnDeck.includes(moduleType) + + let defaultSlot = getNextAvailableModuleSlot( + modules, + additionalEquipment + ) + if (moduleType === THERMOCYCLER_MODULE_TYPE) { + defaultSlot = 'B1' + } else if (moduleType === MAGNETIC_BLOCK_TYPE) { + defaultSlot = 'D2' + } + const isDisabled = getDisabledEquipment({ + additionalEquipment, + modules, + })?.includes(moduleType) + const handleMultiplesClick = (num: number): void => { + const temperatureModules = + modules != null + ? Object.entries(modules).filter( + ([key, module]) => module.type === TEMPERATURE_MODULE_TYPE + ) + : [] + + if (num > temperatureModules.length) { + for (let i = 0; i < num - temperatureModules.length; i++) { + setValue('modules', { + ...modules, + [uuid()]: { + model: moduleModel, + type: moduleType, + slot: getNextAvailableModuleSlot( + modules, + additionalEquipment + ), + }, + }) + } + } else if (num < temperatureModules.length) { + const modulesToRemove = temperatureModules.length - num + for (let i = 0; i < modulesToRemove; i++) { + const lastTempKey = + temperatureModules[temperatureModules.length - 1 - i][0] + // @ts-expect-error: TS can't determine modules's type correctly + const { [lastTempKey]: omit, ...rest } = modules + setValue('modules', rest) + } + } + } + + const handleOnClick = (): void => { + if ( + (moduleType !== TEMPERATURE_MODULE_TYPE && enableMoamFf) || + !enableMoamFf + ) { + if (moduleOnDeck) { + const updatedModules = + modules != null + ? Object.fromEntries( + Object.entries(modules).filter( + ([key, value]) => value.type !== moduleType + ) + ) + : {} + setValue('modules', updatedModules) + } else { + setValue('modules', { + ...modules, + [uuid()]: { + model: moduleModel, + type: moduleType, + slot: defaultSlot, + }, + }) + } + } + } + return ( } text={getModuleDisplayName(moduleModel)} - disabled={ - getLastCheckedEquipment({ - additionalEquipment, - moduleTypesOnDeck, - }) === moduleType + disabled={isDisabled && !moduleOnDeck} + onClick={handleOnClick} + multiples={ + moduleType === TEMPERATURE_MODULE_TYPE && enableMoamFf + ? { + maxItems: MAX_TEMPERATURE_MODULES, + setValue: handleMultiplesClick, + numItems: + modules != null + ? Object.values(modules).filter( + module => module.type === TEMPERATURE_MODULE_TYPE + ).length + : 0, + isDisabled: isDisabled ?? false, + } + : undefined + } + showCheckbox={ + enableMoamFf ? moduleType !== TEMPERATURE_MODULE_TYPE : true } - onClick={() => { - if (moduleOnDeck) { - const updatedModulesByType = - modules != null - ? Object.fromEntries( - Object.entries(modules).filter( - ([key, value]) => value.type !== moduleType - ) - ) - : {} - setValue('modules', updatedModulesByType) - } else { - setValue('modules', { - ...modules, - [uuid()]: { - model: moduleModel, - type: moduleType, - slot: DEFAULT_SLOT_MAP[moduleModel] ?? '', - }, - }) - } - }} - showCheckbox /> ) })} @@ -271,6 +345,11 @@ function FlexModuleFields(props: WizardTileProps): JSX.Element { robotType={FLEX_ROBOT_TYPE} onClick={() => handleSetEquipmentOption('wasteChute')} isSelected={additionalEquipment.includes('wasteChute')} + disabled={ + modules != null + ? Object.values(modules).some(module => module.slot === 'D3') + : false + } image={ ({ + cutoutId, + cutoutFixtureId: SINGLE_RIGHT_SLOT_FIXTURE, + }) +) + export function StagingAreaTile(props: WizardTileProps): JSX.Element | null { const { getValues, goBack, proceed, setValue, watch } = props const { t } = useTranslation(['modal', 'application']) const { fields, pipettesByMount } = getValues() const additionalEquipment = watch('additionalEquipment') + const modules = watch('modules') const isOt2 = fields.robotType === OT2_ROBOT_TYPE const stagingAreaItems = additionalEquipment.filter(equipment => // TODO(bc, 11/14/2023): refactor the additional items field to include a cutoutId // and a cutoutFixtureId so that we don't have to string parse here to generate them equipment.includes('stagingArea') ) + const unoccupiedStagingAreaSlots = getUnoccupiedStagingAreaSlots(modules) const savedStagingAreaSlots: DeckConfiguration = stagingAreaItems.flatMap( item => { @@ -49,14 +59,7 @@ export function StagingAreaTile(props: WizardTileProps): JSX.Element | null { } ) - const STANDARD_EMPTY_SLOTS: DeckConfiguration = STAGING_AREA_CUTOUTS.map( - cutoutId => ({ - cutoutId, - cutoutFixtureId: SINGLE_RIGHT_SLOT_FIXTURE, - }) - ) - - STANDARD_EMPTY_SLOTS.forEach(emptySlot => { + unoccupiedStagingAreaSlots.forEach(emptySlot => { if ( !savedStagingAreaSlots.some( ({ cutoutId }) => cutoutId === emptySlot.cutoutId @@ -67,7 +70,9 @@ export function StagingAreaTile(props: WizardTileProps): JSX.Element | null { }) const initialSlots = - stagingAreaItems.length > 0 ? savedStagingAreaSlots : STANDARD_EMPTY_SLOTS + stagingAreaItems.length > 0 + ? savedStagingAreaSlots + : unoccupiedStagingAreaSlots const [updatedSlots, setUpdatedSlots] = React.useState( initialSlots diff --git a/protocol-designer/src/components/modals/CreateFileWizard/__tests__/EquipmentOption.test.tsx b/protocol-designer/src/components/modals/CreateFileWizard/__tests__/EquipmentOption.test.tsx index 09128361135..c83b1e99404 100644 --- a/protocol-designer/src/components/modals/CreateFileWizard/__tests__/EquipmentOption.test.tsx +++ b/protocol-designer/src/components/modals/CreateFileWizard/__tests__/EquipmentOption.test.tsx @@ -1,7 +1,7 @@ import * as React from 'react' import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' import '@testing-library/jest-dom/vitest' -import { screen, cleanup } from '@testing-library/react' +import { screen, cleanup, fireEvent } from '@testing-library/react' import { BORDERS, COLORS } from '@opentrons/components' import { FLEX_ROBOT_TYPE } from '@opentrons/shared-data' import { i18n } from '../../../../localization' @@ -39,7 +39,7 @@ describe('EquipmentOption', () => { } render(props) expect(screen.getByLabelText('EquipmentOption_flex_mockText')).toHaveStyle( - `background-color: ${COLORS.white}` + `background-color: ${COLORS.grey10}` ) }) it('renders the equipment option without check not selected and image', () => { @@ -73,4 +73,21 @@ describe('EquipmentOption', () => { `border: ${BORDERS.activeLineBorder}` ) }) + it('renders the equipment option with multiples allowed', () => { + props = { + ...props, + multiples: { + numItems: 1, + maxItems: 4, + setValue: vi.fn(), + isDisabled: false, + }, + } + render(props) + screen.getByText('Amount:') + screen.getByText('1') + fireEvent.click(screen.getByTestId('EquipmentOption_upArrow')) + expect(props.multiples?.setValue).toHaveBeenCalled() + screen.getByTestId('EquipmentOption_downArrow') + }) }) diff --git a/protocol-designer/src/components/modals/CreateFileWizard/__tests__/ModulesAndOtherTile.test.tsx b/protocol-designer/src/components/modals/CreateFileWizard/__tests__/ModulesAndOtherTile.test.tsx index ba9924ee13e..63da7f3ed30 100644 --- a/protocol-designer/src/components/modals/CreateFileWizard/__tests__/ModulesAndOtherTile.test.tsx +++ b/protocol-designer/src/components/modals/CreateFileWizard/__tests__/ModulesAndOtherTile.test.tsx @@ -5,7 +5,10 @@ import { fireEvent, screen, cleanup } from '@testing-library/react' import { FLEX_ROBOT_TYPE, OT2_ROBOT_TYPE } from '@opentrons/shared-data' import { renderWithProviders } from '../../../../__testing-utils__' import { i18n } from '../../../../localization' -import { getDisableModuleRestrictions } from '../../../../feature-flags/selectors' +import { + getDisableModuleRestrictions, + getEnableMoam, +} from '../../../../feature-flags/selectors' import { CrashInfoBox } from '../../../modules' import { ModuleFields } from '../../FilePipettesModal/ModuleFields' import { ModulesAndOtherTile } from '../ModulesAndOtherTile' @@ -58,6 +61,7 @@ describe('ModulesAndOtherTile', () => { ...props, ...mockWizardTileProps, } as WizardTileProps + vi.mocked(getEnableMoam).mockReturnValue(true) vi.mocked(CrashInfoBox).mockReturnValue(
mock CrashInfoBox
) vi.mocked(EquipmentOption).mockReturnValue(
mock EquipmentOption
) vi.mocked(getDisableModuleRestrictions).mockReturnValue(false) diff --git a/protocol-designer/src/components/modals/CreateFileWizard/__tests__/utils.test.tsx b/protocol-designer/src/components/modals/CreateFileWizard/__tests__/utils.test.tsx index 213f3466c0e..240120c8b92 100644 --- a/protocol-designer/src/components/modals/CreateFileWizard/__tests__/utils.test.tsx +++ b/protocol-designer/src/components/modals/CreateFileWizard/__tests__/utils.test.tsx @@ -1,17 +1,22 @@ +import { it, describe, expect } from 'vitest' import { FLEX_ROBOT_TYPE, HEATERSHAKER_MODULE_TYPE, + SINGLE_RIGHT_SLOT_FIXTURE, TEMPERATURE_MODULE_TYPE, THERMOCYCLER_MODULE_TYPE, } from '@opentrons/shared-data' -import { it, describe, expect } from 'vitest' import { FLEX_TRASH_DEFAULT_SLOT, - getLastCheckedEquipment, + getUnoccupiedStagingAreaSlots, getTrashSlot, + getNextAvailableModuleSlot, + getDisabledEquipment, + getTrashBinOptionDisabled, } from '../utils' +import { STANDARD_EMPTY_SLOTS } from '../StagingAreaTile' import type { FormPipettesByMount } from '../../../../step-forms' -import type { AdditionalEquipment, FormState } from '../types' +import type { FormState } from '../types' let MOCK_FORM_STATE = { fields: { @@ -28,43 +33,169 @@ let MOCK_FORM_STATE = { additionalEquipment: [], } as FormState -describe('getLastCheckedEquipment', () => { - it('should return null when there is no trash bin', () => { - const result = getLastCheckedEquipment({ - additionalEquipment: [], - moduleTypesOnDeck: [], +describe('getUnoccupiedStagingAreaSlots', () => { + it('should return all staging area slots when there are no modules', () => { + const result = getUnoccupiedStagingAreaSlots(null) + expect(result).toStrictEqual(STANDARD_EMPTY_SLOTS) + }) + it('should return one staging area slot when there are modules in the way of the other slots', () => { + const result = getUnoccupiedStagingAreaSlots({ + 0: { model: 'magneticBlockV1', type: 'magneticBlockType', slot: 'A3' }, + 1: { + model: 'temperatureModuleV2', + type: 'temperatureModuleType', + slot: 'B3', + }, + 2: { + model: 'temperatureModuleV2', + type: 'temperatureModuleType', + slot: 'C3', + }, }) - expect(result).toBe(null) + expect(result).toStrictEqual([ + { cutoutId: 'cutoutD3', cutoutFixtureId: SINGLE_RIGHT_SLOT_FIXTURE }, + ]) }) - it('should return null if not all the modules or staging areas are selected', () => { - const LastCheckedProps = { - additionalEquipment: [ + describe('getNextAvailableModuleSlot', () => { + it('should return D1 when there are no modules or staging areas', () => { + const result = getNextAvailableModuleSlot(null, []) + expect(result).toStrictEqual('D1') + }) + it('should return a C3 when all the modules are on the deck', () => { + const result = getNextAvailableModuleSlot( + { + 0: { + model: 'magneticBlockV1', + type: 'magneticBlockType', + slot: 'D1', + }, + 1: { + model: 'thermocyclerModuleV2', + type: 'thermocyclerModuleType', + slot: 'B1', + }, + 2: { + model: 'temperatureModuleV2', + type: 'temperatureModuleType', + slot: 'C1', + }, + }, + [] + ) + expect(result).toStrictEqual('C3') + }) + }) + it('should return an empty string when all the modules and staging area slots are on the deck without TC', () => { + const result = getNextAvailableModuleSlot( + { + 0: { + model: 'heaterShakerModuleV1', + type: 'heaterShakerModuleType', + slot: 'D1', + }, + 1: { + model: 'temperatureModuleV2', + type: 'temperatureModuleType', + slot: 'C1', + }, + 2: { + model: 'temperatureModuleV2', + type: 'temperatureModuleType', + slot: 'B1', + }, + }, + [ + 'stagingArea_cutoutA3', + 'stagingArea_cutoutB3', + 'stagingArea_cutoutC3', + 'stagingArea_cutoutD3', 'trashBin', + ] + ) + expect(result).toStrictEqual('') + }) + it('should return an empty string when all the modules and staging area slots are on the deck with TC', () => { + const result = getNextAvailableModuleSlot( + { + 0: { + model: 'heaterShakerModuleV1', + type: 'heaterShakerModuleType', + slot: 'D1', + }, + 1: { + model: 'thermocyclerModuleV2', + type: 'thermocyclerModuleType', + slot: 'B1', + }, + }, + [ + 'stagingArea_cutoutA3', + 'stagingArea_cutoutB3', + 'stagingArea_cutoutC3', 'stagingArea_cutoutD3', - ] as AdditionalEquipment[], - moduleTypesOnDeck: [THERMOCYCLER_MODULE_TYPE], - } - const result = getLastCheckedEquipment(LastCheckedProps) - expect(result).toBe(null) + 'trashBin', + ] + ) + expect(result).toStrictEqual('') + }) +}) +describe('getNextAvailableModuleSlot', () => { + it('should return nothing as disabled', () => { + const result = getDisabledEquipment({ + additionalEquipment: [], + modules: null, + }) + expect(result).toStrictEqual([]) }) - it('should return temperature module if other modules and staging areas are selected', () => { - const LastCheckedProps = { + it('should return the TC as disabled', () => { + const result = getDisabledEquipment({ + additionalEquipment: [], + modules: { + 0: { + model: 'heaterShakerModuleV1', + type: 'heaterShakerModuleType', + slot: 'A1', + }, + }, + }) + expect(result).toStrictEqual([THERMOCYCLER_MODULE_TYPE]) + }) + it('should return all module types if there is no available slot', () => { + const result = getDisabledEquipment({ additionalEquipment: [ - 'trashBin', 'stagingArea_cutoutA3', 'stagingArea_cutoutB3', 'stagingArea_cutoutC3', 'stagingArea_cutoutD3', - ] as AdditionalEquipment[], - moduleTypesOnDeck: [THERMOCYCLER_MODULE_TYPE, HEATERSHAKER_MODULE_TYPE], - } - const result = getLastCheckedEquipment(LastCheckedProps) - expect(result).toBe(TEMPERATURE_MODULE_TYPE) + 'trashBin', + ], + modules: { + 0: { + model: 'heaterShakerModuleV1', + type: 'heaterShakerModuleType', + slot: 'D1', + }, + 1: { + model: 'temperatureModuleV2', + type: 'temperatureModuleType', + slot: 'C1', + }, + 2: { + model: 'temperatureModuleV2', + type: 'temperatureModuleType', + slot: 'B1', + }, + }, + }) + expect(result).toStrictEqual([ + THERMOCYCLER_MODULE_TYPE, + TEMPERATURE_MODULE_TYPE, + HEATERSHAKER_MODULE_TYPE, + ]) }) }) - describe('getTrashSlot', () => { - it('should return the default slot A3 when there is no staging area in that slot', () => { + it('should return the default slot A3 when there is no staging area or module in that slot', () => { MOCK_FORM_STATE = { ...MOCK_FORM_STATE, additionalEquipment: ['trashBin'], @@ -72,7 +203,7 @@ describe('getTrashSlot', () => { const result = getTrashSlot(MOCK_FORM_STATE) expect(result).toBe(FLEX_TRASH_DEFAULT_SLOT) }) - it('should return cutoutB3 when there is a staging area in slot A3', () => { + it('should return cutoutA1 when there is a staging area in slot A3', () => { MOCK_FORM_STATE = { ...MOCK_FORM_STATE, additionalEquipment: ['stagingArea_cutoutA3'], @@ -80,4 +211,59 @@ describe('getTrashSlot', () => { const result = getTrashSlot(MOCK_FORM_STATE) expect(result).toBe('cutoutA1') }) + describe('getTrashBinOptionDisabled', () => { + it('returns false when there is a trash bin already', () => { + const result = getTrashBinOptionDisabled({ + additionalEquipment: ['trashBin'], + modules: { + 0: { + model: 'heaterShakerModuleV1', + type: 'heaterShakerModuleType', + slot: 'D1', + }, + }, + }) + expect(result).toBe(false) + }) + it('returns false when there is an available slot', () => { + const result = getTrashBinOptionDisabled({ + additionalEquipment: ['trashBin'], + modules: null, + }) + expect(result).toBe(false) + }) + it('returns true when there is no available slot and trash bin is not selected yet', () => { + const result = getTrashBinOptionDisabled({ + additionalEquipment: [ + 'stagingArea_cutoutA3', + 'stagingArea_cutoutB3', + 'stagingArea_cutoutC3', + 'stagingArea_cutoutD3', + ], + modules: { + 0: { + model: 'heaterShakerModuleV1', + type: 'heaterShakerModuleType', + slot: 'D1', + }, + 1: { + model: 'temperatureModuleV2', + type: 'temperatureModuleType', + slot: 'C1', + }, + 2: { + model: 'temperatureModuleV2', + type: 'temperatureModuleType', + slot: 'B1', + }, + 3: { + model: 'temperatureModuleV2', + type: 'temperatureModuleType', + slot: 'A1', + }, + }, + }) + expect(result).toBe(true) + }) + }) }) diff --git a/protocol-designer/src/components/modals/CreateFileWizard/utils.ts b/protocol-designer/src/components/modals/CreateFileWizard/utils.ts index 989dabe2839..2e0e8d54a72 100644 --- a/protocol-designer/src/components/modals/CreateFileWizard/utils.ts +++ b/protocol-designer/src/components/modals/CreateFileWizard/utils.ts @@ -1,62 +1,52 @@ import { - getModuleType, HEATERSHAKER_MODULE_TYPE, TEMPERATURE_MODULE_TYPE, THERMOCYCLER_MODULE_TYPE, + WASTE_CHUTE_CUTOUT, } from '@opentrons/shared-data' import { isModuleWithCollisionIssue } from '../../modules' -import { - FLEX_SUPPORTED_MODULE_MODELS, - DEFAULT_SLOT_MAP, -} from './ModulesAndOtherTile' +import { STANDARD_EMPTY_SLOTS } from './StagingAreaTile' -import type { ModuleType } from '@opentrons/shared-data' +import type { DeckConfiguration, ModuleType } from '@opentrons/shared-data' import type { FormModules } from '../../../step-forms' import type { AdditionalEquipment, FormState } from './types' export const FLEX_TRASH_DEFAULT_SLOT = 'cutoutA3' -const ALL_STAGING_AREAS = 4 - -interface LastCheckedProps { - additionalEquipment: AdditionalEquipment[] - moduleTypesOnDeck: ModuleType[] -} - -export const getLastCheckedEquipment = ( - props: LastCheckedProps -): string | null => { - const { additionalEquipment, moduleTypesOnDeck } = props - const hasAllStagingAreas = - additionalEquipment.filter(equipment => equipment.includes('stagingArea')) - .length === ALL_STAGING_AREAS - const hasTrashBin = additionalEquipment.includes('trashBin') - if (!hasTrashBin || !hasAllStagingAreas) { - return null - } - - if ( - moduleTypesOnDeck.includes(THERMOCYCLER_MODULE_TYPE) && - moduleTypesOnDeck.includes(HEATERSHAKER_MODULE_TYPE) - ) { - return TEMPERATURE_MODULE_TYPE - } - - if ( - moduleTypesOnDeck.includes(HEATERSHAKER_MODULE_TYPE) && - moduleTypesOnDeck.includes(TEMPERATURE_MODULE_TYPE) - ) { - return THERMOCYCLER_MODULE_TYPE - } - - if ( - moduleTypesOnDeck.includes(THERMOCYCLER_MODULE_TYPE) && - moduleTypesOnDeck.includes(TEMPERATURE_MODULE_TYPE) - ) { - return HEATERSHAKER_MODULE_TYPE - } - return null -} +const MODULES_SLOTS_FLEX = [ + { + value: 'cutoutD1', + slot: 'D1', + }, + { + value: 'cutoutC3', + slot: 'C3', + }, + { + value: 'cutoutB1', + slot: 'B1', + }, + { + value: 'cutoutB3', + slot: 'B3', + }, + { + value: 'cutoutA3', + slot: 'A3', + }, + { + value: 'cutoutD3', + slot: 'D3', + }, + { + value: 'cutoutC1', + slot: 'C1', + }, + { + value: 'cutoutA1', + slot: 'A1', + }, +] export const getCrashableModuleSelected = ( modules: FormModules | null, @@ -75,20 +65,6 @@ export const getCrashableModuleSelected = ( return crashableModuleOnDeck } -export const getTrashBinOptionDisabled = (props: LastCheckedProps): boolean => { - const { additionalEquipment, moduleTypesOnDeck } = props - const allStagingAreasInUse = - additionalEquipment.filter(equipment => equipment.includes('stagingArea')) - .length === ALL_STAGING_AREAS - - const allModulesInSideSlotsOnDeck = - moduleTypesOnDeck.includes(HEATERSHAKER_MODULE_TYPE) && - moduleTypesOnDeck.includes(TEMPERATURE_MODULE_TYPE) && - moduleTypesOnDeck.includes(THERMOCYCLER_MODULE_TYPE) - - return allStagingAreasInUse && allModulesInSideSlotsOnDeck -} - export const MOVABLE_TRASH_CUTOUTS = [ { value: 'cutoutA1', @@ -124,37 +100,159 @@ export const MOVABLE_TRASH_CUTOUTS = [ }, ] +export const getUnoccupiedStagingAreaSlots = ( + modules: FormState['modules'] +): DeckConfiguration => { + let unoccupiedSlots = STANDARD_EMPTY_SLOTS + const moduleCutoutIds = + modules != null + ? Object.values(modules).flatMap(module => + module.type === THERMOCYCLER_MODULE_TYPE + ? [`cutout${module.slot}`, 'cutoutA1'] + : `cutout${module.slot}` + ) + : [] + + unoccupiedSlots = unoccupiedSlots.filter(emptySlot => { + return !moduleCutoutIds.includes(emptySlot.cutoutId) + }) + + return unoccupiedSlots +} + +export const getNextAvailableModuleSlot = ( + modules: FormState['modules'], + additionalEquipment: FormState['additionalEquipment'] +): string => { + const moduleSlots = + modules != null + ? Object.values(modules).flatMap(module => + module.type === THERMOCYCLER_MODULE_TYPE + ? [module.slot, 'A1'] + : module.slot + ) + : [] + const stagingAreas = additionalEquipment.filter(equipment => + equipment.includes('stagingArea') + ) + const stagingAreaCutouts = stagingAreas.map(cutout => cutout.split('_')[1]) + const hasWasteChute = additionalEquipment.find(equipment => + equipment.includes('wasteChute') + ) + const wasteChuteSlot = Boolean(hasWasteChute) + ? [WASTE_CHUTE_CUTOUT as string] + : [] + const trashBin = additionalEquipment.find(equipment => + equipment.includes('trashBin') + ) + const hasTC = + modules != null + ? Object.values(modules).some( + module => module.type === THERMOCYCLER_MODULE_TYPE + ) + : false + + // removing slot(s) for the trash if spaces are limited + let removeSlotForTrash = MODULES_SLOTS_FLEX + if (trashBin != null && hasTC) { + removeSlotForTrash = MODULES_SLOTS_FLEX.slice(0, -2) + } else if (trashBin != null && !hasTC) { + removeSlotForTrash = MODULES_SLOTS_FLEX.slice(0, -1) + } + const unoccupiedSlot = removeSlotForTrash.find( + cutout => + !stagingAreaCutouts.includes(cutout.value) && + !moduleSlots.includes(cutout.slot) && + !wasteChuteSlot.includes(cutout.value) + ) + if (unoccupiedSlot == null) { + return '' + } + + return unoccupiedSlot?.slot ?? '' +} + +interface DisabledEquipmentProps { + additionalEquipment: AdditionalEquipment[] + modules: FormModules | null +} + +export const getDisabledEquipment = ( + props: DisabledEquipmentProps +): string[] => { + const { additionalEquipment, modules } = props + const nextAvailableSlot = getNextAvailableModuleSlot( + modules, + additionalEquipment + ) + const disabledEquipment: string[] = [] + + const moduleSlots = + modules != null + ? Object.values(modules).flatMap(module => + module.type === THERMOCYCLER_MODULE_TYPE + ? [module.slot, 'A1'] + : module.slot + ) + : [] + + if (moduleSlots.includes('A1') || moduleSlots.includes('B1')) { + disabledEquipment.push(THERMOCYCLER_MODULE_TYPE) + } + if (nextAvailableSlot === '') { + disabledEquipment.push(TEMPERATURE_MODULE_TYPE, HEATERSHAKER_MODULE_TYPE) + } + + return disabledEquipment +} + +export const getTrashBinOptionDisabled = ( + props: DisabledEquipmentProps +): boolean => { + const { additionalEquipment, modules } = props + const nextAvailableSlot = getNextAvailableModuleSlot( + modules, + additionalEquipment + ) + const hasTrashBinAlready = additionalEquipment.includes('trashBin') + return nextAvailableSlot === '' && !hasTrashBinAlready +} + export const getTrashSlot = (values: FormState): string => { const { additionalEquipment, modules } = values - const moduleTypesOnDeck = - modules != null ? Object.values(modules).map(module => module.type) : [] + const moduleSlots = + modules != null + ? Object.values(modules).flatMap(module => + module.type === THERMOCYCLER_MODULE_TYPE + ? [module.slot, 'A1'] + : module.slot + ) + : [] const stagingAreas = additionalEquipment.filter(equipment => equipment.includes('stagingArea') ) // TODO(Jr, 11/16/23): refactor additionalEquipment to store cutouts // so the split isn't needed const cutouts = stagingAreas.map(cutout => cutout.split('_')[1]) + const hasWasteChute = additionalEquipment.find(equipment => + equipment.includes('wasteChute') + ) + const wasteChuteSlot = Boolean(hasWasteChute) + ? [WASTE_CHUTE_CUTOUT as string] + : [] - if (!cutouts.includes(FLEX_TRASH_DEFAULT_SLOT)) { + if ( + !cutouts.includes(FLEX_TRASH_DEFAULT_SLOT) && + !moduleSlots.includes('A3') + ) { return FLEX_TRASH_DEFAULT_SLOT } - const moduleSlots: string[] = FLEX_SUPPORTED_MODULE_MODELS.reduce( - (slots: string[], model) => { - const moduleType = getModuleType(model) - if (moduleTypesOnDeck.includes(moduleType)) { - const slot = String(DEFAULT_SLOT_MAP[model]) - return moduleType === THERMOCYCLER_MODULE_TYPE - ? [...slots, 'A1', slot] - : [...slots, slot] - } - return slots - }, - [] - ) const unoccupiedSlot = MOVABLE_TRASH_CUTOUTS.find( cutout => - !cutouts.includes(cutout.value) && !moduleSlots.includes(cutout.slot) + !cutouts.includes(cutout.value) && + !moduleSlots.includes(cutout.slot) && + !wasteChuteSlot.includes(cutout.value) ) if (unoccupiedSlot == null) { console.error( diff --git a/protocol-designer/src/localization/en/shared.json b/protocol-designer/src/localization/en/shared.json index d69d55ffe32..89d916bce35 100644 --- a/protocol-designer/src/localization/en/shared.json +++ b/protocol-designer/src/localization/en/shared.json @@ -1,5 +1,6 @@ { "add": "add", + "amount": "Amount:", "confirm_reorder": "Are you sure you want to reorder these steps, it may cause errors?", "edit": "edit", "exit": "exit", diff --git a/protocol-designer/src/localization/en/tooltip.json b/protocol-designer/src/localization/en/tooltip.json index 8e41f7c1382..7ef580d81ce 100644 --- a/protocol-designer/src/localization/en/tooltip.json +++ b/protocol-designer/src/localization/en/tooltip.json @@ -4,8 +4,9 @@ "disabled_cannot_delete_trash": "A Trash Bin or Waste Chute is required", "disabled_off_deck": "Off-deck labware cannot be modified unless on starting deck state.", "disabled_step_creation": "New steps cannot be added in Batch Edit mode.", - "disabled_no_space_additional_items": "No space for this combination of staging area slots and modules.", + "disabled_no_space_additional_items": "No space for this combination of staging area slots, trash, and modules.", "disabled_you_can_add_one_type": "Only one module of each type is allowed on the deck at a time", + "not_enough_space_for_temp": "There is not enough space on the deck to add more temperature modules", "not_in_beta": "ⓘ Coming Soon", "step_description": { From f69c2cbf94a08042f6593eae2686b875f35afd29 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 8 Apr 2024 10:31:12 -0400 Subject: [PATCH 063/194] fix(app-testing): snapshot failure capture (#14813) This PR is an automated snapshot update request. Please review the changes and merge if they are acceptable or find you bug and fix it. Co-authored-by: y3rsh --- ...66d05][OT2_P20S_None_2_7_Walkthrough].json | 2 +- ...P300M_P20S_TC_HS_TM_2_13_SmokeTestV3].json | 2 +- ...3][OT2_P300S_Thermocycler_Moam_Error].json | 2 +- ...nalysisError_ModuleInStagingAreaCol3].json | 2 +- ...or_HeaterShakerConflictWithTrashBin2].json | 2 +- ...P300M_P20S_MM_TM_TC1_5_2_6_PD40Error].json | 4 +- ...alysisError_GripperCollisionWithTips].json | 310 - ...nalysisError_ModuleInStagingAreaCol4].json | 2 +- ...M_P20S_MM_HS_TD_TC_6_1_AllMods_Error].json | 25 - ...rror_TrashBinAndThermocyclerConflict].json | 2 +- ...0SRight_None_6_1_SimpleTransferError].json | 33 - ...OT2_None_None_2_13_PythonSyntaxError].json | 2 +- ...ne_2_16_AnalysisError_TrashBinInCol2].json | 2 +- ...82e960][OT2_P300MLeft_MM_TM_2_4_Zymo].json | 2 +- ...lysisError_TrashBinInStagingAreaCol4].json | 2 +- ...isError_MagneticModuleInFlexProtocol].json | 2 +- ...e_TM_2_16_AnalysisError_ModuleInCol2].json | 2 +- ...AnalysisError_AccessToFixedTrashProp].json | 2 +- ...or_HeaterShakerConflictWithTrashBin1].json | 2 +- ...P300M_P20S_TC_HS_TM_2_17_SmokeTestV3].json | 15651 +++++++++++++++- 20 files changed, 15661 insertions(+), 392 deletions(-) diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0f71566d05][OT2_P20S_None_2_7_Walkthrough].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0f71566d05][OT2_P20S_None_2_7_Walkthrough].json index 8564dda276d..e52cb9863b1 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0f71566d05][OT2_P20S_None_2_7_Walkthrough].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0f71566d05][OT2_P20S_None_2_7_Walkthrough].json @@ -3293,7 +3293,7 @@ "errorInfo": { "args": "()", "class": "AssertionError", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/protocol_engine.py\", line 442, in finish\n await exit_stack.aclose()\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 656, in aclose\n await self.__aexit__(None, None, None)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 714, in __aexit__\n raise exc_details[1]\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 697, in __aexit__\n cb_suppress = await cb(*exc_details)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 608, in _exit_wrapper\n await callback(*args, **kwds)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 102, in stop\n await p.teardown()\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 112, in teardown\n await self._action_dispatching_task\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 160, in _dispatch_all_actions\n self.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 37, in dispatch\n return self._action_dispatcher.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/actions/action_dispatcher.py\", line 30, in dispatch\n self._sink.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/state.py\", line 207, in handle_action\n substore.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/commands.py\", line 243, in handle_action\n self._state.command_history.set_command_running(running_command)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/command_history.py\", line 175, in set_command_running\n assert self.get_running_command() is None\n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/protocol_engine.py\", line 442, in finish\n await exit_stack.aclose()\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 656, in aclose\n await self.__aexit__(None, None, None)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 714, in __aexit__\n raise exc_details[1]\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 697, in __aexit__\n cb_suppress = await cb(*exc_details)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 608, in _exit_wrapper\n await callback(*args, **kwds)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 102, in stop\n await p.teardown()\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 112, in teardown\n await self._action_dispatching_task\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 160, in _dispatch_all_actions\n self.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 37, in dispatch\n return self._action_dispatcher.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/actions/action_dispatcher.py\", line 30, in dispatch\n self._sink.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/state.py\", line 207, in handle_action\n substore.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/commands.py\", line 257, in handle_action\n self._state.command_history.set_command_running(running_command)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/command_history.py\", line 175, in set_command_running\n assert self.get_running_command() is None\n" }, "errorType": "PythonException", "wrappedErrors": [] diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[20cefcac62][OT2_P300M_P20S_TC_HS_TM_2_13_SmokeTestV3].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[20cefcac62][OT2_P300M_P20S_TC_HS_TM_2_13_SmokeTestV3].json index ddce1f10c7f..2f1e2018f18 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[20cefcac62][OT2_P300M_P20S_TC_HS_TM_2_13_SmokeTestV3].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[20cefcac62][OT2_P300M_P20S_TC_HS_TM_2_13_SmokeTestV3].json @@ -11889,7 +11889,7 @@ "errorInfo": { "args": "()", "class": "AssertionError", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/protocol_engine.py\", line 442, in finish\n await exit_stack.aclose()\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 656, in aclose\n await self.__aexit__(None, None, None)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 714, in __aexit__\n raise exc_details[1]\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 697, in __aexit__\n cb_suppress = await cb(*exc_details)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 608, in _exit_wrapper\n await callback(*args, **kwds)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 102, in stop\n await p.teardown()\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 112, in teardown\n await self._action_dispatching_task\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 160, in _dispatch_all_actions\n self.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 37, in dispatch\n return self._action_dispatcher.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/actions/action_dispatcher.py\", line 30, in dispatch\n self._sink.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/state.py\", line 207, in handle_action\n substore.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/commands.py\", line 243, in handle_action\n self._state.command_history.set_command_running(running_command)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/command_history.py\", line 175, in set_command_running\n assert self.get_running_command() is None\n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/protocol_engine.py\", line 442, in finish\n await exit_stack.aclose()\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 656, in aclose\n await self.__aexit__(None, None, None)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 714, in __aexit__\n raise exc_details[1]\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 697, in __aexit__\n cb_suppress = await cb(*exc_details)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 608, in _exit_wrapper\n await callback(*args, **kwds)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 102, in stop\n await p.teardown()\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 112, in teardown\n await self._action_dispatching_task\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 160, in _dispatch_all_actions\n self.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 37, in dispatch\n return self._action_dispatcher.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/actions/action_dispatcher.py\", line 30, in dispatch\n self._sink.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/state.py\", line 207, in handle_action\n substore.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/commands.py\", line 257, in handle_action\n self._state.command_history.set_command_running(running_command)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/command_history.py\", line 175, in set_command_running\n assert self.get_running_command() is None\n" }, "errorType": "PythonException", "wrappedErrors": [] diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[240b279ac3][OT2_P300S_Thermocycler_Moam_Error].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[240b279ac3][OT2_P300S_Thermocycler_Moam_Error].json index 0581fee8962..35ec253ed42 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[240b279ac3][OT2_P300S_Thermocycler_Moam_Error].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[240b279ac3][OT2_P300S_Thermocycler_Moam_Error].json @@ -2680,7 +2680,7 @@ "errorInfo": { "args": "('thermocyclerModuleV2 in slot 7 prevents thermocyclerModuleV1 from using slot 7.',)", "class": "DeckConflictError", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 124, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"OT2_P300S_Thermocycler_Moam_Error.py\", line 19, 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 810, in load_module\n module_core = self._core.load_module(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/legacy/legacy_protocol_core.py\", line 333, in load_module\n self._deck_layout[resolved_location] = geometry\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/legacy/deck.py\", line 186, in __setitem__\n deck_conflict.check(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/motion_planning/deck_conflict.py\", line 210, in check\n raise DeckConflictError(\n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 124, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"OT2_P300S_Thermocycler_Moam_Error.py\", line 19, 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 814, in load_module\n module_core = self._core.load_module(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/legacy/legacy_protocol_core.py\", line 333, in load_module\n self._deck_layout[resolved_location] = geometry\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/legacy/deck.py\", line 186, in __setitem__\n deck_conflict.check(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/motion_planning/deck_conflict.py\", line 210, in check\n raise DeckConflictError(\n" }, "errorType": "PythonException", "wrappedErrors": [] 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 bf492fe0746..74cf05cce32 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 @@ -567,7 +567,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 124, 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 810, 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 424, 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 203, in check\n wrapped_deck_conflict.check(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/motion_planning/deck_conflict.py\", line 223, in check\n raise DeckConflictError(\n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 124, 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 814, 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 424, 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 203, in check\n wrapped_deck_conflict.check(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/motion_planning/deck_conflict.py\", line 223, in check\n raise DeckConflictError(\n" }, "errorType": "PythonException", "wrappedErrors": [] diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[3b1bfd0d2d][OT2_None_None_HS_2_16_AnalysisError_HeaterShakerConflictWithTrashBin2].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[3b1bfd0d2d][OT2_None_None_HS_2_16_AnalysisError_HeaterShakerConflictWithTrashBin2].json index 6a28756037c..a8091a65bdd 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[3b1bfd0d2d][OT2_None_None_HS_2_16_AnalysisError_HeaterShakerConflictWithTrashBin2].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[3b1bfd0d2d][OT2_None_None_HS_2_16_AnalysisError_HeaterShakerConflictWithTrashBin2].json @@ -478,7 +478,7 @@ "errorInfo": { "args": "('trash bin in slot 12 prevents heaterShakerModuleV1 from using slot 9.',)", "class": "DeckConflictError", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 124, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"OT2_None_None_HS_2_16_AnalysisError_HeaterShakerConflictWithTrashBin2.py\", line 11, 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 810, 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 424, 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 203, in check\n wrapped_deck_conflict.check(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/motion_planning/deck_conflict.py\", line 210, in check\n raise DeckConflictError(\n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 124, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"OT2_None_None_HS_2_16_AnalysisError_HeaterShakerConflictWithTrashBin2.py\", line 11, 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 814, 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 424, 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 203, in check\n wrapped_deck_conflict.check(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/motion_planning/deck_conflict.py\", line 210, in check\n raise DeckConflictError(\n" }, "errorType": "PythonException", "wrappedErrors": [] 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 1c888cd46cc..636e3ae1cbc 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 @@ -6924,7 +6924,7 @@ "errorInfo": { "args": "('Cannot aspirate more than pipette max volume',)", "class": "AssertionError", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/commands/publisher.py\", line 113, in publish_context\n yield\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/instrument_context.py\", line 270, 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/legacy_commands/publisher.py\", line 113, in publish_context\n yield\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/instrument_context.py\", line 270, 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": [] @@ -6965,7 +6965,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 84, in _run\n await self._run_func()\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/task_queue.py\", line 61, in _do_run\n await func(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/protocol_runner.py\", line 218, in run_func\n await self._legacy_executor.execute(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_wrappers.py\", line 180, in execute\n await to_thread.run_sync(\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 63, 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 270, 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 84, in _run\n await self._run_func()\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/task_queue.py\", line 61, in _do_run\n await func(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/protocol_runner.py\", line 219, in run_func\n await self._legacy_executor.execute(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_wrappers.py\", line 180, in execute\n await to_thread.run_sync(\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 63, 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 270, 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[4e17da0b57][Flex_P1000_96_Gripper_TC_TM_HS_AnalysisError_GripperCollisionWithTips].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4e17da0b57][Flex_P1000_96_Gripper_TC_TM_HS_AnalysisError_GripperCollisionWithTips].json index 5bd1dec9c82..babe140d830 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4e17da0b57][Flex_P1000_96_Gripper_TC_TM_HS_AnalysisError_GripperCollisionWithTips].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4e17da0b57][Flex_P1000_96_Gripper_TC_TM_HS_AnalysisError_GripperCollisionWithTips].json @@ -12252,316 +12252,6 @@ "strategy": "usingGripper" }, "status": "failed" - }, - { - "commandType": "moveLabware", - "params": { - "newLocation": {}, - "strategy": "usingGripper" - }, - "status": "failed" - }, - { - "commandType": "waitForDuration", - "params": { - "message": "", - "seconds": 60.0 - }, - "status": "failed" - }, - { - "commandType": "temperatureModule/deactivate", - "params": {}, - "status": "failed" - }, - { - "commandType": "heaterShaker/deactivateHeater", - "params": {}, - "status": "failed" - }, - { - "commandType": "heaterShaker/openLabwareLatch", - "params": {}, - "status": "failed" - }, - { - "commandType": "moveLabware", - "params": { - "newLocation": {}, - "strategy": "usingGripper" - }, - "status": "failed" - }, - { - "commandType": "heaterShaker/closeLabwareLatch", - "params": {}, - "status": "failed" - }, - { - "commandType": "heaterShaker/setTargetTemperature", - "params": { - "celsius": 40.0 - }, - "status": "failed" - }, - { - "commandType": "heaterShaker/setAndWaitForShakeSpeed", - "params": { - "rpm": 1000.0 - }, - "status": "failed" - }, - { - "commandType": "thermocycler/openLid", - "params": {}, - "status": "failed" - }, - { - "commandType": "thermocycler/deactivateBlock", - "params": {}, - "status": "failed" - }, - { - "commandType": "thermocycler/deactivateLid", - "params": {}, - "status": "failed" - }, - { - "commandType": "moveLabware", - "params": { - "newLocation": { - "slotName": "B2" - }, - "strategy": "usingGripper" - }, - "status": "failed" - }, - { - "commandType": "waitForDuration", - "params": { - "message": "", - "seconds": 60.0 - }, - "status": "failed" - }, - { - "commandType": "heaterShaker/deactivateHeater", - "params": {}, - "status": "failed" - }, - { - "commandType": "heaterShaker/deactivateShaker", - "params": {}, - "status": "failed" - }, - { - "commandType": "heaterShaker/openLabwareLatch", - "params": {}, - "status": "failed" - }, - { - "commandType": "moveLabware", - "params": { - "newLocation": { - "slotName": "D2" - }, - "strategy": "usingGripper" - }, - "status": "failed" - }, - { - "commandType": "aspirate", - "params": { - "flowRate": 6.0, - "volume": 150.0, - "wellLocation": { - "offset": { - "x": 0, - "y": 0, - "z": 0.5 - }, - "origin": "bottom" - }, - "wellName": "A1" - }, - "status": "failed" - }, - { - "commandType": "dispense", - "params": { - "flowRate": 6.0, - "volume": 150.0, - "wellLocation": { - "offset": { - "x": 0, - "y": 0, - "z": 0.5 - }, - "origin": "bottom" - }, - "wellName": "A1" - }, - "status": "failed" - }, - { - "commandType": "aspirate", - "params": { - "flowRate": 6.0, - "volume": 150.0, - "wellLocation": { - "offset": { - "x": 0, - "y": 0, - "z": 0.5 - }, - "origin": "bottom" - }, - "wellName": "A1" - }, - "status": "failed" - }, - { - "commandType": "dispense", - "params": { - "flowRate": 6.0, - "volume": 150.0, - "wellLocation": { - "offset": { - "x": 0, - "y": 0, - "z": 0.5 - }, - "origin": "bottom" - }, - "wellName": "A1" - }, - "status": "failed" - }, - { - "commandType": "aspirate", - "params": { - "flowRate": 6.0, - "volume": 50.0, - "wellLocation": { - "offset": { - "x": 0, - "y": 0, - "z": 1.0 - }, - "origin": "bottom" - }, - "wellName": "A1" - }, - "status": "failed" - }, - { - "commandType": "moveToAddressableArea", - "params": { - "addressableAreaName": "movableTrashA3", - "forceDirect": false, - "offset": { - "x": 0.0, - "y": 0.0, - "z": 0.0 - }, - "stayAtHighestPossibleZ": false - }, - "status": "failed" - }, - { - "commandType": "dispenseInPlace", - "params": { - "flowRate": 6.0, - "volume": 50.0 - }, - "status": "failed" - }, - { - "commandType": "moveToAddressableArea", - "params": { - "addressableAreaName": "movableTrashA3", - "forceDirect": false, - "offset": { - "x": 0.0, - "y": 0.0, - "z": 0.0 - }, - "stayAtHighestPossibleZ": false - }, - "status": "failed" - }, - { - "commandType": "dropTipInPlace", - "params": {}, - "status": "failed" - }, - { - "commandType": "pickUpTip", - "params": { - "wellLocation": { - "offset": { - "x": 0, - "y": 0, - "z": 0 - }, - "origin": "top" - }, - "wellName": "A1" - }, - "status": "failed" - }, - { - "commandType": "aspirate", - "params": { - "flowRate": 6.0, - "volume": 50.0, - "wellLocation": { - "offset": { - "x": 0, - "y": 0, - "z": 1.0 - }, - "origin": "bottom" - }, - "wellName": "A1" - }, - "status": "failed" - }, - { - "commandType": "dispense", - "params": { - "flowRate": 6.0, - "volume": 50.0, - "wellLocation": { - "offset": { - "x": 0, - "y": 0, - "z": 0.5 - }, - "origin": "bottom" - }, - "wellName": "A1" - }, - "status": "failed" - }, - { - "commandType": "moveToAddressableArea", - "params": { - "addressableAreaName": "movableTrashA3", - "forceDirect": false, - "offset": { - "x": 0.0, - "y": 0.0, - "z": 0.0 - }, - "stayAtHighestPossibleZ": false - }, - "status": "failed" - }, - { - "commandType": "dropTipInPlace", - "params": {}, - "status": "failed" } ], "config": { diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[512a897a47][Flex_None_None_TM_2_16_AnalysisError_ModuleInStagingAreaCol4].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[512a897a47][Flex_None_None_TM_2_16_AnalysisError_ModuleInStagingAreaCol4].json index 3fcada17001..ce2f5357e41 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[512a897a47][Flex_None_None_TM_2_16_AnalysisError_ModuleInStagingAreaCol4].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[512a897a47][Flex_None_None_TM_2_16_AnalysisError_ModuleInStagingAreaCol4].json @@ -28,7 +28,7 @@ "errorInfo": { "args": "('Cannot load a module onto a staging slot.',)", "class": "ValueError", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 124, 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_ModuleInStagingAreaCol4.py\", line 15, 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 808, in load_module\n raise ValueError(\"Cannot load a module onto a staging slot.\")\n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 124, 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_ModuleInStagingAreaCol4.py\", line 15, 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 812, in load_module\n raise ValueError(\"Cannot load a module onto a staging slot.\")\n" }, "errorType": "PythonException", "wrappedErrors": [] diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[52a42597a5][OT2_P300M_P20S_MM_HS_TD_TC_6_1_AllMods_Error].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[52a42597a5][OT2_P300M_P20S_MM_HS_TD_TC_6_1_AllMods_Error].json index 6dfd0ab19b6..b36a44a5457 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[52a42597a5][OT2_P300M_P20S_MM_HS_TD_TC_6_1_AllMods_Error].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[52a42597a5][OT2_P300M_P20S_MM_HS_TD_TC_6_1_AllMods_Error].json @@ -7280,31 +7280,6 @@ "notes": [], "params": {}, "status": "failed" - }, - { - "commandType": "heaterShaker/deactivateHeater", - "params": {}, - "status": "failed" - }, - { - "commandType": "heaterShaker/deactivateShaker", - "params": {}, - "status": "failed" - }, - { - "commandType": "thermocycler/openLid", - "params": {}, - "status": "failed" - }, - { - "commandType": "thermocycler/deactivateBlock", - "params": {}, - "status": "failed" - }, - { - "commandType": "thermocycler/deactivateLid", - "params": {}, - "status": "failed" } ], "config": { diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[5931902632][Flex_None_None_TC_2_16_AnalysisError_TrashBinAndThermocyclerConflict].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[5931902632][Flex_None_None_TC_2_16_AnalysisError_TrashBinAndThermocyclerConflict].json index 9ac5392e5ff..65d49f5fb6b 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[5931902632][Flex_None_None_TC_2_16_AnalysisError_TrashBinAndThermocyclerConflict].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[5931902632][Flex_None_None_TC_2_16_AnalysisError_TrashBinAndThermocyclerConflict].json @@ -137,7 +137,7 @@ "errorInfo": { "args": "('thermocyclerModuleV2 in slot B1 prevents trash bin from using slot A1.',)", "class": "DeckConflictError", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 124, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"Flex_None_None_TC_2_16_AnalysisError_TrashBinAndThermocyclerConflict.py\", line 13, 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 514, in load_trash_bin\n trash_bin = self._core.load_trash_bin(slot_name, addressable_area_name)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/protocol.py\", line 529, in load_trash_bin\n self._add_disposal_location_to_engine(trash_bin)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/protocol.py\", line 148, in _add_disposal_location_to_engine\n deck_conflict.check(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/deck_conflict.py\", line 203, in check\n wrapped_deck_conflict.check(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/motion_planning/deck_conflict.py\", line 210, in check\n raise DeckConflictError(\n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 124, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"Flex_None_None_TC_2_16_AnalysisError_TrashBinAndThermocyclerConflict.py\", line 13, 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 518, in load_trash_bin\n trash_bin = self._core.load_trash_bin(slot_name, addressable_area_name)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/protocol.py\", line 529, in load_trash_bin\n self._add_disposal_location_to_engine(trash_bin)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/protocol.py\", line 148, in _add_disposal_location_to_engine\n deck_conflict.check(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/deck_conflict.py\", line 203, in check\n wrapped_deck_conflict.check(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/motion_planning/deck_conflict.py\", line 210, in check\n raise DeckConflictError(\n" }, "errorType": "PythonException", "wrappedErrors": [] diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[5fc4f3adbc][OT2_P20SRight_None_6_1_SimpleTransferError].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[5fc4f3adbc][OT2_P20SRight_None_6_1_SimpleTransferError].json index 3e3b00c26a8..a5d379ba794 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[5fc4f3adbc][OT2_P20SRight_None_6_1_SimpleTransferError].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[5fc4f3adbc][OT2_P20SRight_None_6_1_SimpleTransferError].json @@ -1718,39 +1718,6 @@ "wellName": "A1" }, "status": "failed" - }, - { - "commandType": "dispense", - "params": { - "flowRate": 3.78, - "volume": 10.0, - "wellLocation": { - "offset": { - "x": 0, - "y": 0, - "z": 0.5 - }, - "origin": "bottom" - }, - "wellName": "B1" - }, - "status": "failed" - }, - { - "commandType": "dropTip", - "params": { - "alternateDropLocation": false, - "wellLocation": { - "offset": { - "x": 0, - "y": 0, - "z": 0 - }, - "origin": "default" - }, - "wellName": "A1" - }, - "status": "failed" } ], "config": { 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 aab8caadd15..af560dfb9f3 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 @@ -31,7 +31,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 84, in _run\n await self._run_func()\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/task_queue.py\", line 61, in _do_run\n await func(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/protocol_runner.py\", line 218, in run_func\n await self._legacy_executor.execute(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_wrappers.py\", line 180, in execute\n await to_thread.run_sync(\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 40, in run_protocol\n run_python(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 95, 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 84, in _run\n await self._run_func()\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/task_queue.py\", line 61, in _do_run\n await func(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/protocol_runner.py\", line 219, in run_func\n await self._legacy_executor.execute(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_wrappers.py\", line 180, in execute\n await to_thread.run_sync(\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 40, in run_protocol\n run_python(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 95, 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[7be98bf838][Flex_None_None_2_16_AnalysisError_TrashBinInCol2].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7be98bf838][Flex_None_None_2_16_AnalysisError_TrashBinInCol2].json index 031b3816aa9..bd95551628d 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7be98bf838][Flex_None_None_2_16_AnalysisError_TrashBinInCol2].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7be98bf838][Flex_None_None_2_16_AnalysisError_TrashBinInCol2].json @@ -28,7 +28,7 @@ "errorInfo": { "args": "('Invalid location for trash bin: C2.\\nValid slots: Any slot in column 1 or 3.',)", "class": "InvalidTrashBinLocationError", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 124, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"Flex_None_None_2_16_AnalysisError_TrashBinInCol2.py\", line 15, 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 509, in load_trash_bin\n addressable_area_name = validation.ensure_and_convert_trash_bin_location(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/validation.py\", line 327, in ensure_and_convert_trash_bin_location\n raise InvalidTrashBinLocationError(\n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 124, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"Flex_None_None_2_16_AnalysisError_TrashBinInCol2.py\", line 15, 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 513, in load_trash_bin\n addressable_area_name = validation.ensure_and_convert_trash_bin_location(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/validation.py\", line 327, in ensure_and_convert_trash_bin_location\n raise InvalidTrashBinLocationError(\n" }, "errorType": "PythonException", "wrappedErrors": [] diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8df082e960][OT2_P300MLeft_MM_TM_2_4_Zymo].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8df082e960][OT2_P300MLeft_MM_TM_2_4_Zymo].json index b32d3d55f65..a2af41a1a02 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8df082e960][OT2_P300MLeft_MM_TM_2_4_Zymo].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8df082e960][OT2_P300MLeft_MM_TM_2_4_Zymo].json @@ -10913,7 +10913,7 @@ "errorInfo": { "args": "()", "class": "AssertionError", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/protocol_engine.py\", line 442, in finish\n await exit_stack.aclose()\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 656, in aclose\n await self.__aexit__(None, None, None)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 714, in __aexit__\n raise exc_details[1]\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 697, in __aexit__\n cb_suppress = await cb(*exc_details)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 608, in _exit_wrapper\n await callback(*args, **kwds)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 102, in stop\n await p.teardown()\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 112, in teardown\n await self._action_dispatching_task\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 160, in _dispatch_all_actions\n self.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 37, in dispatch\n return self._action_dispatcher.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/actions/action_dispatcher.py\", line 30, in dispatch\n self._sink.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/state.py\", line 207, in handle_action\n substore.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/commands.py\", line 243, in handle_action\n self._state.command_history.set_command_running(running_command)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/command_history.py\", line 175, in set_command_running\n assert self.get_running_command() is None\n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/protocol_engine.py\", line 442, in finish\n await exit_stack.aclose()\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 656, in aclose\n await self.__aexit__(None, None, None)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 714, in __aexit__\n raise exc_details[1]\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 697, in __aexit__\n cb_suppress = await cb(*exc_details)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 608, in _exit_wrapper\n await callback(*args, **kwds)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 102, in stop\n await p.teardown()\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 112, in teardown\n await self._action_dispatching_task\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 160, in _dispatch_all_actions\n self.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 37, in dispatch\n return self._action_dispatcher.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/actions/action_dispatcher.py\", line 30, in dispatch\n self._sink.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/state.py\", line 207, in handle_action\n substore.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/commands.py\", line 257, in handle_action\n self._state.command_history.set_command_running(running_command)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/command_history.py\", line 175, in set_command_running\n assert self.get_running_command() is None\n" }, "errorType": "PythonException", "wrappedErrors": [] diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ac35bb394d][Flex_None_None_2_16_AnalysisError_TrashBinInStagingAreaCol4].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ac35bb394d][Flex_None_None_2_16_AnalysisError_TrashBinInStagingAreaCol4].json index ba5644090a2..4beea85705a 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ac35bb394d][Flex_None_None_2_16_AnalysisError_TrashBinInStagingAreaCol4].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ac35bb394d][Flex_None_None_2_16_AnalysisError_TrashBinInStagingAreaCol4].json @@ -28,7 +28,7 @@ "errorInfo": { "args": "('Staging areas not permitted for trash bin.',)", "class": "ValueError", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 124, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"Flex_None_None_2_16_AnalysisError_TrashBinInStagingAreaCol4.py\", line 15, 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 508, in load_trash_bin\n raise ValueError(\"Staging areas not permitted for trash bin.\")\n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 124, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"Flex_None_None_2_16_AnalysisError_TrashBinInStagingAreaCol4.py\", line 15, 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 512, in load_trash_bin\n raise ValueError(\"Staging areas not permitted for trash bin.\")\n" }, "errorType": "PythonException", "wrappedErrors": [] diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[cda954ef1e][Flex_None_None_MM_2_16_AnalysisError_MagneticModuleInFlexProtocol].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[cda954ef1e][Flex_None_None_MM_2_16_AnalysisError_MagneticModuleInFlexProtocol].json index fbcb54a5e13..d88dc1e3bc9 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[cda954ef1e][Flex_None_None_MM_2_16_AnalysisError_MagneticModuleInFlexProtocol].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[cda954ef1e][Flex_None_None_MM_2_16_AnalysisError_MagneticModuleInFlexProtocol].json @@ -28,7 +28,7 @@ "errorInfo": { "args": "('A magneticModuleType cannot be loaded into slot C1',)", "class": "ValueError", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 124, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"Flex_None_None_MM_2_16_AnalysisError_MagneticModuleInFlexProtocol.py\", line 15, 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 810, 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 412, in load_module\n self._ensure_module_location(normalized_deck_slot, module_type)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/protocol.py\", line 631, in _ensure_module_location\n raise ValueError(f\"A {module_type.value} cannot be loaded into slot {slot}\")\n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 124, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"Flex_None_None_MM_2_16_AnalysisError_MagneticModuleInFlexProtocol.py\", line 15, 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 814, 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 412, in load_module\n self._ensure_module_location(normalized_deck_slot, module_type)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/protocol.py\", line 631, in _ensure_module_location\n raise ValueError(f\"A {module_type.value} cannot be loaded into slot {slot}\")\n" }, "errorType": "PythonException", "wrappedErrors": [] diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ce0f35b3c6][Flex_None_None_TM_2_16_AnalysisError_ModuleInCol2].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ce0f35b3c6][Flex_None_None_TM_2_16_AnalysisError_ModuleInCol2].json index e1ab5bc6247..9f85e6ecdcb 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ce0f35b3c6][Flex_None_None_TM_2_16_AnalysisError_ModuleInCol2].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ce0f35b3c6][Flex_None_None_TM_2_16_AnalysisError_ModuleInCol2].json @@ -28,7 +28,7 @@ "errorInfo": { "args": "('A temperatureModuleType cannot be loaded into slot C2',)", "class": "ValueError", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 124, 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_ModuleInCol2.py\", line 15, 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 810, 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 412, in load_module\n self._ensure_module_location(normalized_deck_slot, module_type)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/protocol.py\", line 631, in _ensure_module_location\n raise ValueError(f\"A {module_type.value} cannot be loaded into slot {slot}\")\n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 124, 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_ModuleInCol2.py\", line 15, 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 814, 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 412, in load_module\n self._ensure_module_location(normalized_deck_slot, module_type)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/protocol.py\", line 631, in _ensure_module_location\n raise ValueError(f\"A {module_type.value} cannot be loaded into slot {slot}\")\n" }, "errorType": "PythonException", "wrappedErrors": [] diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[dc8ac87114][Flex_None_None_2_16_AnalysisError_AccessToFixedTrashProp].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[dc8ac87114][Flex_None_None_2_16_AnalysisError_AccessToFixedTrashProp].json index 57381580c07..257a29f5a73 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[dc8ac87114][Flex_None_None_2_16_AnalysisError_AccessToFixedTrashProp].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[dc8ac87114][Flex_None_None_2_16_AnalysisError_AccessToFixedTrashProp].json @@ -28,7 +28,7 @@ "errorInfo": { "args": "('Fixed Trash is not supported on Flex protocols in API Version 2.16 and above.',)", "class": "APIVersionError", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 124, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"Flex_None_None_2_16_AnalysisError_AccessToFixedTrashProp.py\", line 15, 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 1114, in fixed_trash\n raise APIVersionError(\n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 124, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"Flex_None_None_2_16_AnalysisError_AccessToFixedTrashProp.py\", line 15, 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 1118, in fixed_trash\n raise APIVersionError(\n" }, "errorType": "PythonException", "wrappedErrors": [] diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e49dae5293][OT2_None_None_HS_2_16_AnalysisError_HeaterShakerConflictWithTrashBin1].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e49dae5293][OT2_None_None_HS_2_16_AnalysisError_HeaterShakerConflictWithTrashBin1].json index 4cf6892135d..7c7138566d7 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e49dae5293][OT2_None_None_HS_2_16_AnalysisError_HeaterShakerConflictWithTrashBin1].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e49dae5293][OT2_None_None_HS_2_16_AnalysisError_HeaterShakerConflictWithTrashBin1].json @@ -478,7 +478,7 @@ "errorInfo": { "args": "('trash bin in slot 12 prevents heaterShakerModuleV1 from using slot 11.',)", "class": "DeckConflictError", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 124, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"OT2_None_None_HS_2_16_AnalysisError_HeaterShakerConflictWithTrashBin1.py\", line 11, 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 810, 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 424, 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 203, in check\n wrapped_deck_conflict.check(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/motion_planning/deck_conflict.py\", line 223, in check\n raise DeckConflictError(\n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 124, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"OT2_None_None_HS_2_16_AnalysisError_HeaterShakerConflictWithTrashBin1.py\", line 11, 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 814, 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 424, 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 203, in check\n wrapped_deck_conflict.check(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/motion_planning/deck_conflict.py\", line 223, in check\n raise DeckConflictError(\n" }, "errorType": "PythonException", "wrappedErrors": [] diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f91ecb541c][OT2_P300M_P20S_TC_HS_TM_2_17_SmokeTestV3].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f91ecb541c][OT2_P300M_P20S_TC_HS_TM_2_17_SmokeTestV3].json index 1912a8a3d55..0245a572ca9 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f91ecb541c][OT2_P300M_P20S_TC_HS_TM_2_17_SmokeTestV3].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f91ecb541c][OT2_P300M_P20S_TC_HS_TM_2_17_SmokeTestV3].json @@ -6,12 +6,15543 @@ "params": {}, "result": {}, "status": "succeeded" + }, + { + "commandType": "setRailLights", + "notes": [], + "params": { + "on": true + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "custom", + "notes": [], + "params": { + "legacyCommandText": "Let there be light! True 🌠🌠🌠", + "legacyCommandType": "command.COMMENT" + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "custom", + "notes": [], + "params": { + "legacyCommandText": "Is the door is closed? True 🚪🚪🚪", + "legacyCommandType": "command.COMMENT" + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "custom", + "notes": [], + "params": { + "legacyCommandText": "Is this a simulation? True 🔮🔮🔮", + "legacyCommandType": "command.COMMENT" + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "custom", + "notes": [], + "params": { + "legacyCommandText": "Running against API Version: 2.17", + "legacyCommandType": "command.COMMENT" + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "loadLabware", + "notes": [], + "params": { + "displayName": "300ul tips", + "loadName": "opentrons_96_tiprack_300ul", + "location": { + "slotName": "5" + }, + "namespace": "opentrons", + "version": 1 + }, + "result": { + "definition": { + "allowedRoles": [], + "brand": { + "brand": "Opentrons", + "brandId": [], + "links": [ + "https://shop.opentrons.com/collections/opentrons-tips/products/opentrons-300ul-tips" + ] + }, + "cornerOffsetFromSlot": { + "x": 0, + "y": 0, + "z": 0 + }, + "dimensions": { + "xDimension": 127.76, + "yDimension": 85.48, + "zDimension": 64.49 + }, + "gripperOffsets": {}, + "groups": [ + { + "metadata": {}, + "wells": [ + "A1", + "A10", + "A11", + "A12", + "A2", + "A3", + "A4", + "A5", + "A6", + "A7", + "A8", + "A9", + "B1", + "B10", + "B11", + "B12", + "B2", + "B3", + "B4", + "B5", + "B6", + "B7", + "B8", + "B9", + "C1", + "C10", + "C11", + "C12", + "C2", + "C3", + "C4", + "C5", + "C6", + "C7", + "C8", + "C9", + "D1", + "D10", + "D11", + "D12", + "D2", + "D3", + "D4", + "D5", + "D6", + "D7", + "D8", + "D9", + "E1", + "E10", + "E11", + "E12", + "E2", + "E3", + "E4", + "E5", + "E6", + "E7", + "E8", + "E9", + "F1", + "F10", + "F11", + "F12", + "F2", + "F3", + "F4", + "F5", + "F6", + "F7", + "F8", + "F9", + "G1", + "G10", + "G11", + "G12", + "G2", + "G3", + "G4", + "G5", + "G6", + "G7", + "G8", + "G9", + "H1", + "H10", + "H11", + "H12", + "H2", + "H3", + "H4", + "H5", + "H6", + "H7", + "H8", + "H9" + ] + } + ], + "metadata": { + "displayCategory": "tipRack", + "displayName": "Opentrons OT-2 96 Tip Rack 300 µL", + "displayVolumeUnits": "µL", + "tags": [] + }, + "namespace": "opentrons", + "ordering": [ + [ + "A1", + "B1", + "C1", + "D1", + "E1", + "F1", + "G1", + "H1" + ], + [ + "A10", + "B10", + "C10", + "D10", + "E10", + "F10", + "G10", + "H10" + ], + [ + "A11", + "B11", + "C11", + "D11", + "E11", + "F11", + "G11", + "H11" + ], + [ + "A12", + "B12", + "C12", + "D12", + "E12", + "F12", + "G12", + "H12" + ], + [ + "A2", + "B2", + "C2", + "D2", + "E2", + "F2", + "G2", + "H2" + ], + [ + "A3", + "B3", + "C3", + "D3", + "E3", + "F3", + "G3", + "H3" + ], + [ + "A4", + "B4", + "C4", + "D4", + "E4", + "F4", + "G4", + "H4" + ], + [ + "A5", + "B5", + "C5", + "D5", + "E5", + "F5", + "G5", + "H5" + ], + [ + "A6", + "B6", + "C6", + "D6", + "E6", + "F6", + "G6", + "H6" + ], + [ + "A7", + "B7", + "C7", + "D7", + "E7", + "F7", + "G7", + "H7" + ], + [ + "A8", + "B8", + "C8", + "D8", + "E8", + "F8", + "G8", + "H8" + ], + [ + "A9", + "B9", + "C9", + "D9", + "E9", + "F9", + "G9", + "H9" + ] + ], + "parameters": { + "format": "96Standard", + "isMagneticModuleCompatible": false, + "isTiprack": true, + "loadName": "opentrons_96_tiprack_300ul", + "tipLength": 59.3, + "tipOverlap": 7.47 + }, + "schemaVersion": 2, + "stackingOffsetWithLabware": {}, + "stackingOffsetWithModule": {}, + "version": 1, + "wells": { + "A1": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 14.38, + "y": 74.24, + "z": 5.39 + }, + "A10": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 95.38, + "y": 74.24, + "z": 5.39 + }, + "A11": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 104.38, + "y": 74.24, + "z": 5.39 + }, + "A12": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 113.38, + "y": 74.24, + "z": 5.39 + }, + "A2": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 23.38, + "y": 74.24, + "z": 5.39 + }, + "A3": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 32.38, + "y": 74.24, + "z": 5.39 + }, + "A4": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 41.38, + "y": 74.24, + "z": 5.39 + }, + "A5": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 50.38, + "y": 74.24, + "z": 5.39 + }, + "A6": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 59.38, + "y": 74.24, + "z": 5.39 + }, + "A7": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 68.38, + "y": 74.24, + "z": 5.39 + }, + "A8": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 77.38, + "y": 74.24, + "z": 5.39 + }, + "A9": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 86.38, + "y": 74.24, + "z": 5.39 + }, + "B1": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 14.38, + "y": 65.24, + "z": 5.39 + }, + "B10": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 95.38, + "y": 65.24, + "z": 5.39 + }, + "B11": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 104.38, + "y": 65.24, + "z": 5.39 + }, + "B12": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 113.38, + "y": 65.24, + "z": 5.39 + }, + "B2": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 23.38, + "y": 65.24, + "z": 5.39 + }, + "B3": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 32.38, + "y": 65.24, + "z": 5.39 + }, + "B4": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 41.38, + "y": 65.24, + "z": 5.39 + }, + "B5": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 50.38, + "y": 65.24, + "z": 5.39 + }, + "B6": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 59.38, + "y": 65.24, + "z": 5.39 + }, + "B7": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 68.38, + "y": 65.24, + "z": 5.39 + }, + "B8": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 77.38, + "y": 65.24, + "z": 5.39 + }, + "B9": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 86.38, + "y": 65.24, + "z": 5.39 + }, + "C1": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 14.38, + "y": 56.24, + "z": 5.39 + }, + "C10": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 95.38, + "y": 56.24, + "z": 5.39 + }, + "C11": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 104.38, + "y": 56.24, + "z": 5.39 + }, + "C12": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 113.38, + "y": 56.24, + "z": 5.39 + }, + "C2": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 23.38, + "y": 56.24, + "z": 5.39 + }, + "C3": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 32.38, + "y": 56.24, + "z": 5.39 + }, + "C4": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 41.38, + "y": 56.24, + "z": 5.39 + }, + "C5": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 50.38, + "y": 56.24, + "z": 5.39 + }, + "C6": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 59.38, + "y": 56.24, + "z": 5.39 + }, + "C7": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 68.38, + "y": 56.24, + "z": 5.39 + }, + "C8": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 77.38, + "y": 56.24, + "z": 5.39 + }, + "C9": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 86.38, + "y": 56.24, + "z": 5.39 + }, + "D1": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 14.38, + "y": 47.24, + "z": 5.39 + }, + "D10": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 95.38, + "y": 47.24, + "z": 5.39 + }, + "D11": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 104.38, + "y": 47.24, + "z": 5.39 + }, + "D12": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 113.38, + "y": 47.24, + "z": 5.39 + }, + "D2": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 23.38, + "y": 47.24, + "z": 5.39 + }, + "D3": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 32.38, + "y": 47.24, + "z": 5.39 + }, + "D4": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 41.38, + "y": 47.24, + "z": 5.39 + }, + "D5": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 50.38, + "y": 47.24, + "z": 5.39 + }, + "D6": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 59.38, + "y": 47.24, + "z": 5.39 + }, + "D7": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 68.38, + "y": 47.24, + "z": 5.39 + }, + "D8": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 77.38, + "y": 47.24, + "z": 5.39 + }, + "D9": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 86.38, + "y": 47.24, + "z": 5.39 + }, + "E1": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 14.38, + "y": 38.24, + "z": 5.39 + }, + "E10": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 95.38, + "y": 38.24, + "z": 5.39 + }, + "E11": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 104.38, + "y": 38.24, + "z": 5.39 + }, + "E12": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 113.38, + "y": 38.24, + "z": 5.39 + }, + "E2": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 23.38, + "y": 38.24, + "z": 5.39 + }, + "E3": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 32.38, + "y": 38.24, + "z": 5.39 + }, + "E4": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 41.38, + "y": 38.24, + "z": 5.39 + }, + "E5": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 50.38, + "y": 38.24, + "z": 5.39 + }, + "E6": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 59.38, + "y": 38.24, + "z": 5.39 + }, + "E7": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 68.38, + "y": 38.24, + "z": 5.39 + }, + "E8": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 77.38, + "y": 38.24, + "z": 5.39 + }, + "E9": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 86.38, + "y": 38.24, + "z": 5.39 + }, + "F1": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 14.38, + "y": 29.24, + "z": 5.39 + }, + "F10": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 95.38, + "y": 29.24, + "z": 5.39 + }, + "F11": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 104.38, + "y": 29.24, + "z": 5.39 + }, + "F12": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 113.38, + "y": 29.24, + "z": 5.39 + }, + "F2": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 23.38, + "y": 29.24, + "z": 5.39 + }, + "F3": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 32.38, + "y": 29.24, + "z": 5.39 + }, + "F4": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 41.38, + "y": 29.24, + "z": 5.39 + }, + "F5": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 50.38, + "y": 29.24, + "z": 5.39 + }, + "F6": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 59.38, + "y": 29.24, + "z": 5.39 + }, + "F7": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 68.38, + "y": 29.24, + "z": 5.39 + }, + "F8": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 77.38, + "y": 29.24, + "z": 5.39 + }, + "F9": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 86.38, + "y": 29.24, + "z": 5.39 + }, + "G1": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 14.38, + "y": 20.24, + "z": 5.39 + }, + "G10": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 95.38, + "y": 20.24, + "z": 5.39 + }, + "G11": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 104.38, + "y": 20.24, + "z": 5.39 + }, + "G12": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 113.38, + "y": 20.24, + "z": 5.39 + }, + "G2": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 23.38, + "y": 20.24, + "z": 5.39 + }, + "G3": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 32.38, + "y": 20.24, + "z": 5.39 + }, + "G4": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 41.38, + "y": 20.24, + "z": 5.39 + }, + "G5": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 50.38, + "y": 20.24, + "z": 5.39 + }, + "G6": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 59.38, + "y": 20.24, + "z": 5.39 + }, + "G7": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 68.38, + "y": 20.24, + "z": 5.39 + }, + "G8": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 77.38, + "y": 20.24, + "z": 5.39 + }, + "G9": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 86.38, + "y": 20.24, + "z": 5.39 + }, + "H1": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 14.38, + "y": 11.24, + "z": 5.39 + }, + "H10": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 95.38, + "y": 11.24, + "z": 5.39 + }, + "H11": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 104.38, + "y": 11.24, + "z": 5.39 + }, + "H12": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 113.38, + "y": 11.24, + "z": 5.39 + }, + "H2": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 23.38, + "y": 11.24, + "z": 5.39 + }, + "H3": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 32.38, + "y": 11.24, + "z": 5.39 + }, + "H4": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 41.38, + "y": 11.24, + "z": 5.39 + }, + "H5": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 50.38, + "y": 11.24, + "z": 5.39 + }, + "H6": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 59.38, + "y": 11.24, + "z": 5.39 + }, + "H7": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 68.38, + "y": 11.24, + "z": 5.39 + }, + "H8": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 77.38, + "y": 11.24, + "z": 5.39 + }, + "H9": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 86.38, + "y": 11.24, + "z": 5.39 + } + } + } + }, + "status": "succeeded" + }, + { + "commandType": "loadLabware", + "notes": [], + "params": { + "displayName": "20ul tips", + "loadName": "opentrons_96_tiprack_20ul", + "location": { + "slotName": "4" + }, + "namespace": "opentrons", + "version": 1 + }, + "result": { + "definition": { + "allowedRoles": [], + "brand": { + "brand": "Opentrons", + "brandId": [], + "links": [ + "https://shop.opentrons.com/collections/opentrons-tips/products/opentrons-10ul-tips" + ] + }, + "cornerOffsetFromSlot": { + "x": 0, + "y": 0, + "z": 0 + }, + "dimensions": { + "xDimension": 127.76, + "yDimension": 85.48, + "zDimension": 64.69 + }, + "gripperOffsets": {}, + "groups": [ + { + "metadata": {}, + "wells": [ + "A1", + "A10", + "A11", + "A12", + "A2", + "A3", + "A4", + "A5", + "A6", + "A7", + "A8", + "A9", + "B1", + "B10", + "B11", + "B12", + "B2", + "B3", + "B4", + "B5", + "B6", + "B7", + "B8", + "B9", + "C1", + "C10", + "C11", + "C12", + "C2", + "C3", + "C4", + "C5", + "C6", + "C7", + "C8", + "C9", + "D1", + "D10", + "D11", + "D12", + "D2", + "D3", + "D4", + "D5", + "D6", + "D7", + "D8", + "D9", + "E1", + "E10", + "E11", + "E12", + "E2", + "E3", + "E4", + "E5", + "E6", + "E7", + "E8", + "E9", + "F1", + "F10", + "F11", + "F12", + "F2", + "F3", + "F4", + "F5", + "F6", + "F7", + "F8", + "F9", + "G1", + "G10", + "G11", + "G12", + "G2", + "G3", + "G4", + "G5", + "G6", + "G7", + "G8", + "G9", + "H1", + "H10", + "H11", + "H12", + "H2", + "H3", + "H4", + "H5", + "H6", + "H7", + "H8", + "H9" + ] + } + ], + "metadata": { + "displayCategory": "tipRack", + "displayName": "Opentrons OT-2 96 Tip Rack 20 µL", + "displayVolumeUnits": "µL", + "tags": [] + }, + "namespace": "opentrons", + "ordering": [ + [ + "A1", + "B1", + "C1", + "D1", + "E1", + "F1", + "G1", + "H1" + ], + [ + "A10", + "B10", + "C10", + "D10", + "E10", + "F10", + "G10", + "H10" + ], + [ + "A11", + "B11", + "C11", + "D11", + "E11", + "F11", + "G11", + "H11" + ], + [ + "A12", + "B12", + "C12", + "D12", + "E12", + "F12", + "G12", + "H12" + ], + [ + "A2", + "B2", + "C2", + "D2", + "E2", + "F2", + "G2", + "H2" + ], + [ + "A3", + "B3", + "C3", + "D3", + "E3", + "F3", + "G3", + "H3" + ], + [ + "A4", + "B4", + "C4", + "D4", + "E4", + "F4", + "G4", + "H4" + ], + [ + "A5", + "B5", + "C5", + "D5", + "E5", + "F5", + "G5", + "H5" + ], + [ + "A6", + "B6", + "C6", + "D6", + "E6", + "F6", + "G6", + "H6" + ], + [ + "A7", + "B7", + "C7", + "D7", + "E7", + "F7", + "G7", + "H7" + ], + [ + "A8", + "B8", + "C8", + "D8", + "E8", + "F8", + "G8", + "H8" + ], + [ + "A9", + "B9", + "C9", + "D9", + "E9", + "F9", + "G9", + "H9" + ] + ], + "parameters": { + "format": "96Standard", + "isMagneticModuleCompatible": false, + "isTiprack": true, + "loadName": "opentrons_96_tiprack_20ul", + "tipLength": 39.2, + "tipOverlap": 8.25 + }, + "schemaVersion": 2, + "stackingOffsetWithLabware": {}, + "stackingOffsetWithModule": {}, + "version": 1, + "wells": { + "A1": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 14.38, + "y": 74.24, + "z": 25.49 + }, + "A10": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 95.38, + "y": 74.24, + "z": 25.49 + }, + "A11": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 104.38, + "y": 74.24, + "z": 25.49 + }, + "A12": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 113.38, + "y": 74.24, + "z": 25.49 + }, + "A2": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 23.38, + "y": 74.24, + "z": 25.49 + }, + "A3": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 32.38, + "y": 74.24, + "z": 25.49 + }, + "A4": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 41.38, + "y": 74.24, + "z": 25.49 + }, + "A5": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 50.38, + "y": 74.24, + "z": 25.49 + }, + "A6": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 59.38, + "y": 74.24, + "z": 25.49 + }, + "A7": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 68.38, + "y": 74.24, + "z": 25.49 + }, + "A8": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 77.38, + "y": 74.24, + "z": 25.49 + }, + "A9": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 86.38, + "y": 74.24, + "z": 25.49 + }, + "B1": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 14.38, + "y": 65.24, + "z": 25.49 + }, + "B10": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 95.38, + "y": 65.24, + "z": 25.49 + }, + "B11": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 104.38, + "y": 65.24, + "z": 25.49 + }, + "B12": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 113.38, + "y": 65.24, + "z": 25.49 + }, + "B2": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 23.38, + "y": 65.24, + "z": 25.49 + }, + "B3": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 32.38, + "y": 65.24, + "z": 25.49 + }, + "B4": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 41.38, + "y": 65.24, + "z": 25.49 + }, + "B5": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 50.38, + "y": 65.24, + "z": 25.49 + }, + "B6": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 59.38, + "y": 65.24, + "z": 25.49 + }, + "B7": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 68.38, + "y": 65.24, + "z": 25.49 + }, + "B8": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 77.38, + "y": 65.24, + "z": 25.49 + }, + "B9": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 86.38, + "y": 65.24, + "z": 25.49 + }, + "C1": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 14.38, + "y": 56.24, + "z": 25.49 + }, + "C10": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 95.38, + "y": 56.24, + "z": 25.49 + }, + "C11": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 104.38, + "y": 56.24, + "z": 25.49 + }, + "C12": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 113.38, + "y": 56.24, + "z": 25.49 + }, + "C2": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 23.38, + "y": 56.24, + "z": 25.49 + }, + "C3": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 32.38, + "y": 56.24, + "z": 25.49 + }, + "C4": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 41.38, + "y": 56.24, + "z": 25.49 + }, + "C5": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 50.38, + "y": 56.24, + "z": 25.49 + }, + "C6": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 59.38, + "y": 56.24, + "z": 25.49 + }, + "C7": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 68.38, + "y": 56.24, + "z": 25.49 + }, + "C8": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 77.38, + "y": 56.24, + "z": 25.49 + }, + "C9": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 86.38, + "y": 56.24, + "z": 25.49 + }, + "D1": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 14.38, + "y": 47.24, + "z": 25.49 + }, + "D10": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 95.38, + "y": 47.24, + "z": 25.49 + }, + "D11": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 104.38, + "y": 47.24, + "z": 25.49 + }, + "D12": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 113.38, + "y": 47.24, + "z": 25.49 + }, + "D2": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 23.38, + "y": 47.24, + "z": 25.49 + }, + "D3": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 32.38, + "y": 47.24, + "z": 25.49 + }, + "D4": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 41.38, + "y": 47.24, + "z": 25.49 + }, + "D5": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 50.38, + "y": 47.24, + "z": 25.49 + }, + "D6": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 59.38, + "y": 47.24, + "z": 25.49 + }, + "D7": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 68.38, + "y": 47.24, + "z": 25.49 + }, + "D8": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 77.38, + "y": 47.24, + "z": 25.49 + }, + "D9": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 86.38, + "y": 47.24, + "z": 25.49 + }, + "E1": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 14.38, + "y": 38.24, + "z": 25.49 + }, + "E10": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 95.38, + "y": 38.24, + "z": 25.49 + }, + "E11": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 104.38, + "y": 38.24, + "z": 25.49 + }, + "E12": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 113.38, + "y": 38.24, + "z": 25.49 + }, + "E2": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 23.38, + "y": 38.24, + "z": 25.49 + }, + "E3": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 32.38, + "y": 38.24, + "z": 25.49 + }, + "E4": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 41.38, + "y": 38.24, + "z": 25.49 + }, + "E5": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 50.38, + "y": 38.24, + "z": 25.49 + }, + "E6": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 59.38, + "y": 38.24, + "z": 25.49 + }, + "E7": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 68.38, + "y": 38.24, + "z": 25.49 + }, + "E8": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 77.38, + "y": 38.24, + "z": 25.49 + }, + "E9": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 86.38, + "y": 38.24, + "z": 25.49 + }, + "F1": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 14.38, + "y": 29.24, + "z": 25.49 + }, + "F10": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 95.38, + "y": 29.24, + "z": 25.49 + }, + "F11": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 104.38, + "y": 29.24, + "z": 25.49 + }, + "F12": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 113.38, + "y": 29.24, + "z": 25.49 + }, + "F2": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 23.38, + "y": 29.24, + "z": 25.49 + }, + "F3": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 32.38, + "y": 29.24, + "z": 25.49 + }, + "F4": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 41.38, + "y": 29.24, + "z": 25.49 + }, + "F5": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 50.38, + "y": 29.24, + "z": 25.49 + }, + "F6": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 59.38, + "y": 29.24, + "z": 25.49 + }, + "F7": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 68.38, + "y": 29.24, + "z": 25.49 + }, + "F8": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 77.38, + "y": 29.24, + "z": 25.49 + }, + "F9": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 86.38, + "y": 29.24, + "z": 25.49 + }, + "G1": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 14.38, + "y": 20.24, + "z": 25.49 + }, + "G10": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 95.38, + "y": 20.24, + "z": 25.49 + }, + "G11": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 104.38, + "y": 20.24, + "z": 25.49 + }, + "G12": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 113.38, + "y": 20.24, + "z": 25.49 + }, + "G2": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 23.38, + "y": 20.24, + "z": 25.49 + }, + "G3": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 32.38, + "y": 20.24, + "z": 25.49 + }, + "G4": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 41.38, + "y": 20.24, + "z": 25.49 + }, + "G5": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 50.38, + "y": 20.24, + "z": 25.49 + }, + "G6": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 59.38, + "y": 20.24, + "z": 25.49 + }, + "G7": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 68.38, + "y": 20.24, + "z": 25.49 + }, + "G8": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 77.38, + "y": 20.24, + "z": 25.49 + }, + "G9": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 86.38, + "y": 20.24, + "z": 25.49 + }, + "H1": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 14.38, + "y": 11.24, + "z": 25.49 + }, + "H10": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 95.38, + "y": 11.24, + "z": 25.49 + }, + "H11": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 104.38, + "y": 11.24, + "z": 25.49 + }, + "H12": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 113.38, + "y": 11.24, + "z": 25.49 + }, + "H2": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 23.38, + "y": 11.24, + "z": 25.49 + }, + "H3": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 32.38, + "y": 11.24, + "z": 25.49 + }, + "H4": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 41.38, + "y": 11.24, + "z": 25.49 + }, + "H5": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 50.38, + "y": 11.24, + "z": 25.49 + }, + "H6": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 59.38, + "y": 11.24, + "z": 25.49 + }, + "H7": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 68.38, + "y": 11.24, + "z": 25.49 + }, + "H8": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 77.38, + "y": 11.24, + "z": 25.49 + }, + "H9": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 86.38, + "y": 11.24, + "z": 25.49 + } + } + } + }, + "status": "succeeded" + }, + { + "commandType": "loadPipette", + "notes": [], + "params": { + "mount": "left", + "pipetteName": "p300_multi_gen2" + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "loadPipette", + "notes": [], + "params": { + "mount": "right", + "pipetteName": "p20_single_gen2" + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "loadModule", + "notes": [], + "params": { + "location": { + "slotName": "1" + }, + "model": "heaterShakerModuleV1" + }, + "result": { + "definition": { + "calibrationPoint": { + "x": 12.0, + "y": 8.75, + "z": 68.275 + }, + "compatibleWith": [], + "dimensions": { + "bareOverallHeight": 82.0, + "overLabwareHeight": 0.0 + }, + "displayName": "Heater-Shaker Module GEN1", + "gripperOffsets": { + "default": { + "dropOffset": { + "x": 0.0, + "y": 0.0, + "z": 1.0 + }, + "pickUpOffset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + } + } + }, + "labwareOffset": { + "x": -0.125, + "y": 1.125, + "z": 68.275 + }, + "model": "heaterShakerModuleV1", + "moduleType": "heaterShakerModuleType", + "otSharedSchema": "module/schemas/2", + "quirks": [], + "slotTransforms": { + "ot2_short_trash": { + "3": { + "labwareOffset": [ + [ + -1, + 0, + 0, + 0 + ], + [ + -1, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ] + ] + }, + "6": { + "labwareOffset": [ + [ + -1, + 0, + 0, + 0 + ], + [ + -1, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ] + ] + }, + "9": { + "labwareOffset": [ + [ + -1, + 0, + 0, + 0 + ], + [ + -1, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ] + ] + } + }, + "ot2_standard": { + "3": { + "labwareOffset": [ + [ + -1, + 0, + 0, + 0 + ], + [ + -1, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ] + ] + }, + "6": { + "labwareOffset": [ + [ + -1, + 0, + 0, + 0 + ], + [ + -1, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ] + ] + }, + "9": { + "labwareOffset": [ + [ + -1, + 0, + 0, + 0 + ], + [ + -1, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ] + ] + } + }, + "ot3_standard": { + "A1": { + "labwareOffset": [ + [ + -49.325, + 0, + 0, + 1 + ], + [ + -1.125, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0.125, + 1 + ] + ] + }, + "A3": { + "labwareOffset": [ + [ + -49.325, + 0, + 0, + 1 + ], + [ + -1.125, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0.125, + 1 + ] + ] + }, + "B1": { + "labwareOffset": [ + [ + -49.325, + 0, + 0, + 1 + ], + [ + -1.125, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0.125, + 1 + ] + ] + }, + "B3": { + "labwareOffset": [ + [ + -49.325, + 0, + 0, + 1 + ], + [ + -1.125, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0.125, + 1 + ] + ] + }, + "C1": { + "labwareOffset": [ + [ + -49.325, + 0, + 0, + 1 + ], + [ + -1.125, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0.125, + 1 + ] + ] + }, + "C3": { + "labwareOffset": [ + [ + -49.325, + 0, + 0, + 1 + ], + [ + -1.125, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0.125, + 1 + ] + ] + }, + "D1": { + "labwareOffset": [ + [ + -49.325, + 0, + 0, + 1 + ], + [ + -1.125, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0.125, + 1 + ] + ] + }, + "D3": { + "labwareOffset": [ + [ + -49.325, + 0, + 0, + 1 + ], + [ + -1.125, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0.125, + 1 + ] + ] + } + } + } + }, + "model": "heaterShakerModuleV1" + }, + "status": "succeeded" + }, + { + "commandType": "loadModule", + "notes": [], + "params": { + "location": { + "slotName": "9" + }, + "model": "temperatureModuleV2" + }, + "result": { + "definition": { + "calibrationPoint": { + "x": 11.7, + "y": 8.75, + "z": 80.09 + }, + "compatibleWith": [ + "temperatureModuleV1" + ], + "dimensions": { + "bareOverallHeight": 84.0, + "overLabwareHeight": 0.0 + }, + "displayName": "Temperature Module GEN2", + "gripperOffsets": { + "default": { + "dropOffset": { + "x": 0.0, + "y": 0.0, + "z": 1.0 + }, + "pickUpOffset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + } + } + }, + "labwareOffset": { + "x": -1.45, + "y": -0.15, + "z": 80.09 + }, + "model": "temperatureModuleV2", + "moduleType": "temperatureModuleType", + "otSharedSchema": "module/schemas/2", + "quirks": [], + "slotTransforms": { + "ot2_short_trash": { + "3": { + "labwareOffset": [ + [ + -1, + -0.15, + 0, + 0 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ] + ] + }, + "6": { + "labwareOffset": [ + [ + -1, + -0.15, + 0, + 0 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ] + ] + }, + "9": { + "labwareOffset": [ + [ + -1, + -0.15, + 0, + 0 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ] + ] + } + }, + "ot2_standard": { + "3": { + "labwareOffset": [ + [ + -1, + -0.3, + 0, + 0 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ] + ] + }, + "6": { + "labwareOffset": [ + [ + -1, + -0.3, + 0, + 0 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ] + ] + }, + "9": { + "labwareOffset": [ + [ + -1, + -0.3, + 0, + 0 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ] + ] + } + }, + "ot3_standard": { + "A1": { + "labwareOffset": [ + [ + -71.09, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0.15, + 1 + ], + [ + 0, + 0, + 1, + 1.45 + ] + ] + }, + "A3": { + "labwareOffset": [ + [ + -71.09, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0.15, + 1 + ], + [ + 0, + 0, + 1, + 1.45 + ] + ] + }, + "B1": { + "labwareOffset": [ + [ + -71.09, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0.15, + 1 + ], + [ + 0, + 0, + 1, + 1.45 + ] + ] + }, + "B3": { + "labwareOffset": [ + [ + -71.09, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0.15, + 1 + ], + [ + 0, + 0, + 1, + 1.45 + ] + ] + }, + "C1": { + "labwareOffset": [ + [ + -71.09, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0.15, + 1 + ], + [ + 0, + 0, + 1, + 1.45 + ] + ] + }, + "C3": { + "labwareOffset": [ + [ + -71.09, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0.15, + 1 + ], + [ + 0, + 0, + 1, + 1.45 + ] + ] + }, + "D1": { + "labwareOffset": [ + [ + -71.09, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0.15, + 1 + ], + [ + 0, + 0, + 1, + 1.45 + ] + ] + }, + "D3": { + "labwareOffset": [ + [ + -71.09, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0.15, + 1 + ], + [ + 0, + 0, + 1, + 1.45 + ] + ] + } + } + } + }, + "model": "temperatureModuleV2" + }, + "status": "succeeded" + }, + { + "commandType": "loadModule", + "notes": [], + "params": { + "location": { + "slotName": "7" + }, + "model": "thermocyclerModuleV2" + }, + "result": { + "definition": { + "calibrationPoint": { + "x": 14.4, + "y": 64.93, + "z": 97.8 + }, + "compatibleWith": [], + "dimensions": { + "bareOverallHeight": 108.96, + "lidHeight": 61.7, + "overLabwareHeight": 0.0 + }, + "displayName": "Thermocycler Module GEN2", + "gripperOffsets": { + "default": { + "dropOffset": { + "x": 0.0, + "y": 0.0, + "z": 5.6 + }, + "pickUpOffset": { + "x": 0.0, + "y": 0.0, + "z": 4.6 + } + } + }, + "labwareOffset": { + "x": 0.0, + "y": 68.8, + "z": 108.96 + }, + "model": "thermocyclerModuleV2", + "moduleType": "thermocyclerModuleType", + "otSharedSchema": "module/schemas/2", + "quirks": [], + "slotTransforms": { + "ot3_standard": { + "B1": { + "cornerOffsetFromSlot": [ + [ + -98, + 0, + 0, + 1 + ], + [ + -20.005, + 0, + 0, + 1 + ], + [ + -0.84, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ] + ], + "labwareOffset": [ + [ + -98, + 0, + 0, + 1 + ], + [ + -20.005, + 0, + 0, + 1 + ], + [ + -0.84, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ] + ] + } + } + } + }, + "model": "thermocyclerModuleV2" + }, + "status": "succeeded" + }, + { + "commandType": "loadLabware", + "notes": [], + "params": { + "loadName": "opentrons_96_well_aluminum_block", + "location": {}, + "namespace": "opentrons", + "version": 1 + }, + "result": { + "definition": { + "allowedRoles": [ + "adapter" + ], + "brand": { + "brand": "Opentrons", + "brandId": [] + }, + "cornerOffsetFromSlot": { + "x": 0, + "y": 0, + "z": 0 + }, + "dimensions": { + "xDimension": 127.76, + "yDimension": 85.48, + "zDimension": 18.16 + }, + "gripperOffsets": { + "default": { + "dropOffset": { + "x": 0, + "y": 0, + "z": 1.0 + }, + "pickUpOffset": { + "x": 0, + "y": 0, + "z": 0 + } + } + }, + "groups": [ + { + "metadata": { + "wellBottomShape": "v" + }, + "wells": [ + "A1", + "A10", + "A11", + "A12", + "A2", + "A3", + "A4", + "A5", + "A6", + "A7", + "A8", + "A9", + "B1", + "B10", + "B11", + "B12", + "B2", + "B3", + "B4", + "B5", + "B6", + "B7", + "B8", + "B9", + "C1", + "C10", + "C11", + "C12", + "C2", + "C3", + "C4", + "C5", + "C6", + "C7", + "C8", + "C9", + "D1", + "D10", + "D11", + "D12", + "D2", + "D3", + "D4", + "D5", + "D6", + "D7", + "D8", + "D9", + "E1", + "E10", + "E11", + "E12", + "E2", + "E3", + "E4", + "E5", + "E6", + "E7", + "E8", + "E9", + "F1", + "F10", + "F11", + "F12", + "F2", + "F3", + "F4", + "F5", + "F6", + "F7", + "F8", + "F9", + "G1", + "G10", + "G11", + "G12", + "G2", + "G3", + "G4", + "G5", + "G6", + "G7", + "G8", + "G9", + "H1", + "H10", + "H11", + "H12", + "H2", + "H3", + "H4", + "H5", + "H6", + "H7", + "H8", + "H9" + ] + } + ], + "metadata": { + "displayCategory": "adapter", + "displayName": "Opentrons 96 Well Aluminum Block", + "displayVolumeUnits": "µL", + "tags": [] + }, + "namespace": "opentrons", + "ordering": [ + [ + "A1", + "B1", + "C1", + "D1", + "E1", + "F1", + "G1", + "H1" + ], + [ + "A10", + "B10", + "C10", + "D10", + "E10", + "F10", + "G10", + "H10" + ], + [ + "A11", + "B11", + "C11", + "D11", + "E11", + "F11", + "G11", + "H11" + ], + [ + "A12", + "B12", + "C12", + "D12", + "E12", + "F12", + "G12", + "H12" + ], + [ + "A2", + "B2", + "C2", + "D2", + "E2", + "F2", + "G2", + "H2" + ], + [ + "A3", + "B3", + "C3", + "D3", + "E3", + "F3", + "G3", + "H3" + ], + [ + "A4", + "B4", + "C4", + "D4", + "E4", + "F4", + "G4", + "H4" + ], + [ + "A5", + "B5", + "C5", + "D5", + "E5", + "F5", + "G5", + "H5" + ], + [ + "A6", + "B6", + "C6", + "D6", + "E6", + "F6", + "G6", + "H6" + ], + [ + "A7", + "B7", + "C7", + "D7", + "E7", + "F7", + "G7", + "H7" + ], + [ + "A8", + "B8", + "C8", + "D8", + "E8", + "F8", + "G8", + "H8" + ], + [ + "A9", + "B9", + "C9", + "D9", + "E9", + "F9", + "G9", + "H9" + ] + ], + "parameters": { + "format": "96Standard", + "isMagneticModuleCompatible": false, + "isTiprack": false, + "loadName": "opentrons_96_well_aluminum_block", + "quirks": [] + }, + "schemaVersion": 2, + "stackingOffsetWithLabware": {}, + "stackingOffsetWithModule": {}, + "version": 1, + "wells": { + "A1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 14.38, + "y": 74.24, + "z": 3.38 + }, + "A10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 95.38, + "y": 74.24, + "z": 3.38 + }, + "A11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 104.38, + "y": 74.24, + "z": 3.38 + }, + "A12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 113.38, + "y": 74.24, + "z": 3.38 + }, + "A2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 23.38, + "y": 74.24, + "z": 3.38 + }, + "A3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 32.38, + "y": 74.24, + "z": 3.38 + }, + "A4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 41.38, + "y": 74.24, + "z": 3.38 + }, + "A5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 50.38, + "y": 74.24, + "z": 3.38 + }, + "A6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 59.38, + "y": 74.24, + "z": 3.38 + }, + "A7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 68.38, + "y": 74.24, + "z": 3.38 + }, + "A8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 77.38, + "y": 74.24, + "z": 3.38 + }, + "A9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 86.38, + "y": 74.24, + "z": 3.38 + }, + "B1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 14.38, + "y": 65.24, + "z": 3.38 + }, + "B10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 95.38, + "y": 65.24, + "z": 3.38 + }, + "B11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 104.38, + "y": 65.24, + "z": 3.38 + }, + "B12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 113.38, + "y": 65.24, + "z": 3.38 + }, + "B2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 23.38, + "y": 65.24, + "z": 3.38 + }, + "B3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 32.38, + "y": 65.24, + "z": 3.38 + }, + "B4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 41.38, + "y": 65.24, + "z": 3.38 + }, + "B5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 50.38, + "y": 65.24, + "z": 3.38 + }, + "B6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 59.38, + "y": 65.24, + "z": 3.38 + }, + "B7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 68.38, + "y": 65.24, + "z": 3.38 + }, + "B8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 77.38, + "y": 65.24, + "z": 3.38 + }, + "B9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 86.38, + "y": 65.24, + "z": 3.38 + }, + "C1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 14.38, + "y": 56.24, + "z": 3.38 + }, + "C10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 95.38, + "y": 56.24, + "z": 3.38 + }, + "C11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 104.38, + "y": 56.24, + "z": 3.38 + }, + "C12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 113.38, + "y": 56.24, + "z": 3.38 + }, + "C2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 23.38, + "y": 56.24, + "z": 3.38 + }, + "C3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 32.38, + "y": 56.24, + "z": 3.38 + }, + "C4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 41.38, + "y": 56.24, + "z": 3.38 + }, + "C5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 50.38, + "y": 56.24, + "z": 3.38 + }, + "C6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 59.38, + "y": 56.24, + "z": 3.38 + }, + "C7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 68.38, + "y": 56.24, + "z": 3.38 + }, + "C8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 77.38, + "y": 56.24, + "z": 3.38 + }, + "C9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 86.38, + "y": 56.24, + "z": 3.38 + }, + "D1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 14.38, + "y": 47.24, + "z": 3.38 + }, + "D10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 95.38, + "y": 47.24, + "z": 3.38 + }, + "D11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 104.38, + "y": 47.24, + "z": 3.38 + }, + "D12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 113.38, + "y": 47.24, + "z": 3.38 + }, + "D2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 23.38, + "y": 47.24, + "z": 3.38 + }, + "D3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 32.38, + "y": 47.24, + "z": 3.38 + }, + "D4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 41.38, + "y": 47.24, + "z": 3.38 + }, + "D5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 50.38, + "y": 47.24, + "z": 3.38 + }, + "D6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 59.38, + "y": 47.24, + "z": 3.38 + }, + "D7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 68.38, + "y": 47.24, + "z": 3.38 + }, + "D8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 77.38, + "y": 47.24, + "z": 3.38 + }, + "D9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 86.38, + "y": 47.24, + "z": 3.38 + }, + "E1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 14.38, + "y": 38.24, + "z": 3.38 + }, + "E10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 95.38, + "y": 38.24, + "z": 3.38 + }, + "E11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 104.38, + "y": 38.24, + "z": 3.38 + }, + "E12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 113.38, + "y": 38.24, + "z": 3.38 + }, + "E2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 23.38, + "y": 38.24, + "z": 3.38 + }, + "E3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 32.38, + "y": 38.24, + "z": 3.38 + }, + "E4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 41.38, + "y": 38.24, + "z": 3.38 + }, + "E5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 50.38, + "y": 38.24, + "z": 3.38 + }, + "E6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 59.38, + "y": 38.24, + "z": 3.38 + }, + "E7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 68.38, + "y": 38.24, + "z": 3.38 + }, + "E8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 77.38, + "y": 38.24, + "z": 3.38 + }, + "E9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 86.38, + "y": 38.24, + "z": 3.38 + }, + "F1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 14.38, + "y": 29.24, + "z": 3.38 + }, + "F10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 95.38, + "y": 29.24, + "z": 3.38 + }, + "F11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 104.38, + "y": 29.24, + "z": 3.38 + }, + "F12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 113.38, + "y": 29.24, + "z": 3.38 + }, + "F2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 23.38, + "y": 29.24, + "z": 3.38 + }, + "F3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 32.38, + "y": 29.24, + "z": 3.38 + }, + "F4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 41.38, + "y": 29.24, + "z": 3.38 + }, + "F5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 50.38, + "y": 29.24, + "z": 3.38 + }, + "F6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 59.38, + "y": 29.24, + "z": 3.38 + }, + "F7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 68.38, + "y": 29.24, + "z": 3.38 + }, + "F8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 77.38, + "y": 29.24, + "z": 3.38 + }, + "F9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 86.38, + "y": 29.24, + "z": 3.38 + }, + "G1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 14.38, + "y": 20.24, + "z": 3.38 + }, + "G10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 95.38, + "y": 20.24, + "z": 3.38 + }, + "G11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 104.38, + "y": 20.24, + "z": 3.38 + }, + "G12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 113.38, + "y": 20.24, + "z": 3.38 + }, + "G2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 23.38, + "y": 20.24, + "z": 3.38 + }, + "G3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 32.38, + "y": 20.24, + "z": 3.38 + }, + "G4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 41.38, + "y": 20.24, + "z": 3.38 + }, + "G5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 50.38, + "y": 20.24, + "z": 3.38 + }, + "G6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 59.38, + "y": 20.24, + "z": 3.38 + }, + "G7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 68.38, + "y": 20.24, + "z": 3.38 + }, + "G8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 77.38, + "y": 20.24, + "z": 3.38 + }, + "G9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 86.38, + "y": 20.24, + "z": 3.38 + }, + "H1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 14.38, + "y": 11.24, + "z": 3.38 + }, + "H10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 95.38, + "y": 11.24, + "z": 3.38 + }, + "H11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 104.38, + "y": 11.24, + "z": 3.38 + }, + "H12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 113.38, + "y": 11.24, + "z": 3.38 + }, + "H2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 23.38, + "y": 11.24, + "z": 3.38 + }, + "H3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 32.38, + "y": 11.24, + "z": 3.38 + }, + "H4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 41.38, + "y": 11.24, + "z": 3.38 + }, + "H5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 50.38, + "y": 11.24, + "z": 3.38 + }, + "H6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 59.38, + "y": 11.24, + "z": 3.38 + }, + "H7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 68.38, + "y": 11.24, + "z": 3.38 + }, + "H8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 77.38, + "y": 11.24, + "z": 3.38 + }, + "H9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 86.38, + "y": 11.24, + "z": 3.38 + } + } + } + }, + "status": "succeeded" + }, + { + "commandType": "loadLabware", + "notes": [], + "params": { + "displayName": "Temperature-Controlled plate", + "loadName": "nest_96_wellplate_100ul_pcr_full_skirt", + "location": {}, + "namespace": "opentrons", + "version": 2 + }, + "result": { + "definition": { + "allowedRoles": [], + "brand": { + "brand": "NEST", + "brandId": [ + "402501" + ], + "links": [ + "https://www.nest-biotech.com/pcr-plates/58773587.html" + ] + }, + "cornerOffsetFromSlot": { + "x": 0, + "y": 0, + "z": 0 + }, + "dimensions": { + "xDimension": 127.76, + "yDimension": 85.48, + "zDimension": 15.7 + }, + "gripForce": 15.0, + "gripHeightFromLabwareBottom": 10.65, + "gripperOffsets": {}, + "groups": [ + { + "metadata": { + "wellBottomShape": "v" + }, + "wells": [ + "A1", + "A10", + "A11", + "A12", + "A2", + "A3", + "A4", + "A5", + "A6", + "A7", + "A8", + "A9", + "B1", + "B10", + "B11", + "B12", + "B2", + "B3", + "B4", + "B5", + "B6", + "B7", + "B8", + "B9", + "C1", + "C10", + "C11", + "C12", + "C2", + "C3", + "C4", + "C5", + "C6", + "C7", + "C8", + "C9", + "D1", + "D10", + "D11", + "D12", + "D2", + "D3", + "D4", + "D5", + "D6", + "D7", + "D8", + "D9", + "E1", + "E10", + "E11", + "E12", + "E2", + "E3", + "E4", + "E5", + "E6", + "E7", + "E8", + "E9", + "F1", + "F10", + "F11", + "F12", + "F2", + "F3", + "F4", + "F5", + "F6", + "F7", + "F8", + "F9", + "G1", + "G10", + "G11", + "G12", + "G2", + "G3", + "G4", + "G5", + "G6", + "G7", + "G8", + "G9", + "H1", + "H10", + "H11", + "H12", + "H2", + "H3", + "H4", + "H5", + "H6", + "H7", + "H8", + "H9" + ] + } + ], + "metadata": { + "displayCategory": "wellPlate", + "displayName": "NEST 96 Well Plate 100 µL PCR Full Skirt", + "displayVolumeUnits": "µL", + "tags": [] + }, + "namespace": "opentrons", + "ordering": [ + [ + "A1", + "B1", + "C1", + "D1", + "E1", + "F1", + "G1", + "H1" + ], + [ + "A10", + "B10", + "C10", + "D10", + "E10", + "F10", + "G10", + "H10" + ], + [ + "A11", + "B11", + "C11", + "D11", + "E11", + "F11", + "G11", + "H11" + ], + [ + "A12", + "B12", + "C12", + "D12", + "E12", + "F12", + "G12", + "H12" + ], + [ + "A2", + "B2", + "C2", + "D2", + "E2", + "F2", + "G2", + "H2" + ], + [ + "A3", + "B3", + "C3", + "D3", + "E3", + "F3", + "G3", + "H3" + ], + [ + "A4", + "B4", + "C4", + "D4", + "E4", + "F4", + "G4", + "H4" + ], + [ + "A5", + "B5", + "C5", + "D5", + "E5", + "F5", + "G5", + "H5" + ], + [ + "A6", + "B6", + "C6", + "D6", + "E6", + "F6", + "G6", + "H6" + ], + [ + "A7", + "B7", + "C7", + "D7", + "E7", + "F7", + "G7", + "H7" + ], + [ + "A8", + "B8", + "C8", + "D8", + "E8", + "F8", + "G8", + "H8" + ], + [ + "A9", + "B9", + "C9", + "D9", + "E9", + "F9", + "G9", + "H9" + ] + ], + "parameters": { + "format": "96Standard", + "isMagneticModuleCompatible": true, + "isTiprack": false, + "loadName": "nest_96_wellplate_100ul_pcr_full_skirt", + "magneticModuleEngageHeight": 20 + }, + "schemaVersion": 2, + "stackingOffsetWithLabware": { + "opentrons_96_pcr_adapter": { + "x": 0, + "y": 0, + "z": 10.2 + }, + "opentrons_96_well_aluminum_block": { + "x": 0, + "y": 0, + "z": 12.66 + } + }, + "stackingOffsetWithModule": { + "thermocyclerModuleV2": { + "x": 0, + "y": 0, + "z": 10.8 + } + }, + "version": 2, + "wells": { + "A1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 74.24, + "z": 0.92 + }, + "A10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 74.24, + "z": 0.92 + }, + "A11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 74.24, + "z": 0.92 + }, + "A12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 74.24, + "z": 0.92 + }, + "A2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 74.24, + "z": 0.92 + }, + "A3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 74.24, + "z": 0.92 + }, + "A4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 74.24, + "z": 0.92 + }, + "A5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 74.24, + "z": 0.92 + }, + "A6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 74.24, + "z": 0.92 + }, + "A7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 74.24, + "z": 0.92 + }, + "A8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 74.24, + "z": 0.92 + }, + "A9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 74.24, + "z": 0.92 + }, + "B1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 65.24, + "z": 0.92 + }, + "B10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 65.24, + "z": 0.92 + }, + "B11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 65.24, + "z": 0.92 + }, + "B12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 65.24, + "z": 0.92 + }, + "B2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 65.24, + "z": 0.92 + }, + "B3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 65.24, + "z": 0.92 + }, + "B4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 65.24, + "z": 0.92 + }, + "B5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 65.24, + "z": 0.92 + }, + "B6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 65.24, + "z": 0.92 + }, + "B7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 65.24, + "z": 0.92 + }, + "B8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 65.24, + "z": 0.92 + }, + "B9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 65.24, + "z": 0.92 + }, + "C1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 56.24, + "z": 0.92 + }, + "C10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 56.24, + "z": 0.92 + }, + "C11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 56.24, + "z": 0.92 + }, + "C12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 56.24, + "z": 0.92 + }, + "C2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 56.24, + "z": 0.92 + }, + "C3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 56.24, + "z": 0.92 + }, + "C4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 56.24, + "z": 0.92 + }, + "C5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 56.24, + "z": 0.92 + }, + "C6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 56.24, + "z": 0.92 + }, + "C7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 56.24, + "z": 0.92 + }, + "C8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 56.24, + "z": 0.92 + }, + "C9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 56.24, + "z": 0.92 + }, + "D1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 47.24, + "z": 0.92 + }, + "D10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 47.24, + "z": 0.92 + }, + "D11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 47.24, + "z": 0.92 + }, + "D12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 47.24, + "z": 0.92 + }, + "D2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 47.24, + "z": 0.92 + }, + "D3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 47.24, + "z": 0.92 + }, + "D4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 47.24, + "z": 0.92 + }, + "D5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 47.24, + "z": 0.92 + }, + "D6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 47.24, + "z": 0.92 + }, + "D7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 47.24, + "z": 0.92 + }, + "D8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 47.24, + "z": 0.92 + }, + "D9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 47.24, + "z": 0.92 + }, + "E1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 38.24, + "z": 0.92 + }, + "E10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 38.24, + "z": 0.92 + }, + "E11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 38.24, + "z": 0.92 + }, + "E12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 38.24, + "z": 0.92 + }, + "E2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 38.24, + "z": 0.92 + }, + "E3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 38.24, + "z": 0.92 + }, + "E4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 38.24, + "z": 0.92 + }, + "E5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 38.24, + "z": 0.92 + }, + "E6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 38.24, + "z": 0.92 + }, + "E7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 38.24, + "z": 0.92 + }, + "E8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 38.24, + "z": 0.92 + }, + "E9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 38.24, + "z": 0.92 + }, + "F1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 29.24, + "z": 0.92 + }, + "F10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 29.24, + "z": 0.92 + }, + "F11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 29.24, + "z": 0.92 + }, + "F12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 29.24, + "z": 0.92 + }, + "F2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 29.24, + "z": 0.92 + }, + "F3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 29.24, + "z": 0.92 + }, + "F4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 29.24, + "z": 0.92 + }, + "F5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 29.24, + "z": 0.92 + }, + "F6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 29.24, + "z": 0.92 + }, + "F7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 29.24, + "z": 0.92 + }, + "F8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 29.24, + "z": 0.92 + }, + "F9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 29.24, + "z": 0.92 + }, + "G1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 20.24, + "z": 0.92 + }, + "G10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 20.24, + "z": 0.92 + }, + "G11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 20.24, + "z": 0.92 + }, + "G12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 20.24, + "z": 0.92 + }, + "G2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 20.24, + "z": 0.92 + }, + "G3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 20.24, + "z": 0.92 + }, + "G4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 20.24, + "z": 0.92 + }, + "G5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 20.24, + "z": 0.92 + }, + "G6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 20.24, + "z": 0.92 + }, + "G7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 20.24, + "z": 0.92 + }, + "G8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 20.24, + "z": 0.92 + }, + "G9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 20.24, + "z": 0.92 + }, + "H1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 11.24, + "z": 0.92 + }, + "H10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 11.24, + "z": 0.92 + }, + "H11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 11.24, + "z": 0.92 + }, + "H12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 11.24, + "z": 0.92 + }, + "H2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 11.24, + "z": 0.92 + }, + "H3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 11.24, + "z": 0.92 + }, + "H4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 11.24, + "z": 0.92 + }, + "H5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 11.24, + "z": 0.92 + }, + "H6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 11.24, + "z": 0.92 + }, + "H7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 11.24, + "z": 0.92 + }, + "H8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 11.24, + "z": 0.92 + }, + "H9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 11.24, + "z": 0.92 + } + } + } + }, + "status": "succeeded" + }, + { + "commandType": "loadLabware", + "notes": [], + "params": { + "loadName": "opentrons_96_pcr_adapter", + "location": {}, + "namespace": "opentrons", + "version": 1 + }, + "result": { + "definition": { + "allowedRoles": [ + "adapter" + ], + "brand": { + "brand": "Opentrons", + "brandId": [] + }, + "cornerOffsetFromSlot": { + "x": 8.5, + "y": 5.5, + "z": 0 + }, + "dimensions": { + "xDimension": 111, + "yDimension": 75, + "zDimension": 13.85 + }, + "gripperOffsets": { + "default": { + "dropOffset": { + "x": 0, + "y": 0, + "z": 1.0 + }, + "pickUpOffset": { + "x": 0, + "y": 0, + "z": 0 + } + } + }, + "groups": [ + { + "metadata": { + "wellBottomShape": "v" + }, + "wells": [ + "A1", + "A10", + "A11", + "A12", + "A2", + "A3", + "A4", + "A5", + "A6", + "A7", + "A8", + "A9", + "B1", + "B10", + "B11", + "B12", + "B2", + "B3", + "B4", + "B5", + "B6", + "B7", + "B8", + "B9", + "C1", + "C10", + "C11", + "C12", + "C2", + "C3", + "C4", + "C5", + "C6", + "C7", + "C8", + "C9", + "D1", + "D10", + "D11", + "D12", + "D2", + "D3", + "D4", + "D5", + "D6", + "D7", + "D8", + "D9", + "E1", + "E10", + "E11", + "E12", + "E2", + "E3", + "E4", + "E5", + "E6", + "E7", + "E8", + "E9", + "F1", + "F10", + "F11", + "F12", + "F2", + "F3", + "F4", + "F5", + "F6", + "F7", + "F8", + "F9", + "G1", + "G10", + "G11", + "G12", + "G2", + "G3", + "G4", + "G5", + "G6", + "G7", + "G8", + "G9", + "H1", + "H10", + "H11", + "H12", + "H2", + "H3", + "H4", + "H5", + "H6", + "H7", + "H8", + "H9" + ] + } + ], + "metadata": { + "displayCategory": "adapter", + "displayName": "Opentrons 96 PCR Heater-Shaker Adapter", + "displayVolumeUnits": "µL", + "tags": [] + }, + "namespace": "opentrons", + "ordering": [ + [ + "A1", + "B1", + "C1", + "D1", + "E1", + "F1", + "G1", + "H1" + ], + [ + "A10", + "B10", + "C10", + "D10", + "E10", + "F10", + "G10", + "H10" + ], + [ + "A11", + "B11", + "C11", + "D11", + "E11", + "F11", + "G11", + "H11" + ], + [ + "A12", + "B12", + "C12", + "D12", + "E12", + "F12", + "G12", + "H12" + ], + [ + "A2", + "B2", + "C2", + "D2", + "E2", + "F2", + "G2", + "H2" + ], + [ + "A3", + "B3", + "C3", + "D3", + "E3", + "F3", + "G3", + "H3" + ], + [ + "A4", + "B4", + "C4", + "D4", + "E4", + "F4", + "G4", + "H4" + ], + [ + "A5", + "B5", + "C5", + "D5", + "E5", + "F5", + "G5", + "H5" + ], + [ + "A6", + "B6", + "C6", + "D6", + "E6", + "F6", + "G6", + "H6" + ], + [ + "A7", + "B7", + "C7", + "D7", + "E7", + "F7", + "G7", + "H7" + ], + [ + "A8", + "B8", + "C8", + "D8", + "E8", + "F8", + "G8", + "H8" + ], + [ + "A9", + "B9", + "C9", + "D9", + "E9", + "F9", + "G9", + "H9" + ] + ], + "parameters": { + "format": "96Standard", + "isMagneticModuleCompatible": false, + "isTiprack": false, + "loadName": "opentrons_96_pcr_adapter", + "quirks": [] + }, + "schemaVersion": 2, + "stackingOffsetWithLabware": {}, + "stackingOffsetWithModule": {}, + "version": 1, + "wells": { + "A1": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 6, + "y": 69, + "z": 1.85 + }, + "A10": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 87, + "y": 69, + "z": 1.85 + }, + "A11": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 96, + "y": 69, + "z": 1.85 + }, + "A12": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 105, + "y": 69, + "z": 1.85 + }, + "A2": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 15, + "y": 69, + "z": 1.85 + }, + "A3": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 24, + "y": 69, + "z": 1.85 + }, + "A4": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 33, + "y": 69, + "z": 1.85 + }, + "A5": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 42, + "y": 69, + "z": 1.85 + }, + "A6": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 51, + "y": 69, + "z": 1.85 + }, + "A7": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 60, + "y": 69, + "z": 1.85 + }, + "A8": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 69, + "y": 69, + "z": 1.85 + }, + "A9": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 78, + "y": 69, + "z": 1.85 + }, + "B1": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 6, + "y": 60, + "z": 1.85 + }, + "B10": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 87, + "y": 60, + "z": 1.85 + }, + "B11": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 96, + "y": 60, + "z": 1.85 + }, + "B12": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 105, + "y": 60, + "z": 1.85 + }, + "B2": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 15, + "y": 60, + "z": 1.85 + }, + "B3": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 24, + "y": 60, + "z": 1.85 + }, + "B4": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 33, + "y": 60, + "z": 1.85 + }, + "B5": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 42, + "y": 60, + "z": 1.85 + }, + "B6": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 51, + "y": 60, + "z": 1.85 + }, + "B7": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 60, + "y": 60, + "z": 1.85 + }, + "B8": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 69, + "y": 60, + "z": 1.85 + }, + "B9": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 78, + "y": 60, + "z": 1.85 + }, + "C1": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 6, + "y": 51, + "z": 1.85 + }, + "C10": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 87, + "y": 51, + "z": 1.85 + }, + "C11": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 96, + "y": 51, + "z": 1.85 + }, + "C12": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 105, + "y": 51, + "z": 1.85 + }, + "C2": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 15, + "y": 51, + "z": 1.85 + }, + "C3": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 24, + "y": 51, + "z": 1.85 + }, + "C4": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 33, + "y": 51, + "z": 1.85 + }, + "C5": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 42, + "y": 51, + "z": 1.85 + }, + "C6": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 51, + "y": 51, + "z": 1.85 + }, + "C7": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 60, + "y": 51, + "z": 1.85 + }, + "C8": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 69, + "y": 51, + "z": 1.85 + }, + "C9": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 78, + "y": 51, + "z": 1.85 + }, + "D1": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 6, + "y": 42, + "z": 1.85 + }, + "D10": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 87, + "y": 42, + "z": 1.85 + }, + "D11": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 96, + "y": 42, + "z": 1.85 + }, + "D12": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 105, + "y": 42, + "z": 1.85 + }, + "D2": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 15, + "y": 42, + "z": 1.85 + }, + "D3": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 24, + "y": 42, + "z": 1.85 + }, + "D4": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 33, + "y": 42, + "z": 1.85 + }, + "D5": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 42, + "y": 42, + "z": 1.85 + }, + "D6": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 51, + "y": 42, + "z": 1.85 + }, + "D7": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 60, + "y": 42, + "z": 1.85 + }, + "D8": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 69, + "y": 42, + "z": 1.85 + }, + "D9": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 78, + "y": 42, + "z": 1.85 + }, + "E1": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 6, + "y": 33, + "z": 1.85 + }, + "E10": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 87, + "y": 33, + "z": 1.85 + }, + "E11": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 96, + "y": 33, + "z": 1.85 + }, + "E12": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 105, + "y": 33, + "z": 1.85 + }, + "E2": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 15, + "y": 33, + "z": 1.85 + }, + "E3": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 24, + "y": 33, + "z": 1.85 + }, + "E4": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 33, + "y": 33, + "z": 1.85 + }, + "E5": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 42, + "y": 33, + "z": 1.85 + }, + "E6": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 51, + "y": 33, + "z": 1.85 + }, + "E7": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 60, + "y": 33, + "z": 1.85 + }, + "E8": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 69, + "y": 33, + "z": 1.85 + }, + "E9": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 78, + "y": 33, + "z": 1.85 + }, + "F1": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 6, + "y": 24, + "z": 1.85 + }, + "F10": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 87, + "y": 24, + "z": 1.85 + }, + "F11": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 96, + "y": 24, + "z": 1.85 + }, + "F12": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 105, + "y": 24, + "z": 1.85 + }, + "F2": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 15, + "y": 24, + "z": 1.85 + }, + "F3": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 24, + "y": 24, + "z": 1.85 + }, + "F4": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 33, + "y": 24, + "z": 1.85 + }, + "F5": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 42, + "y": 24, + "z": 1.85 + }, + "F6": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 51, + "y": 24, + "z": 1.85 + }, + "F7": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 60, + "y": 24, + "z": 1.85 + }, + "F8": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 69, + "y": 24, + "z": 1.85 + }, + "F9": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 78, + "y": 24, + "z": 1.85 + }, + "G1": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 6, + "y": 15, + "z": 1.85 + }, + "G10": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 87, + "y": 15, + "z": 1.85 + }, + "G11": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 96, + "y": 15, + "z": 1.85 + }, + "G12": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 105, + "y": 15, + "z": 1.85 + }, + "G2": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 15, + "y": 15, + "z": 1.85 + }, + "G3": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 24, + "y": 15, + "z": 1.85 + }, + "G4": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 33, + "y": 15, + "z": 1.85 + }, + "G5": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 42, + "y": 15, + "z": 1.85 + }, + "G6": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 51, + "y": 15, + "z": 1.85 + }, + "G7": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 60, + "y": 15, + "z": 1.85 + }, + "G8": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 69, + "y": 15, + "z": 1.85 + }, + "G9": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 78, + "y": 15, + "z": 1.85 + }, + "H1": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 6, + "y": 6, + "z": 1.85 + }, + "H10": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 87, + "y": 6, + "z": 1.85 + }, + "H11": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 96, + "y": 6, + "z": 1.85 + }, + "H12": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 105, + "y": 6, + "z": 1.85 + }, + "H2": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 15, + "y": 6, + "z": 1.85 + }, + "H3": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 24, + "y": 6, + "z": 1.85 + }, + "H4": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 33, + "y": 6, + "z": 1.85 + }, + "H5": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 42, + "y": 6, + "z": 1.85 + }, + "H6": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 51, + "y": 6, + "z": 1.85 + }, + "H7": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 60, + "y": 6, + "z": 1.85 + }, + "H8": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 69, + "y": 6, + "z": 1.85 + }, + "H9": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 78, + "y": 6, + "z": 1.85 + } + } + } + }, + "status": "succeeded" + }, + { + "commandType": "loadLabware", + "notes": [], + "params": { + "loadName": "nest_96_wellplate_100ul_pcr_full_skirt", + "location": {}, + "namespace": "opentrons", + "version": 2 + }, + "result": { + "definition": { + "allowedRoles": [], + "brand": { + "brand": "NEST", + "brandId": [ + "402501" + ], + "links": [ + "https://www.nest-biotech.com/pcr-plates/58773587.html" + ] + }, + "cornerOffsetFromSlot": { + "x": 0, + "y": 0, + "z": 0 + }, + "dimensions": { + "xDimension": 127.76, + "yDimension": 85.48, + "zDimension": 15.7 + }, + "gripForce": 15.0, + "gripHeightFromLabwareBottom": 10.65, + "gripperOffsets": {}, + "groups": [ + { + "metadata": { + "wellBottomShape": "v" + }, + "wells": [ + "A1", + "A10", + "A11", + "A12", + "A2", + "A3", + "A4", + "A5", + "A6", + "A7", + "A8", + "A9", + "B1", + "B10", + "B11", + "B12", + "B2", + "B3", + "B4", + "B5", + "B6", + "B7", + "B8", + "B9", + "C1", + "C10", + "C11", + "C12", + "C2", + "C3", + "C4", + "C5", + "C6", + "C7", + "C8", + "C9", + "D1", + "D10", + "D11", + "D12", + "D2", + "D3", + "D4", + "D5", + "D6", + "D7", + "D8", + "D9", + "E1", + "E10", + "E11", + "E12", + "E2", + "E3", + "E4", + "E5", + "E6", + "E7", + "E8", + "E9", + "F1", + "F10", + "F11", + "F12", + "F2", + "F3", + "F4", + "F5", + "F6", + "F7", + "F8", + "F9", + "G1", + "G10", + "G11", + "G12", + "G2", + "G3", + "G4", + "G5", + "G6", + "G7", + "G8", + "G9", + "H1", + "H10", + "H11", + "H12", + "H2", + "H3", + "H4", + "H5", + "H6", + "H7", + "H8", + "H9" + ] + } + ], + "metadata": { + "displayCategory": "wellPlate", + "displayName": "NEST 96 Well Plate 100 µL PCR Full Skirt", + "displayVolumeUnits": "µL", + "tags": [] + }, + "namespace": "opentrons", + "ordering": [ + [ + "A1", + "B1", + "C1", + "D1", + "E1", + "F1", + "G1", + "H1" + ], + [ + "A10", + "B10", + "C10", + "D10", + "E10", + "F10", + "G10", + "H10" + ], + [ + "A11", + "B11", + "C11", + "D11", + "E11", + "F11", + "G11", + "H11" + ], + [ + "A12", + "B12", + "C12", + "D12", + "E12", + "F12", + "G12", + "H12" + ], + [ + "A2", + "B2", + "C2", + "D2", + "E2", + "F2", + "G2", + "H2" + ], + [ + "A3", + "B3", + "C3", + "D3", + "E3", + "F3", + "G3", + "H3" + ], + [ + "A4", + "B4", + "C4", + "D4", + "E4", + "F4", + "G4", + "H4" + ], + [ + "A5", + "B5", + "C5", + "D5", + "E5", + "F5", + "G5", + "H5" + ], + [ + "A6", + "B6", + "C6", + "D6", + "E6", + "F6", + "G6", + "H6" + ], + [ + "A7", + "B7", + "C7", + "D7", + "E7", + "F7", + "G7", + "H7" + ], + [ + "A8", + "B8", + "C8", + "D8", + "E8", + "F8", + "G8", + "H8" + ], + [ + "A9", + "B9", + "C9", + "D9", + "E9", + "F9", + "G9", + "H9" + ] + ], + "parameters": { + "format": "96Standard", + "isMagneticModuleCompatible": true, + "isTiprack": false, + "loadName": "nest_96_wellplate_100ul_pcr_full_skirt", + "magneticModuleEngageHeight": 20 + }, + "schemaVersion": 2, + "stackingOffsetWithLabware": { + "opentrons_96_pcr_adapter": { + "x": 0, + "y": 0, + "z": 10.2 + }, + "opentrons_96_well_aluminum_block": { + "x": 0, + "y": 0, + "z": 12.66 + } + }, + "stackingOffsetWithModule": { + "thermocyclerModuleV2": { + "x": 0, + "y": 0, + "z": 10.8 + } + }, + "version": 2, + "wells": { + "A1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 74.24, + "z": 0.92 + }, + "A10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 74.24, + "z": 0.92 + }, + "A11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 74.24, + "z": 0.92 + }, + "A12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 74.24, + "z": 0.92 + }, + "A2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 74.24, + "z": 0.92 + }, + "A3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 74.24, + "z": 0.92 + }, + "A4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 74.24, + "z": 0.92 + }, + "A5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 74.24, + "z": 0.92 + }, + "A6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 74.24, + "z": 0.92 + }, + "A7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 74.24, + "z": 0.92 + }, + "A8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 74.24, + "z": 0.92 + }, + "A9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 74.24, + "z": 0.92 + }, + "B1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 65.24, + "z": 0.92 + }, + "B10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 65.24, + "z": 0.92 + }, + "B11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 65.24, + "z": 0.92 + }, + "B12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 65.24, + "z": 0.92 + }, + "B2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 65.24, + "z": 0.92 + }, + "B3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 65.24, + "z": 0.92 + }, + "B4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 65.24, + "z": 0.92 + }, + "B5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 65.24, + "z": 0.92 + }, + "B6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 65.24, + "z": 0.92 + }, + "B7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 65.24, + "z": 0.92 + }, + "B8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 65.24, + "z": 0.92 + }, + "B9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 65.24, + "z": 0.92 + }, + "C1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 56.24, + "z": 0.92 + }, + "C10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 56.24, + "z": 0.92 + }, + "C11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 56.24, + "z": 0.92 + }, + "C12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 56.24, + "z": 0.92 + }, + "C2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 56.24, + "z": 0.92 + }, + "C3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 56.24, + "z": 0.92 + }, + "C4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 56.24, + "z": 0.92 + }, + "C5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 56.24, + "z": 0.92 + }, + "C6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 56.24, + "z": 0.92 + }, + "C7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 56.24, + "z": 0.92 + }, + "C8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 56.24, + "z": 0.92 + }, + "C9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 56.24, + "z": 0.92 + }, + "D1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 47.24, + "z": 0.92 + }, + "D10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 47.24, + "z": 0.92 + }, + "D11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 47.24, + "z": 0.92 + }, + "D12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 47.24, + "z": 0.92 + }, + "D2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 47.24, + "z": 0.92 + }, + "D3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 47.24, + "z": 0.92 + }, + "D4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 47.24, + "z": 0.92 + }, + "D5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 47.24, + "z": 0.92 + }, + "D6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 47.24, + "z": 0.92 + }, + "D7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 47.24, + "z": 0.92 + }, + "D8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 47.24, + "z": 0.92 + }, + "D9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 47.24, + "z": 0.92 + }, + "E1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 38.24, + "z": 0.92 + }, + "E10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 38.24, + "z": 0.92 + }, + "E11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 38.24, + "z": 0.92 + }, + "E12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 38.24, + "z": 0.92 + }, + "E2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 38.24, + "z": 0.92 + }, + "E3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 38.24, + "z": 0.92 + }, + "E4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 38.24, + "z": 0.92 + }, + "E5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 38.24, + "z": 0.92 + }, + "E6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 38.24, + "z": 0.92 + }, + "E7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 38.24, + "z": 0.92 + }, + "E8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 38.24, + "z": 0.92 + }, + "E9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 38.24, + "z": 0.92 + }, + "F1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 29.24, + "z": 0.92 + }, + "F10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 29.24, + "z": 0.92 + }, + "F11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 29.24, + "z": 0.92 + }, + "F12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 29.24, + "z": 0.92 + }, + "F2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 29.24, + "z": 0.92 + }, + "F3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 29.24, + "z": 0.92 + }, + "F4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 29.24, + "z": 0.92 + }, + "F5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 29.24, + "z": 0.92 + }, + "F6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 29.24, + "z": 0.92 + }, + "F7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 29.24, + "z": 0.92 + }, + "F8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 29.24, + "z": 0.92 + }, + "F9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 29.24, + "z": 0.92 + }, + "G1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 20.24, + "z": 0.92 + }, + "G10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 20.24, + "z": 0.92 + }, + "G11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 20.24, + "z": 0.92 + }, + "G12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 20.24, + "z": 0.92 + }, + "G2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 20.24, + "z": 0.92 + }, + "G3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 20.24, + "z": 0.92 + }, + "G4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 20.24, + "z": 0.92 + }, + "G5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 20.24, + "z": 0.92 + }, + "G6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 20.24, + "z": 0.92 + }, + "G7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 20.24, + "z": 0.92 + }, + "G8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 20.24, + "z": 0.92 + }, + "G9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 20.24, + "z": 0.92 + }, + "H1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 11.24, + "z": 0.92 + }, + "H10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 11.24, + "z": 0.92 + }, + "H11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 11.24, + "z": 0.92 + }, + "H12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 11.24, + "z": 0.92 + }, + "H2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 11.24, + "z": 0.92 + }, + "H3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 11.24, + "z": 0.92 + }, + "H4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 11.24, + "z": 0.92 + }, + "H5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 11.24, + "z": 0.92 + }, + "H6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 11.24, + "z": 0.92 + }, + "H7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 11.24, + "z": 0.92 + }, + "H8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 11.24, + "z": 0.92 + }, + "H9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 11.24, + "z": 0.92 + } + } + } + }, + "status": "succeeded" + }, + { + "commandType": "loadLabware", + "notes": [], + "params": { + "loadName": "nest_96_wellplate_100ul_pcr_full_skirt", + "location": {}, + "namespace": "opentrons", + "version": 2 + }, + "result": { + "definition": { + "allowedRoles": [], + "brand": { + "brand": "NEST", + "brandId": [ + "402501" + ], + "links": [ + "https://www.nest-biotech.com/pcr-plates/58773587.html" + ] + }, + "cornerOffsetFromSlot": { + "x": 0, + "y": 0, + "z": 0 + }, + "dimensions": { + "xDimension": 127.76, + "yDimension": 85.48, + "zDimension": 15.7 + }, + "gripForce": 15.0, + "gripHeightFromLabwareBottom": 10.65, + "gripperOffsets": {}, + "groups": [ + { + "metadata": { + "wellBottomShape": "v" + }, + "wells": [ + "A1", + "A10", + "A11", + "A12", + "A2", + "A3", + "A4", + "A5", + "A6", + "A7", + "A8", + "A9", + "B1", + "B10", + "B11", + "B12", + "B2", + "B3", + "B4", + "B5", + "B6", + "B7", + "B8", + "B9", + "C1", + "C10", + "C11", + "C12", + "C2", + "C3", + "C4", + "C5", + "C6", + "C7", + "C8", + "C9", + "D1", + "D10", + "D11", + "D12", + "D2", + "D3", + "D4", + "D5", + "D6", + "D7", + "D8", + "D9", + "E1", + "E10", + "E11", + "E12", + "E2", + "E3", + "E4", + "E5", + "E6", + "E7", + "E8", + "E9", + "F1", + "F10", + "F11", + "F12", + "F2", + "F3", + "F4", + "F5", + "F6", + "F7", + "F8", + "F9", + "G1", + "G10", + "G11", + "G12", + "G2", + "G3", + "G4", + "G5", + "G6", + "G7", + "G8", + "G9", + "H1", + "H10", + "H11", + "H12", + "H2", + "H3", + "H4", + "H5", + "H6", + "H7", + "H8", + "H9" + ] + } + ], + "metadata": { + "displayCategory": "wellPlate", + "displayName": "NEST 96 Well Plate 100 µL PCR Full Skirt", + "displayVolumeUnits": "µL", + "tags": [] + }, + "namespace": "opentrons", + "ordering": [ + [ + "A1", + "B1", + "C1", + "D1", + "E1", + "F1", + "G1", + "H1" + ], + [ + "A10", + "B10", + "C10", + "D10", + "E10", + "F10", + "G10", + "H10" + ], + [ + "A11", + "B11", + "C11", + "D11", + "E11", + "F11", + "G11", + "H11" + ], + [ + "A12", + "B12", + "C12", + "D12", + "E12", + "F12", + "G12", + "H12" + ], + [ + "A2", + "B2", + "C2", + "D2", + "E2", + "F2", + "G2", + "H2" + ], + [ + "A3", + "B3", + "C3", + "D3", + "E3", + "F3", + "G3", + "H3" + ], + [ + "A4", + "B4", + "C4", + "D4", + "E4", + "F4", + "G4", + "H4" + ], + [ + "A5", + "B5", + "C5", + "D5", + "E5", + "F5", + "G5", + "H5" + ], + [ + "A6", + "B6", + "C6", + "D6", + "E6", + "F6", + "G6", + "H6" + ], + [ + "A7", + "B7", + "C7", + "D7", + "E7", + "F7", + "G7", + "H7" + ], + [ + "A8", + "B8", + "C8", + "D8", + "E8", + "F8", + "G8", + "H8" + ], + [ + "A9", + "B9", + "C9", + "D9", + "E9", + "F9", + "G9", + "H9" + ] + ], + "parameters": { + "format": "96Standard", + "isMagneticModuleCompatible": true, + "isTiprack": false, + "loadName": "nest_96_wellplate_100ul_pcr_full_skirt", + "magneticModuleEngageHeight": 20 + }, + "schemaVersion": 2, + "stackingOffsetWithLabware": { + "opentrons_96_pcr_adapter": { + "x": 0, + "y": 0, + "z": 10.2 + }, + "opentrons_96_well_aluminum_block": { + "x": 0, + "y": 0, + "z": 12.66 + } + }, + "stackingOffsetWithModule": { + "thermocyclerModuleV2": { + "x": 0, + "y": 0, + "z": 10.8 + } + }, + "version": 2, + "wells": { + "A1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 74.24, + "z": 0.92 + }, + "A10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 74.24, + "z": 0.92 + }, + "A11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 74.24, + "z": 0.92 + }, + "A12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 74.24, + "z": 0.92 + }, + "A2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 74.24, + "z": 0.92 + }, + "A3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 74.24, + "z": 0.92 + }, + "A4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 74.24, + "z": 0.92 + }, + "A5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 74.24, + "z": 0.92 + }, + "A6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 74.24, + "z": 0.92 + }, + "A7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 74.24, + "z": 0.92 + }, + "A8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 74.24, + "z": 0.92 + }, + "A9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 74.24, + "z": 0.92 + }, + "B1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 65.24, + "z": 0.92 + }, + "B10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 65.24, + "z": 0.92 + }, + "B11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 65.24, + "z": 0.92 + }, + "B12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 65.24, + "z": 0.92 + }, + "B2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 65.24, + "z": 0.92 + }, + "B3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 65.24, + "z": 0.92 + }, + "B4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 65.24, + "z": 0.92 + }, + "B5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 65.24, + "z": 0.92 + }, + "B6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 65.24, + "z": 0.92 + }, + "B7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 65.24, + "z": 0.92 + }, + "B8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 65.24, + "z": 0.92 + }, + "B9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 65.24, + "z": 0.92 + }, + "C1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 56.24, + "z": 0.92 + }, + "C10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 56.24, + "z": 0.92 + }, + "C11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 56.24, + "z": 0.92 + }, + "C12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 56.24, + "z": 0.92 + }, + "C2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 56.24, + "z": 0.92 + }, + "C3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 56.24, + "z": 0.92 + }, + "C4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 56.24, + "z": 0.92 + }, + "C5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 56.24, + "z": 0.92 + }, + "C6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 56.24, + "z": 0.92 + }, + "C7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 56.24, + "z": 0.92 + }, + "C8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 56.24, + "z": 0.92 + }, + "C9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 56.24, + "z": 0.92 + }, + "D1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 47.24, + "z": 0.92 + }, + "D10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 47.24, + "z": 0.92 + }, + "D11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 47.24, + "z": 0.92 + }, + "D12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 47.24, + "z": 0.92 + }, + "D2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 47.24, + "z": 0.92 + }, + "D3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 47.24, + "z": 0.92 + }, + "D4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 47.24, + "z": 0.92 + }, + "D5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 47.24, + "z": 0.92 + }, + "D6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 47.24, + "z": 0.92 + }, + "D7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 47.24, + "z": 0.92 + }, + "D8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 47.24, + "z": 0.92 + }, + "D9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 47.24, + "z": 0.92 + }, + "E1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 38.24, + "z": 0.92 + }, + "E10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 38.24, + "z": 0.92 + }, + "E11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 38.24, + "z": 0.92 + }, + "E12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 38.24, + "z": 0.92 + }, + "E2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 38.24, + "z": 0.92 + }, + "E3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 38.24, + "z": 0.92 + }, + "E4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 38.24, + "z": 0.92 + }, + "E5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 38.24, + "z": 0.92 + }, + "E6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 38.24, + "z": 0.92 + }, + "E7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 38.24, + "z": 0.92 + }, + "E8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 38.24, + "z": 0.92 + }, + "E9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 38.24, + "z": 0.92 + }, + "F1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 29.24, + "z": 0.92 + }, + "F10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 29.24, + "z": 0.92 + }, + "F11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 29.24, + "z": 0.92 + }, + "F12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 29.24, + "z": 0.92 + }, + "F2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 29.24, + "z": 0.92 + }, + "F3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 29.24, + "z": 0.92 + }, + "F4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 29.24, + "z": 0.92 + }, + "F5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 29.24, + "z": 0.92 + }, + "F6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 29.24, + "z": 0.92 + }, + "F7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 29.24, + "z": 0.92 + }, + "F8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 29.24, + "z": 0.92 + }, + "F9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 29.24, + "z": 0.92 + }, + "G1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 20.24, + "z": 0.92 + }, + "G10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 20.24, + "z": 0.92 + }, + "G11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 20.24, + "z": 0.92 + }, + "G12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 20.24, + "z": 0.92 + }, + "G2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 20.24, + "z": 0.92 + }, + "G3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 20.24, + "z": 0.92 + }, + "G4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 20.24, + "z": 0.92 + }, + "G5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 20.24, + "z": 0.92 + }, + "G6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 20.24, + "z": 0.92 + }, + "G7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 20.24, + "z": 0.92 + }, + "G8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 20.24, + "z": 0.92 + }, + "G9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 20.24, + "z": 0.92 + }, + "H1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 11.24, + "z": 0.92 + }, + "H10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 11.24, + "z": 0.92 + }, + "H11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 11.24, + "z": 0.92 + }, + "H12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 11.24, + "z": 0.92 + }, + "H2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 11.24, + "z": 0.92 + }, + "H3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 11.24, + "z": 0.92 + }, + "H4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 11.24, + "z": 0.92 + }, + "H5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 11.24, + "z": 0.92 + }, + "H6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 11.24, + "z": 0.92 + }, + "H7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 11.24, + "z": 0.92 + }, + "H8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 11.24, + "z": 0.92 + }, + "H9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 11.24, + "z": 0.92 + } + } + } + }, + "status": "succeeded" + }, + { + "commandType": "loadLabware", + "notes": [], + "params": { + "displayName": "4 custom tubes", + "loadName": "cpx_4_tuberack_100ul", + "location": { + "slotName": "6" + }, + "namespace": "custom_beta", + "version": 1 + }, + "result": { + "definition": { + "allowedRoles": [], + "brand": { + "brand": "cpx", + "brandId": [] + }, + "cornerOffsetFromSlot": { + "x": 0, + "y": 0, + "z": 0 + }, + "dimensions": { + "xDimension": 127, + "yDimension": 85, + "zDimension": 40 + }, + "gripperOffsets": {}, + "groups": [ + { + "brand": { + "brand": "cpx", + "brandId": [] + }, + "metadata": { + "displayCategory": "tubeRack", + "wellBottomShape": "u" + }, + "wells": [ + "A1", + "A2", + "B1", + "B2" + ] + } + ], + "metadata": { + "displayCategory": "tubeRack", + "displayName": "cpx 4 Tube Rack with cpx 0.1 mL", + "displayVolumeUnits": "µL", + "tags": [] + }, + "namespace": "custom_beta", + "ordering": [ + [ + "A1", + "B1" + ], + [ + "A2", + "B2" + ] + ], + "parameters": { + "format": "irregular", + "isMagneticModuleCompatible": false, + "isTiprack": false, + "loadName": "cpx_4_tuberack_100ul", + "quirks": [] + }, + "schemaVersion": 2, + "stackingOffsetWithLabware": {}, + "stackingOffsetWithModule": {}, + "version": 1, + "wells": { + "A1": { + "depth": 23, + "diameter": 20, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 20, + "y": 65, + "z": 17 + }, + "A2": { + "depth": 23, + "diameter": 20, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50, + "y": 65, + "z": 17 + }, + "B1": { + "depth": 23, + "diameter": 20, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 20, + "y": 35, + "z": 17 + }, + "B2": { + "depth": 23, + "diameter": 20, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50, + "y": 35, + "z": 17 + } + } + } + }, + "status": "succeeded" + }, + { + "commandType": "loadLabware", + "notes": [], + "params": { + "displayName": "logo destination", + "loadName": "nest_96_wellplate_100ul_pcr_full_skirt", + "location": { + "slotName": "2" + }, + "namespace": "opentrons", + "version": 2 + }, + "result": { + "definition": { + "allowedRoles": [], + "brand": { + "brand": "NEST", + "brandId": [ + "402501" + ], + "links": [ + "https://www.nest-biotech.com/pcr-plates/58773587.html" + ] + }, + "cornerOffsetFromSlot": { + "x": 0, + "y": 0, + "z": 0 + }, + "dimensions": { + "xDimension": 127.76, + "yDimension": 85.48, + "zDimension": 15.7 + }, + "gripForce": 15.0, + "gripHeightFromLabwareBottom": 10.65, + "gripperOffsets": {}, + "groups": [ + { + "metadata": { + "wellBottomShape": "v" + }, + "wells": [ + "A1", + "A10", + "A11", + "A12", + "A2", + "A3", + "A4", + "A5", + "A6", + "A7", + "A8", + "A9", + "B1", + "B10", + "B11", + "B12", + "B2", + "B3", + "B4", + "B5", + "B6", + "B7", + "B8", + "B9", + "C1", + "C10", + "C11", + "C12", + "C2", + "C3", + "C4", + "C5", + "C6", + "C7", + "C8", + "C9", + "D1", + "D10", + "D11", + "D12", + "D2", + "D3", + "D4", + "D5", + "D6", + "D7", + "D8", + "D9", + "E1", + "E10", + "E11", + "E12", + "E2", + "E3", + "E4", + "E5", + "E6", + "E7", + "E8", + "E9", + "F1", + "F10", + "F11", + "F12", + "F2", + "F3", + "F4", + "F5", + "F6", + "F7", + "F8", + "F9", + "G1", + "G10", + "G11", + "G12", + "G2", + "G3", + "G4", + "G5", + "G6", + "G7", + "G8", + "G9", + "H1", + "H10", + "H11", + "H12", + "H2", + "H3", + "H4", + "H5", + "H6", + "H7", + "H8", + "H9" + ] + } + ], + "metadata": { + "displayCategory": "wellPlate", + "displayName": "NEST 96 Well Plate 100 µL PCR Full Skirt", + "displayVolumeUnits": "µL", + "tags": [] + }, + "namespace": "opentrons", + "ordering": [ + [ + "A1", + "B1", + "C1", + "D1", + "E1", + "F1", + "G1", + "H1" + ], + [ + "A10", + "B10", + "C10", + "D10", + "E10", + "F10", + "G10", + "H10" + ], + [ + "A11", + "B11", + "C11", + "D11", + "E11", + "F11", + "G11", + "H11" + ], + [ + "A12", + "B12", + "C12", + "D12", + "E12", + "F12", + "G12", + "H12" + ], + [ + "A2", + "B2", + "C2", + "D2", + "E2", + "F2", + "G2", + "H2" + ], + [ + "A3", + "B3", + "C3", + "D3", + "E3", + "F3", + "G3", + "H3" + ], + [ + "A4", + "B4", + "C4", + "D4", + "E4", + "F4", + "G4", + "H4" + ], + [ + "A5", + "B5", + "C5", + "D5", + "E5", + "F5", + "G5", + "H5" + ], + [ + "A6", + "B6", + "C6", + "D6", + "E6", + "F6", + "G6", + "H6" + ], + [ + "A7", + "B7", + "C7", + "D7", + "E7", + "F7", + "G7", + "H7" + ], + [ + "A8", + "B8", + "C8", + "D8", + "E8", + "F8", + "G8", + "H8" + ], + [ + "A9", + "B9", + "C9", + "D9", + "E9", + "F9", + "G9", + "H9" + ] + ], + "parameters": { + "format": "96Standard", + "isMagneticModuleCompatible": true, + "isTiprack": false, + "loadName": "nest_96_wellplate_100ul_pcr_full_skirt", + "magneticModuleEngageHeight": 20 + }, + "schemaVersion": 2, + "stackingOffsetWithLabware": { + "opentrons_96_pcr_adapter": { + "x": 0, + "y": 0, + "z": 10.2 + }, + "opentrons_96_well_aluminum_block": { + "x": 0, + "y": 0, + "z": 12.66 + } + }, + "stackingOffsetWithModule": { + "thermocyclerModuleV2": { + "x": 0, + "y": 0, + "z": 10.8 + } + }, + "version": 2, + "wells": { + "A1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 74.24, + "z": 0.92 + }, + "A10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 74.24, + "z": 0.92 + }, + "A11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 74.24, + "z": 0.92 + }, + "A12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 74.24, + "z": 0.92 + }, + "A2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 74.24, + "z": 0.92 + }, + "A3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 74.24, + "z": 0.92 + }, + "A4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 74.24, + "z": 0.92 + }, + "A5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 74.24, + "z": 0.92 + }, + "A6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 74.24, + "z": 0.92 + }, + "A7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 74.24, + "z": 0.92 + }, + "A8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 74.24, + "z": 0.92 + }, + "A9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 74.24, + "z": 0.92 + }, + "B1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 65.24, + "z": 0.92 + }, + "B10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 65.24, + "z": 0.92 + }, + "B11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 65.24, + "z": 0.92 + }, + "B12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 65.24, + "z": 0.92 + }, + "B2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 65.24, + "z": 0.92 + }, + "B3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 65.24, + "z": 0.92 + }, + "B4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 65.24, + "z": 0.92 + }, + "B5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 65.24, + "z": 0.92 + }, + "B6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 65.24, + "z": 0.92 + }, + "B7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 65.24, + "z": 0.92 + }, + "B8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 65.24, + "z": 0.92 + }, + "B9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 65.24, + "z": 0.92 + }, + "C1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 56.24, + "z": 0.92 + }, + "C10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 56.24, + "z": 0.92 + }, + "C11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 56.24, + "z": 0.92 + }, + "C12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 56.24, + "z": 0.92 + }, + "C2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 56.24, + "z": 0.92 + }, + "C3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 56.24, + "z": 0.92 + }, + "C4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 56.24, + "z": 0.92 + }, + "C5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 56.24, + "z": 0.92 + }, + "C6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 56.24, + "z": 0.92 + }, + "C7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 56.24, + "z": 0.92 + }, + "C8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 56.24, + "z": 0.92 + }, + "C9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 56.24, + "z": 0.92 + }, + "D1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 47.24, + "z": 0.92 + }, + "D10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 47.24, + "z": 0.92 + }, + "D11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 47.24, + "z": 0.92 + }, + "D12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 47.24, + "z": 0.92 + }, + "D2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 47.24, + "z": 0.92 + }, + "D3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 47.24, + "z": 0.92 + }, + "D4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 47.24, + "z": 0.92 + }, + "D5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 47.24, + "z": 0.92 + }, + "D6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 47.24, + "z": 0.92 + }, + "D7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 47.24, + "z": 0.92 + }, + "D8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 47.24, + "z": 0.92 + }, + "D9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 47.24, + "z": 0.92 + }, + "E1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 38.24, + "z": 0.92 + }, + "E10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 38.24, + "z": 0.92 + }, + "E11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 38.24, + "z": 0.92 + }, + "E12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 38.24, + "z": 0.92 + }, + "E2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 38.24, + "z": 0.92 + }, + "E3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 38.24, + "z": 0.92 + }, + "E4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 38.24, + "z": 0.92 + }, + "E5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 38.24, + "z": 0.92 + }, + "E6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 38.24, + "z": 0.92 + }, + "E7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 38.24, + "z": 0.92 + }, + "E8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 38.24, + "z": 0.92 + }, + "E9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 38.24, + "z": 0.92 + }, + "F1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 29.24, + "z": 0.92 + }, + "F10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 29.24, + "z": 0.92 + }, + "F11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 29.24, + "z": 0.92 + }, + "F12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 29.24, + "z": 0.92 + }, + "F2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 29.24, + "z": 0.92 + }, + "F3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 29.24, + "z": 0.92 + }, + "F4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 29.24, + "z": 0.92 + }, + "F5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 29.24, + "z": 0.92 + }, + "F6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 29.24, + "z": 0.92 + }, + "F7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 29.24, + "z": 0.92 + }, + "F8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 29.24, + "z": 0.92 + }, + "F9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 29.24, + "z": 0.92 + }, + "G1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 20.24, + "z": 0.92 + }, + "G10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 20.24, + "z": 0.92 + }, + "G11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 20.24, + "z": 0.92 + }, + "G12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 20.24, + "z": 0.92 + }, + "G2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 20.24, + "z": 0.92 + }, + "G3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 20.24, + "z": 0.92 + }, + "G4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 20.24, + "z": 0.92 + }, + "G5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 20.24, + "z": 0.92 + }, + "G6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 20.24, + "z": 0.92 + }, + "G7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 20.24, + "z": 0.92 + }, + "G8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 20.24, + "z": 0.92 + }, + "G9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 20.24, + "z": 0.92 + }, + "H1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 11.24, + "z": 0.92 + }, + "H10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 11.24, + "z": 0.92 + }, + "H11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 11.24, + "z": 0.92 + }, + "H12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 11.24, + "z": 0.92 + }, + "H2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 11.24, + "z": 0.92 + }, + "H3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 11.24, + "z": 0.92 + }, + "H4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 11.24, + "z": 0.92 + }, + "H5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 11.24, + "z": 0.92 + }, + "H6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 11.24, + "z": 0.92 + }, + "H7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 11.24, + "z": 0.92 + }, + "H8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 11.24, + "z": 0.92 + }, + "H9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 11.24, + "z": 0.92 + } + } + } + }, + "status": "succeeded" + }, + { + "commandType": "loadLabware", + "notes": [], + "params": { + "displayName": "dye container", + "loadName": "nest_12_reservoir_15ml", + "location": { + "slotName": "3" + }, + "namespace": "opentrons", + "version": 1 + }, + "result": { + "definition": { + "allowedRoles": [], + "brand": { + "brand": "NEST", + "brandId": [ + "360102" + ], + "links": [ + "https://www.nest-biotech.com/reagent-reserviors/59178414.html" + ] + }, + "cornerOffsetFromSlot": { + "x": 0, + "y": 0, + "z": 0 + }, + "dimensions": { + "xDimension": 127.76, + "yDimension": 85.48, + "zDimension": 31.4 + }, + "gripperOffsets": {}, + "groups": [ + { + "metadata": { + "wellBottomShape": "v" + }, + "wells": [ + "A1", + "A10", + "A11", + "A12", + "A2", + "A3", + "A4", + "A5", + "A6", + "A7", + "A8", + "A9" + ] + } + ], + "metadata": { + "displayCategory": "reservoir", + "displayName": "NEST 12 Well Reservoir 15 mL", + "displayVolumeUnits": "mL", + "tags": [] + }, + "namespace": "opentrons", + "ordering": [ + [ + "A1" + ], + [ + "A10" + ], + [ + "A11" + ], + [ + "A12" + ], + [ + "A2" + ], + [ + "A3" + ], + [ + "A4" + ], + [ + "A5" + ], + [ + "A6" + ], + [ + "A7" + ], + [ + "A8" + ], + [ + "A9" + ] + ], + "parameters": { + "format": "trough", + "isMagneticModuleCompatible": false, + "isTiprack": false, + "loadName": "nest_12_reservoir_15ml", + "quirks": [ + "centerMultichannelOnWells", + "touchTipDisabled" + ] + }, + "schemaVersion": 2, + "stackingOffsetWithLabware": {}, + "stackingOffsetWithModule": {}, + "version": 1, + "wells": { + "A1": { + "depth": 26.85, + "shape": "rectangular", + "totalLiquidVolume": 15000, + "x": 14.38, + "xDimension": 8.2, + "y": 42.78, + "yDimension": 71.2, + "z": 4.55 + }, + "A10": { + "depth": 26.85, + "shape": "rectangular", + "totalLiquidVolume": 15000, + "x": 95.38, + "xDimension": 8.2, + "y": 42.78, + "yDimension": 71.2, + "z": 4.55 + }, + "A11": { + "depth": 26.85, + "shape": "rectangular", + "totalLiquidVolume": 15000, + "x": 104.38, + "xDimension": 8.2, + "y": 42.78, + "yDimension": 71.2, + "z": 4.55 + }, + "A12": { + "depth": 26.85, + "shape": "rectangular", + "totalLiquidVolume": 15000, + "x": 113.38, + "xDimension": 8.2, + "y": 42.78, + "yDimension": 71.2, + "z": 4.55 + }, + "A2": { + "depth": 26.85, + "shape": "rectangular", + "totalLiquidVolume": 15000, + "x": 23.38, + "xDimension": 8.2, + "y": 42.78, + "yDimension": 71.2, + "z": 4.55 + }, + "A3": { + "depth": 26.85, + "shape": "rectangular", + "totalLiquidVolume": 15000, + "x": 32.38, + "xDimension": 8.2, + "y": 42.78, + "yDimension": 71.2, + "z": 4.55 + }, + "A4": { + "depth": 26.85, + "shape": "rectangular", + "totalLiquidVolume": 15000, + "x": 41.38, + "xDimension": 8.2, + "y": 42.78, + "yDimension": 71.2, + "z": 4.55 + }, + "A5": { + "depth": 26.85, + "shape": "rectangular", + "totalLiquidVolume": 15000, + "x": 50.38, + "xDimension": 8.2, + "y": 42.78, + "yDimension": 71.2, + "z": 4.55 + }, + "A6": { + "depth": 26.85, + "shape": "rectangular", + "totalLiquidVolume": 15000, + "x": 59.38, + "xDimension": 8.2, + "y": 42.78, + "yDimension": 71.2, + "z": 4.55 + }, + "A7": { + "depth": 26.85, + "shape": "rectangular", + "totalLiquidVolume": 15000, + "x": 68.38, + "xDimension": 8.2, + "y": 42.78, + "yDimension": 71.2, + "z": 4.55 + }, + "A8": { + "depth": 26.85, + "shape": "rectangular", + "totalLiquidVolume": 15000, + "x": 77.38, + "xDimension": 8.2, + "y": 42.78, + "yDimension": 71.2, + "z": 4.55 + }, + "A9": { + "depth": 26.85, + "shape": "rectangular", + "totalLiquidVolume": 15000, + "x": 86.38, + "xDimension": 8.2, + "y": 42.78, + "yDimension": 71.2, + "z": 4.55 + } + } + } + }, + "status": "succeeded" + }, + { + "commandType": "loadLiquid", + "notes": [], + "params": { + "volumeByWell": { + "A1": 4000.0 + } + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "loadLiquid", + "notes": [], + "params": { + "volumeByWell": { + "A2": 2000.0 + } + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "loadLiquid", + "notes": [], + "params": { + "volumeByWell": { + "A5": 555.55555 + } + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "loadLiquid", + "notes": [], + "params": { + "volumeByWell": { + "A8": 900.0 + } + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "loadLiquid", + "notes": [], + "params": { + "volumeByWell": { + "A8": 1001.11 + } + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "heaterShaker/closeLabwareLatch", + "notes": [], + "params": {}, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "pickUpTip", + "notes": [], + "params": { + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "origin": "top" + }, + "wellName": "A1" + }, + "result": { + "position": { + "x": 14.38, + "y": 164.74, + "z": 64.69 + }, + "tipDiameter": 3.27, + "tipLength": 30.950000000000003, + "tipVolume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "moveLabware", + "notes": [], + "params": { + "newLocation": "offDeck", + "strategy": "manualMoveWithPause" + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "moveLabware", + "notes": [], + "params": { + "newLocation": { + "slotName": "2" + }, + "strategy": "manualMoveWithPause" + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "moveToWell", + "notes": [], + "params": { + "forceDirect": false, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "origin": "top" + }, + "wellName": "A1" + }, + "result": { + "position": { + "x": 146.88, + "y": 42.78, + "z": 31.400000000000002 + } + }, + "status": "succeeded" + }, + { + "commandType": "waitForResume", + "notes": [], + "params": { + "message": "Is the pipette tip in the middle of reservoir A1 in slot 2?" + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "moveLabware", + "notes": [], + "params": { + "newLocation": { + "slotName": "3" + }, + "strategy": "manualMoveWithPause" + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "moveToWell", + "notes": [], + "params": { + "forceDirect": false, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "origin": "top" + }, + "wellName": "A1" + }, + "result": { + "position": { + "x": 279.38, + "y": 42.78, + "z": 31.400000000000002 + } + }, + "status": "succeeded" + }, + { + "commandType": "waitForResume", + "notes": [], + "params": { + "message": "Is the pipette tip in the middle of reservoir A1 in slot 3?" + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "moveLabware", + "notes": [], + "params": { + "newLocation": { + "slotName": "2" + }, + "strategy": "manualMoveWithPause" + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "moveToWell", + "notes": [], + "params": { + "forceDirect": false, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "origin": "top" + }, + "wellName": "A1" + }, + "result": { + "position": { + "x": 152.5, + "y": 65.0, + "z": 40.0 + } + }, + "status": "succeeded" + }, + { + "commandType": "waitForResume", + "notes": [], + "params": { + "message": "Is the pipette tip in the middle of custom labware A1 in slot 2?" + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "moveLabware", + "notes": [], + "params": { + "newLocation": { + "slotName": "6" + }, + "strategy": "manualMoveWithPause" + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "moveToWell", + "notes": [], + "params": { + "forceDirect": false, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "origin": "top" + }, + "wellName": "A1" + }, + "result": { + "position": { + "x": 285.0, + "y": 155.5, + "z": 40.0 + } + }, + "status": "succeeded" + }, + { + "commandType": "waitForResume", + "notes": [], + "params": { + "message": "Is the pipette tip in the middle of custom labware A1 in slot 6?" + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "moveLabware", + "notes": [], + "params": { + "newLocation": { + "slotName": "2" + }, + "strategy": "manualMoveWithPause" + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "moveToWell", + "notes": [], + "params": { + "forceDirect": false, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "origin": "top" + }, + "wellName": "A1" + }, + "result": { + "position": { + "x": 146.88, + "y": 74.24, + "z": 15.7 + } + }, + "status": "succeeded" + }, + { + "commandType": "waitForResume", + "notes": [], + "params": { + "message": "Is the pipette tip in the middle of well A1 in slot 2?" + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "prepareToAspirate", + "notes": [], + "params": {}, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "moveToWell", + "notes": [], + "params": { + "forceDirect": false, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -24.85 + }, + "origin": "top" + }, + "wellName": "A1" + }, + "result": { + "position": { + "x": 279.38, + "y": 42.78, + "z": 6.55 + } + }, + "status": "succeeded" + }, + { + "commandType": "waitForResume", + "notes": [], + "params": { + "message": "Testing prepare_to_aspirate - watch pipette until next pause.\n The pipette should only move up out of the well after it has aspirated." + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 10.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -24.85 + }, + "origin": "top" + }, + "wellName": "A1" + }, + "result": { + "position": { + "x": 279.38, + "y": 42.78, + "z": 6.55 + }, + "volume": 10.0 + }, + "status": "succeeded" + }, + { + "commandType": "waitForResume", + "notes": [], + "params": { + "message": "Did the pipette move up out of the well, only once, after aspirating?" + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 10.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -24.85 + }, + "origin": "top" + }, + "wellName": "A1" + }, + "result": { + "position": { + "x": 279.38, + "y": 42.78, + "z": 6.55 + }, + "volume": 10.0 + }, + "status": "succeeded" + }, + { + "commandType": "moveToAddressableAreaForDropTip", + "notes": [], + "params": { + "addressableAreaName": "fixedTrash", + "alternateDropLocation": false, + "forceDirect": false, + "ignoreTipConfiguration": true, + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + } + }, + "result": { + "position": { + "x": 347.84000000000003, + "y": 351.5, + "z": 82.0 + } + }, + "status": "succeeded" + }, + { + "commandType": "waitForResume", + "notes": [], + "params": { + "message": "Is the pipette over the trash? Pipette will home after this pause." + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "home", + "notes": [], + "params": {}, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "moveToAddressableAreaForDropTip", + "notes": [], + "params": { + "addressableAreaName": "fixedTrash", + "alternateDropLocation": false, + "forceDirect": false, + "ignoreTipConfiguration": true, + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + } + }, + "result": { + "position": { + "x": 347.84000000000003, + "y": 351.5, + "z": 82.0 + } + }, + "status": "succeeded" + }, + { + "commandType": "waitForResume", + "notes": [], + "params": { + "message": "Is the pipette over the trash?" + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 19.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 19.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 18.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "C7" + }, + "result": { + "position": { + "x": 200.88, + "y": 56.24, + "z": 1.92 + }, + "volume": 18.0 + }, + "status": "succeeded" + }, + { + "commandType": "moveToAddressableAreaForDropTip", + "notes": [], + "params": { + "addressableAreaName": "fixedTrash", + "alternateDropLocation": false, + "forceDirect": false, + "ignoreTipConfiguration": true, + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + } + }, + "result": { + "position": { + "x": 347.84000000000003, + "y": 351.5, + "z": 82.0 + } + }, + "status": "succeeded" + }, + { + "commandType": "blowOutInPlace", + "notes": [], + "params": { + "flowRate": 7.56 + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 19.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 19.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 18.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "D6" + }, + "result": { + "position": { + "x": 191.88, + "y": 47.24, + "z": 1.92 + }, + "volume": 18.0 + }, + "status": "succeeded" + }, + { + "commandType": "moveToAddressableAreaForDropTip", + "notes": [], + "params": { + "addressableAreaName": "fixedTrash", + "alternateDropLocation": false, + "forceDirect": false, + "ignoreTipConfiguration": true, + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + } + }, + "result": { + "position": { + "x": 347.84000000000003, + "y": 351.5, + "z": 82.0 + } + }, + "status": "succeeded" + }, + { + "commandType": "blowOutInPlace", + "notes": [], + "params": { + "flowRate": 7.56 + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 19.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 19.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 18.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "D7" + }, + "result": { + "position": { + "x": 200.88, + "y": 47.24, + "z": 1.92 + }, + "volume": 18.0 + }, + "status": "succeeded" + }, + { + "commandType": "moveToAddressableAreaForDropTip", + "notes": [], + "params": { + "addressableAreaName": "fixedTrash", + "alternateDropLocation": false, + "forceDirect": false, + "ignoreTipConfiguration": true, + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + } + }, + "result": { + "position": { + "x": 347.84000000000003, + "y": 351.5, + "z": 82.0 + } + }, + "status": "succeeded" + }, + { + "commandType": "blowOutInPlace", + "notes": [], + "params": { + "flowRate": 7.56 + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 19.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 19.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 18.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "D8" + }, + "result": { + "position": { + "x": 209.88, + "y": 47.24, + "z": 1.92 + }, + "volume": 18.0 + }, + "status": "succeeded" + }, + { + "commandType": "moveToAddressableAreaForDropTip", + "notes": [], + "params": { + "addressableAreaName": "fixedTrash", + "alternateDropLocation": false, + "forceDirect": false, + "ignoreTipConfiguration": true, + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + } + }, + "result": { + "position": { + "x": 347.84000000000003, + "y": 351.5, + "z": 82.0 + } + }, + "status": "succeeded" + }, + { + "commandType": "blowOutInPlace", + "notes": [], + "params": { + "flowRate": 7.56 + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 19.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 19.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 18.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "E5" + }, + "result": { + "position": { + "x": 182.88, + "y": 38.24, + "z": 1.92 + }, + "volume": 18.0 + }, + "status": "succeeded" + }, + { + "commandType": "moveToAddressableAreaForDropTip", + "notes": [], + "params": { + "addressableAreaName": "fixedTrash", + "alternateDropLocation": false, + "forceDirect": false, + "ignoreTipConfiguration": true, + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + } + }, + "result": { + "position": { + "x": 347.84000000000003, + "y": 351.5, + "z": 82.0 + } + }, + "status": "succeeded" + }, + { + "commandType": "blowOutInPlace", + "notes": [], + "params": { + "flowRate": 7.56 + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "moveToAddressableAreaForDropTip", + "notes": [], + "params": { + "addressableAreaName": "fixedTrash", + "alternateDropLocation": true, + "forceDirect": false, + "ignoreTipConfiguration": true, + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + } + }, + "result": { + "position": { + "x": 363.89500000000004, + "y": 351.5, + "z": 82.0 + } + }, + "status": "succeeded" + }, + { + "commandType": "dropTipInPlace", + "notes": [], + "params": {}, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "pickUpTip", + "notes": [], + "params": { + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "origin": "top" + }, + "wellName": "B1" + }, + "result": { + "position": { + "x": 14.38, + "y": 155.74, + "z": 64.69 + }, + "tipDiameter": 3.27, + "tipLength": 30.950000000000003, + "tipVolume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "pushOut": 0.0, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "pushOut": 0.0, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "A11" + }, + "result": { + "position": { + "x": 236.88, + "y": 74.24, + "z": 1.92 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "A11" + }, + "result": { + "position": { + "x": 236.88, + "y": 74.24, + "z": 1.92 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "A11" + }, + "result": { + "position": { + "x": 236.88, + "y": 74.24, + "z": 1.92 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "touchTip", + "notes": [], + "params": { + "radius": 1.0, + "speed": 60.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -1.0 + }, + "origin": "top" + }, + "wellName": "A11" + }, + "result": { + "position": { + "x": 236.88, + "y": 74.24, + "z": 14.7 + } + }, + "status": "succeeded" + }, + { + "commandType": "blowout", + "notes": [], + "params": { + "flowRate": 7.56, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "origin": "top" + }, + "wellName": "A11" + }, + "result": { + "position": { + "x": 236.88, + "y": 74.24, + "z": 15.7 + } + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "pushOut": 0.0, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "pushOut": 0.0, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "A11" + }, + "result": { + "position": { + "x": 236.88, + "y": 74.24, + "z": 1.92 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "A11" + }, + "result": { + "position": { + "x": 236.88, + "y": 74.24, + "z": 1.92 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "A11" + }, + "result": { + "position": { + "x": 236.88, + "y": 74.24, + "z": 1.92 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "touchTip", + "notes": [], + "params": { + "radius": 1.0, + "speed": 60.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -1.0 + }, + "origin": "top" + }, + "wellName": "A11" + }, + "result": { + "position": { + "x": 236.88, + "y": 74.24, + "z": 14.7 + } + }, + "status": "succeeded" + }, + { + "commandType": "blowout", + "notes": [], + "params": { + "flowRate": 7.56, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "origin": "top" + }, + "wellName": "A11" + }, + "result": { + "position": { + "x": 236.88, + "y": 74.24, + "z": 15.7 + } + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "pushOut": 0.0, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "pushOut": 0.0, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "A11" + }, + "result": { + "position": { + "x": 236.88, + "y": 74.24, + "z": 1.92 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "A11" + }, + "result": { + "position": { + "x": 236.88, + "y": 74.24, + "z": 1.92 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "A11" + }, + "result": { + "position": { + "x": 236.88, + "y": 74.24, + "z": 1.92 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "touchTip", + "notes": [], + "params": { + "radius": 1.0, + "speed": 60.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -1.0 + }, + "origin": "top" + }, + "wellName": "A11" + }, + "result": { + "position": { + "x": 236.88, + "y": 74.24, + "z": 14.7 + } + }, + "status": "succeeded" + }, + { + "commandType": "blowout", + "notes": [], + "params": { + "flowRate": 7.56, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "origin": "top" + }, + "wellName": "A11" + }, + "result": { + "position": { + "x": 236.88, + "y": 74.24, + "z": 15.7 + } + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "pushOut": 0.0, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "pushOut": 0.0, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "B11" + }, + "result": { + "position": { + "x": 236.88, + "y": 65.24, + "z": 1.92 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "B11" + }, + "result": { + "position": { + "x": 236.88, + "y": 65.24, + "z": 1.92 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "B11" + }, + "result": { + "position": { + "x": 236.88, + "y": 65.24, + "z": 1.92 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "touchTip", + "notes": [], + "params": { + "radius": 1.0, + "speed": 60.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -1.0 + }, + "origin": "top" + }, + "wellName": "B11" + }, + "result": { + "position": { + "x": 236.88, + "y": 65.24, + "z": 14.7 + } + }, + "status": "succeeded" + }, + { + "commandType": "blowout", + "notes": [], + "params": { + "flowRate": 7.56, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "origin": "top" + }, + "wellName": "B11" + }, + "result": { + "position": { + "x": 236.88, + "y": 65.24, + "z": 15.7 + } + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "pushOut": 0.0, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "pushOut": 0.0, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "B11" + }, + "result": { + "position": { + "x": 236.88, + "y": 65.24, + "z": 1.92 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "B11" + }, + "result": { + "position": { + "x": 236.88, + "y": 65.24, + "z": 1.92 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "B11" + }, + "result": { + "position": { + "x": 236.88, + "y": 65.24, + "z": 1.92 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "touchTip", + "notes": [], + "params": { + "radius": 1.0, + "speed": 60.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -1.0 + }, + "origin": "top" + }, + "wellName": "B11" + }, + "result": { + "position": { + "x": 236.88, + "y": 65.24, + "z": 14.7 + } + }, + "status": "succeeded" + }, + { + "commandType": "blowout", + "notes": [], + "params": { + "flowRate": 7.56, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "origin": "top" + }, + "wellName": "B11" + }, + "result": { + "position": { + "x": 236.88, + "y": 65.24, + "z": 15.7 + } + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "pushOut": 0.0, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "pushOut": 0.0, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "B11" + }, + "result": { + "position": { + "x": 236.88, + "y": 65.24, + "z": 1.92 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "B11" + }, + "result": { + "position": { + "x": 236.88, + "y": 65.24, + "z": 1.92 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "B11" + }, + "result": { + "position": { + "x": 236.88, + "y": 65.24, + "z": 1.92 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "touchTip", + "notes": [], + "params": { + "radius": 1.0, + "speed": 60.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -1.0 + }, + "origin": "top" + }, + "wellName": "B11" + }, + "result": { + "position": { + "x": 236.88, + "y": 65.24, + "z": 14.7 + } + }, + "status": "succeeded" + }, + { + "commandType": "blowout", + "notes": [], + "params": { + "flowRate": 7.56, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "origin": "top" + }, + "wellName": "B11" + }, + "result": { + "position": { + "x": 236.88, + "y": 65.24, + "z": 15.7 + } + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "pushOut": 0.0, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "pushOut": 0.0, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "C11" + }, + "result": { + "position": { + "x": 236.88, + "y": 56.24, + "z": 1.92 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "C11" + }, + "result": { + "position": { + "x": 236.88, + "y": 56.24, + "z": 1.92 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "C11" + }, + "result": { + "position": { + "x": 236.88, + "y": 56.24, + "z": 1.92 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "touchTip", + "notes": [], + "params": { + "radius": 1.0, + "speed": 60.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -1.0 + }, + "origin": "top" + }, + "wellName": "C11" + }, + "result": { + "position": { + "x": 236.88, + "y": 56.24, + "z": 14.7 + } + }, + "status": "succeeded" + }, + { + "commandType": "blowout", + "notes": [], + "params": { + "flowRate": 7.56, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "origin": "top" + }, + "wellName": "C11" + }, + "result": { + "position": { + "x": 236.88, + "y": 56.24, + "z": 15.7 + } + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "pushOut": 0.0, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "pushOut": 0.0, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "C11" + }, + "result": { + "position": { + "x": 236.88, + "y": 56.24, + "z": 1.92 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "C11" + }, + "result": { + "position": { + "x": 236.88, + "y": 56.24, + "z": 1.92 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "C11" + }, + "result": { + "position": { + "x": 236.88, + "y": 56.24, + "z": 1.92 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "touchTip", + "notes": [], + "params": { + "radius": 1.0, + "speed": 60.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -1.0 + }, + "origin": "top" + }, + "wellName": "C11" + }, + "result": { + "position": { + "x": 236.88, + "y": 56.24, + "z": 14.7 + } + }, + "status": "succeeded" + }, + { + "commandType": "blowout", + "notes": [], + "params": { + "flowRate": 7.56, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "origin": "top" + }, + "wellName": "C11" + }, + "result": { + "position": { + "x": 236.88, + "y": 56.24, + "z": 15.7 + } + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "pushOut": 0.0, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "pushOut": 0.0, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "C11" + }, + "result": { + "position": { + "x": 236.88, + "y": 56.24, + "z": 1.92 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "C11" + }, + "result": { + "position": { + "x": 236.88, + "y": 56.24, + "z": 1.92 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "C11" + }, + "result": { + "position": { + "x": 236.88, + "y": 56.24, + "z": 1.92 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "touchTip", + "notes": [], + "params": { + "radius": 1.0, + "speed": 60.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -1.0 + }, + "origin": "top" + }, + "wellName": "C11" + }, + "result": { + "position": { + "x": 236.88, + "y": 56.24, + "z": 14.7 + } + }, + "status": "succeeded" + }, + { + "commandType": "blowout", + "notes": [], + "params": { + "flowRate": 7.56, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "origin": "top" + }, + "wellName": "C11" + }, + "result": { + "position": { + "x": 236.88, + "y": 56.24, + "z": 15.7 + } + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "A11" + }, + "result": { + "position": { + "x": 236.88, + "y": 74.24, + "z": 1.92 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A5" + }, + "result": { + "position": { + "x": 315.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "blowout", + "notes": [], + "params": { + "flowRate": 7.56, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "origin": "top" + }, + "wellName": "A5" + }, + "result": { + "position": { + "x": 315.38, + "y": 42.78, + "z": 31.400000000000002 + } + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "B11" + }, + "result": { + "position": { + "x": 236.88, + "y": 65.24, + "z": 1.92 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A5" + }, + "result": { + "position": { + "x": 315.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "blowout", + "notes": [], + "params": { + "flowRate": 7.56, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "origin": "top" + }, + "wellName": "A5" + }, + "result": { + "position": { + "x": 315.38, + "y": 42.78, + "z": 31.400000000000002 + } + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "C11" + }, + "result": { + "position": { + "x": 236.88, + "y": 56.24, + "z": 1.92 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A5" + }, + "result": { + "position": { + "x": 315.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "blowout", + "notes": [], + "params": { + "flowRate": 7.56, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "origin": "top" + }, + "wellName": "A5" + }, + "result": { + "position": { + "x": 315.38, + "y": 42.78, + "z": 31.400000000000002 + } + }, + "status": "succeeded" + }, + { + "commandType": "dropTip", + "notes": [], + "params": { + "alternateDropLocation": false, + "wellLocation": { + "offset": { + "x": 0, + "y": 0, + "z": 0 + }, + "origin": "default" + }, + "wellName": "B1" + }, + "result": { + "position": { + "x": 14.38, + "y": 155.74, + "z": 45.09 + } + }, + "status": "succeeded" + }, + { + "commandType": "pickUpTip", + "notes": [], + "params": { + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "origin": "top" + }, + "wellName": "C1" + }, + "result": { + "position": { + "x": 14.38, + "y": 146.74, + "z": 64.69 + }, + "tipDiameter": 3.27, + "tipLength": 30.950000000000003, + "tipVolume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 5.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "A11" + }, + "result": { + "position": { + "x": 236.88, + "y": 74.24, + "z": 1.92 + }, + "volume": 5.0 + }, + "status": "succeeded" + }, + { + "commandType": "moveToWell", + "notes": [], + "params": { + "forceDirect": false, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": 5.0000000000000036 + }, + "origin": "top" + }, + "wellName": "A11" + }, + "result": { + "position": { + "x": 236.88, + "y": 74.24, + "z": 20.700000000000003 + } + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 10.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": 5.0000000000000036 + }, + "origin": "top" + }, + "wellName": "A11" + }, + "result": { + "position": { + "x": 236.88, + "y": 74.24, + "z": 20.700000000000003 + }, + "volume": 10.0 + }, + "status": "succeeded" + }, + { + "commandType": "waitForDuration", + "notes": [], + "params": { + "seconds": 3.0 + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 5.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "H11" + }, + "result": { + "position": { + "x": 236.88, + "y": 11.24, + "z": 1.92 + }, + "volume": 5.0 + }, + "status": "succeeded" + }, + { + "commandType": "moveToWell", + "notes": [], + "params": { + "forceDirect": false, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "origin": "top" + }, + "wellName": "E12" + }, + "result": { + "position": { + "x": 245.88, + "y": 38.24, + "z": 15.7 + } + }, + "status": "succeeded" + }, + { + "commandType": "moveToWell", + "notes": [], + "params": { + "forceDirect": false, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -14.78 + }, + "origin": "top" + }, + "wellName": "E11" + }, + "result": { + "position": { + "x": 236.88, + "y": 38.24, + "z": 0.92 + } + }, + "status": "succeeded" + }, + { + "commandType": "blowout", + "notes": [], + "params": { + "flowRate": 7.56, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -14.78 + }, + "origin": "top" + }, + "wellName": "E11" + }, + "result": { + "position": { + "x": 236.88, + "y": 38.24, + "z": 0.92 + } + }, + "status": "succeeded" + }, + { + "commandType": "touchTip", + "notes": [], + "params": { + "radius": 1.0, + "speed": 60.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -1.0 + }, + "origin": "top" + }, + "wellName": "H1" + }, + "result": { + "position": { + "x": 146.88, + "y": 11.24, + "z": 14.7 + } + }, + "status": "succeeded" + }, + { + "commandType": "waitForResume", + "notes": [], + "params": { + "message": "Is the pipette tip in the middle of the well?" + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "dropTip", + "notes": [], + "params": { + "alternateDropLocation": false, + "wellLocation": { + "offset": { + "x": 0, + "y": 0, + "z": 0 + }, + "origin": "default" + }, + "wellName": "C1" + }, + "result": { + "position": { + "x": 14.38, + "y": 146.74, + "z": 45.09 + } + }, + "status": "succeeded" + }, + { + "commandType": "temperatureModule/waitForTemperature", + "notes": [], + "params": { + "celsius": 25.0 + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "heaterShaker/setAndWaitForShakeSpeed", + "notes": [], + "params": { + "rpm": 466.0 + }, + "result": { + "pipetteRetracted": true + }, + "status": "succeeded" + }, + { + "commandType": "waitForDuration", + "notes": [], + "params": { + "seconds": 5.0 + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "heaterShaker/setTargetTemperature", + "notes": [], + "params": { + "celsius": 38.0 + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "heaterShaker/waitForTemperature", + "notes": [], + "params": {}, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "thermocycler/openLid", + "notes": [], + "params": {}, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "thermocycler/closeLid", + "notes": [], + "params": {}, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "thermocycler/setTargetLidTemperature", + "notes": [], + "params": { + "celsius": 38.0 + }, + "result": { + "targetLidTemperature": 38.0 + }, + "status": "succeeded" + }, + { + "commandType": "thermocycler/waitForLidTemperature", + "notes": [], + "params": {}, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "thermocycler/setTargetBlockTemperature", + "notes": [], + "params": { + "celsius": 28.0, + "holdTimeSeconds": 5.0 + }, + "result": { + "targetBlockTemperature": 28.0 + }, + "status": "succeeded" + }, + { + "commandType": "thermocycler/waitForBlockTemperature", + "notes": [], + "params": {}, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "thermocycler/deactivateBlock", + "notes": [], + "params": {}, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "thermocycler/deactivateLid", + "notes": [], + "params": {}, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "thermocycler/openLid", + "notes": [], + "params": {}, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "heaterShaker/deactivateShaker", + "notes": [], + "params": {}, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "pickUpTip", + "notes": [], + "params": { + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "origin": "top" + }, + "wellName": "D1" + }, + "result": { + "position": { + "x": 14.38, + "y": 137.74, + "z": 64.69 + }, + "tipDiameter": 3.27, + "tipLength": 30.950000000000003, + "tipVolume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 15.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 15.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 15.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.780000000000001 + }, + "origin": "top" + }, + "wellName": "A1" + }, + "result": { + "position": { + "x": 280.53, + "y": 255.08999999999997, + "z": 87.51 + }, + "volume": 15.0 + }, + "status": "succeeded" + }, + { + "commandType": "moveToAddressableAreaForDropTip", + "notes": [], + "params": { + "addressableAreaName": "fixedTrash", + "alternateDropLocation": true, + "forceDirect": false, + "ignoreTipConfiguration": true, + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + } + }, + "result": { + "position": { + "x": 331.785, + "y": 351.5, + "z": 82.0 + } + }, + "status": "succeeded" + }, + { + "commandType": "dropTipInPlace", + "notes": [], + "params": {}, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "pickUpTip", + "notes": [], + "params": { + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "origin": "top" + }, + "wellName": "A1" + }, + "result": { + "position": { + "x": 146.88, + "y": 164.74, + "z": 64.69 + }, + "tipDiameter": 5.23, + "tipLength": 51.099999999999994, + "tipVolume": 300.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 94.0, + "volume": 50.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 50.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 94.0, + "volume": 50.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.780000000000001 + }, + "origin": "top" + }, + "wellName": "A1" + }, + "result": { + "position": { + "x": 14.255, + "y": 75.365, + "z": 73.84500000000001 + }, + "volume": 50.0 + }, + "status": "succeeded" + }, + { + "commandType": "heaterShaker/setAndWaitForShakeSpeed", + "notes": [], + "params": { + "rpm": 350.0 + }, + "result": { + "pipetteRetracted": true + }, + "status": "succeeded" + }, + { + "commandType": "waitForDuration", + "notes": [], + "params": { + "seconds": 5.0 + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "heaterShaker/deactivateShaker", + "notes": [], + "params": {}, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "pickUpTip", + "notes": [], + "params": { + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "origin": "top" + }, + "wellName": "E1" + }, + "result": { + "position": { + "x": 14.38, + "y": 128.74, + "z": 64.69 + }, + "tipDiameter": 3.27, + "tipLength": 30.950000000000003, + "tipVolume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 15.12, + "volume": 10.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 10.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 11.34, + "volume": 10.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -22.0 + }, + "origin": "top" + }, + "wellName": "B2" + }, + "result": { + "position": { + "x": 315.0, + "y": 125.5, + "z": 18.0 + }, + "volume": 10.0 + }, + "status": "succeeded" + }, + { + "commandType": "moveToAddressableAreaForDropTip", + "notes": [], + "params": { + "addressableAreaName": "fixedTrash", + "alternateDropLocation": true, + "forceDirect": false, + "ignoreTipConfiguration": true, + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + } + }, + "result": { + "position": { + "x": 363.89500000000004, + "y": 351.5, + "z": 82.0 + } + }, + "status": "succeeded" + }, + { + "commandType": "dropTipInPlace", + "notes": [], + "params": {}, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 94.0, + "volume": 75.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 75.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 94.0, + "volume": 60.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.780000000000001 + }, + "origin": "top" + }, + "wellName": "A6" + }, + "result": { + "position": { + "x": 59.38, + "y": 324.04, + "z": 100.08 + }, + "volume": 60.0 + }, + "status": "succeeded" + }, + { + "commandType": "moveToAddressableAreaForDropTip", + "notes": [], + "params": { + "addressableAreaName": "fixedTrash", + "alternateDropLocation": true, + "forceDirect": false, + "ignoreTipConfiguration": true, + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + } + }, + "result": { + "position": { + "x": 331.785, + "y": 351.5, + "z": 82.0 + } + }, + "status": "succeeded" + }, + { + "commandType": "dropTipInPlace", + "notes": [], + "params": {}, + "result": {}, + "status": "succeeded" } ], "config": { "apiVersion": [ 2, - 16 + 17 ], "protocolType": "python" }, @@ -42,16 +15573,122 @@ "role": "labware" } ], - "labware": [], - "liquids": [], + "labware": [ + { + "definitionUri": "opentrons/opentrons_96_tiprack_300ul/1", + "displayName": "300ul tips", + "loadName": "opentrons_96_tiprack_300ul", + "location": { + "slotName": "5" + } + }, + { + "definitionUri": "opentrons/opentrons_96_tiprack_20ul/1", + "displayName": "20ul tips", + "loadName": "opentrons_96_tiprack_20ul", + "location": { + "slotName": "4" + } + }, + { + "definitionUri": "opentrons/opentrons_96_well_aluminum_block/1", + "loadName": "opentrons_96_well_aluminum_block", + "location": {} + }, + { + "definitionUri": "opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2", + "displayName": "Temperature-Controlled plate", + "loadName": "nest_96_wellplate_100ul_pcr_full_skirt", + "location": {} + }, + { + "definitionUri": "opentrons/opentrons_96_pcr_adapter/1", + "loadName": "opentrons_96_pcr_adapter", + "location": {} + }, + { + "definitionUri": "opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2", + "loadName": "nest_96_wellplate_100ul_pcr_full_skirt", + "location": {} + }, + { + "definitionUri": "opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2", + "loadName": "nest_96_wellplate_100ul_pcr_full_skirt", + "location": {} + }, + { + "definitionUri": "custom_beta/cpx_4_tuberack_100ul/1", + "displayName": "4 custom tubes", + "loadName": "cpx_4_tuberack_100ul", + "location": { + "slotName": "6" + } + }, + { + "definitionUri": "opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2", + "displayName": "logo destination", + "loadName": "nest_96_wellplate_100ul_pcr_full_skirt", + "location": { + "slotName": "2" + } + }, + { + "definitionUri": "opentrons/nest_12_reservoir_15ml/1", + "displayName": "dye container", + "loadName": "nest_12_reservoir_15ml", + "location": { + "slotName": "3" + } + } + ], + "liquids": [ + { + "description": "H₂O", + "displayColor": "#42AB2D", + "displayName": "water" + }, + { + "description": "C₃H₆O", + "displayColor": "#38588a", + "displayName": "acetone" + } + ], "metadata": { "author": "Opentrons Engineering ", - "description": "Placeholder - 2.17 Smoke Test is the same a 2.16 Smoke Test.", - "protocolName": "🛠️ 2.17 Smoke Test", + "description": "Description of the protocol that is longish \n has \n returns and \n emoji 😊 ⬆️ ", + "protocolName": "🛠️ 2.17 Smoke Test V3 🪄", "source": "Software Testing Team" }, - "modules": [], - "pipettes": [], + "modules": [ + { + "location": { + "slotName": "1" + }, + "model": "heaterShakerModuleV1" + }, + { + "location": { + "slotName": "9" + }, + "model": "temperatureModuleV2" + }, + { + "location": { + "slotName": "7" + }, + "model": "thermocyclerModuleV2" + } + ], + "pipettes": [ + { + "mount": "left", + "pipetteName": "p300_multi_gen2" + }, + { + "mount": "right", + "pipetteName": "p20_single_gen2" + } + ], "robotType": "OT-2 Standard", "runTimeParameters": [] } From 68e250c15d2bf76f2da5015ec7e1a15610f1e812 Mon Sep 17 00:00:00 2001 From: koji Date: Mon, 8 Apr 2024 12:15:10 -0400 Subject: [PATCH 064/194] refactor(app): update storybook of medium button (#14760) * refactor(app): update storybook of medium button --- .../atoms/buttons/MediumButton.stories.tsx | 95 ++++++++++--------- 1 file changed, 48 insertions(+), 47 deletions(-) diff --git a/app/src/atoms/buttons/MediumButton.stories.tsx b/app/src/atoms/buttons/MediumButton.stories.tsx index 667947b7e08..6c7fbd2fe5b 100644 --- a/app/src/atoms/buttons/MediumButton.stories.tsx +++ b/app/src/atoms/buttons/MediumButton.stories.tsx @@ -1,73 +1,74 @@ -import * as React from 'react' import { ICON_DATA_BY_NAME, VIEWPORT } from '@opentrons/components' import { MediumButton } from './' -import type { Story, Meta } from '@storybook/react' +import type { Meta, StoryObj } from '@storybook/react' -export default { +const meta: Meta = { title: 'ODD/Atoms/Buttons/MediumButton', + component: MediumButton, argTypes: { iconName: { control: { type: 'select', - options: Object.keys(ICON_DATA_BY_NAME), }, - defaultValue: undefined, + options: Object.keys(ICON_DATA_BY_NAME), }, buttonCategory: { control: { type: 'select', - options: ['default', 'rounded'], }, - defaultValue: undefined, + options: ['default', 'rounded'], }, onClick: { action: 'clicked' }, - width: { - control: { - type: 'text', - }, - defaultValue: undefined, - }, }, parameters: VIEWPORT.touchScreenViewport, -} as Meta +} -const MediumButtonTemplate: Story< - React.ComponentProps -> = args => +export default meta +type Story = StoryObj -export const PrimaryMediumButton = MediumButtonTemplate.bind({}) -PrimaryMediumButton.args = { - buttonText: 'Button text', - buttonType: 'primary', - disabled: false, +export const PrimaryMediumButton: Story = { + args: { + buttonText: 'Button text', + buttonType: 'primary', + disabled: false, + }, } -export const SecondaryMediumButton = MediumButtonTemplate.bind({}) -SecondaryMediumButton.args = { - buttonText: 'Button text', - buttonType: 'secondary', - disabled: false, + +export const SecondaryMediumButton: Story = { + args: { + buttonText: 'Button text', + buttonType: 'secondary', + disabled: false, + }, } -export const AlertMediumButton = MediumButtonTemplate.bind({}) -AlertMediumButton.args = { - buttonText: 'Button text', - buttonType: 'alert', - disabled: false, + +export const AlertMediumButton: Story = { + args: { + buttonText: 'Button text', + buttonType: 'alert', + disabled: false, + }, } -export const AlertSecondaryMediumButton = MediumButtonTemplate.bind({}) -AlertSecondaryMediumButton.args = { - buttonText: 'Button text', - buttonType: 'alertSecondary', - disabled: false, +export const AlertSecondaryMediumButton: Story = { + args: { + buttonText: 'Button text', + buttonType: 'alertSecondary', + disabled: false, + }, } -export const TertiaryHighMediumButton = MediumButtonTemplate.bind({}) -TertiaryHighMediumButton.args = { - buttonText: 'Button text', - buttonType: 'tertiaryHigh', - disabled: false, + +export const TertiaryHighMediumButton: Story = { + args: { + buttonText: 'Button text', + buttonType: 'tertiaryHigh', + disabled: false, + }, } -export const TertiaryLowLightMediumButton = MediumButtonTemplate.bind({}) -TertiaryLowLightMediumButton.args = { - buttonText: 'Button text', - buttonType: 'tertiaryLowLight', - disabled: false, + +export const TertiaryLowLightMediumButton: Story = { + args: { + buttonText: 'Button text', + buttonType: 'tertiaryLowLight', + disabled: false, + }, } From 22959a6e40c165db4156b46b05a19c611ef3abc3 Mon Sep 17 00:00:00 2001 From: Andy Sigler Date: Mon, 8 Apr 2024 12:29:40 -0400 Subject: [PATCH 065/194] chore(shared-data): Adds new functions for v36 96ch (#14792) Functions for 50, 200, and 1000 ul tips for the `v3.6` 96ch pipettes --- .../ninety_six_channel/p1000/default/3_6.json | 160 +++++++++--------- 1 file changed, 80 insertions(+), 80 deletions(-) diff --git a/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/3_6.json b/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/3_6.json index 8ca9dc4ece4..cac57c41844 100644 --- a/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/3_6.json +++ b/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/3_6.json @@ -20,36 +20,36 @@ "aspirate": { "default": { "1": [ - [1.933333, 2.844459, 4.750159], - [2.833333, 1.12901, 8.066694], - [3.603333, 0.254744, 10.543779], - [4.836667, 1.101839, 7.491414], - [5.755, 0.277649, 11.47775], - [6.643333, 0.14813, 12.223126], - [7.548333, 0.145635, 12.239705], - [8.475, 0.15097, 12.199433], - [13.02, 0.071736, 12.870946], - [22.318333, 0.042305, 13.254131], - [36.463333, 0.021195, 13.725284], - [54.82, 0.001805, 14.43229] + [1.9733, 2.7039, 5.1258], + [2.88, 1.0915, 8.3077], + [3.7642, 0.5906, 9.7502], + [4.9783, 1.0072, 8.1822], + [5.9342, 0.2998, 11.7038], + [6.8708, 0.1887, 12.3626], + [7.8092, 0.1497, 12.631], + [8.7525, 0.1275, 12.804], + [13.4575, 0.0741, 13.2718], + [22.8675, 0.0296, 13.87], + [37.0442, 0.0128, 14.2551], + [55.4792, -0.0013, 14.7754] ] } }, "dispense": { "default": { "1": [ - [1.933333, 2.844459, 4.750159], - [2.833333, 1.12901, 8.066694], - [3.603333, 0.254744, 10.543779], - [4.836667, 1.101839, 7.491414], - [5.755, 0.277649, 11.47775], - [6.643333, 0.14813, 12.223126], - [7.548333, 0.145635, 12.239705], - [8.475, 0.15097, 12.199433], - [13.02, 0.071736, 12.870946], - [22.318333, 0.042305, 13.254131], - [36.463333, 0.021195, 13.725284], - [54.82, 0.001805, 14.43229] + [1.9733, 2.7039, 5.1258], + [2.88, 1.0915, 8.3077], + [3.7642, 0.5906, 9.7502], + [4.9783, 1.0072, 8.1822], + [5.9342, 0.2998, 11.7038], + [6.8708, 0.1887, 12.3626], + [7.8092, 0.1497, 12.631], + [8.7525, 0.1275, 12.804], + [13.4575, 0.0741, 13.2718], + [22.8675, 0.0296, 13.87], + [37.0442, 0.0128, 14.2551], + [55.4792, -0.0013, 14.7754] ] } }, @@ -74,34 +74,34 @@ "aspirate": { "default": { "1": [ - [1.39875, 4.681865, 0.866627], - [2.5225, 2.326382, 4.161359], - [3.625, 1.361424, 6.595466], - [4.69125, 0.848354, 8.455342], - [5.705, 0.519685, 9.997214], - [6.70625, 0.36981, 10.852249], - [7.69375, 0.267029, 11.541523], - [8.67875, 0.210129, 11.979299], - [47.05, 0.030309, 13.539909], - [95.24375, 0.003774, 14.78837], - [211.0225, 0.000928, 15.059476] + [1.9331, 3.4604, 3.5588], + [2.9808, 1.5307, 7.2892], + [3.9869, 0.825, 9.3926], + [4.9762, 0.5141, 10.6323], + [5.9431, 0.3232, 11.5819], + [6.9223, 0.2644, 11.9317], + [7.8877, 0.1832, 12.4935], + [8.8562, 0.1512, 12.7463], + [47.7169, 0.0281, 13.836], + [95.63, 0.0007, 15.147], + [211.1169, 0.0005, 15.1655] ] } }, "dispense": { "default": { "1": [ - [1.39875, 4.681865, 0.866627], - [2.5225, 2.326382, 4.161359], - [3.625, 1.361424, 6.595466], - [4.69125, 0.848354, 8.455342], - [5.705, 0.519685, 9.997214], - [6.70625, 0.36981, 10.852249], - [7.69375, 0.267029, 11.541523], - [8.67875, 0.210129, 11.979299], - [47.05, 0.030309, 13.539909], - [95.24375, 0.003774, 14.78837], - [211.0225, 0.000928, 15.059476] + [1.9331, 3.4604, 3.5588], + [2.9808, 1.5307, 7.2892], + [3.9869, 0.825, 9.3926], + [4.9762, 0.5141, 10.6323], + [5.9431, 0.3232, 11.5819], + [6.9223, 0.2644, 11.9317], + [7.8877, 0.1832, 12.4935], + [8.8562, 0.1512, 12.7463], + [47.7169, 0.0281, 13.836], + [95.63, 0.0007, 15.147], + [211.1169, 0.0005, 15.1655] ] } }, @@ -126,46 +126,46 @@ "aspirate": { "default": { "1": [ - [3.76, 2.041301, 4.284751], - [5.684286, 0.49624, 10.09418], - [8.445714, 0.187358, 11.849952], - [12.981429, 0.073135, 12.814653], - [17.667143, 0.060853, 12.974083], - [46.515714, 0.025888, 13.591828], - [95.032857, 0.006561, 14.490827], - [114.488571, 0.00306, 14.823556], - [192.228571, 0.001447, 15.00822], - [309.921429, 0.000995, 15.095087], - [436.984286, 0.000322, 15.303634], - [632.861429, 0.000208, 15.353582], - [828.952857, 0.00013, 15.402544], - [976.118571, 0.000095, 15.431673], - [1005.275714, -0.000067, 15.589843], - [1024.768571, -0.000021, 15.543681], - [1049.145714, -0.000013, 15.535884] + [3.9, 1.789, 5.4283], + [5.6991, 0.3019, 11.2278], + [8.5155, 0.2111, 11.7453], + [13.1482, 0.0858, 12.8124], + [17.8909, 0.0604, 13.1472], + [46.0982, 0.0155, 13.9505], + [93.5618, 0.0046, 14.4523], + [112.5991, 0.0023, 14.6687], + [189.5555, 0.002, 14.7035], + [305.5891, 0.001, 14.887], + [431.2836, 0.0004, 15.055], + [625.0209, 0.0003, 15.1309], + [818.6909, 0.0001, 15.2112], + [963.9909, 0.0001, 15.2445], + [992.0791, -0.0005, 15.7723], + [1012.2118, 0.0007, 14.6701], + [1037.1873, 0.0005, 14.8072] ] } }, "dispense": { "default": { "1": [ - [3.76, 2.041301, 4.284751], - [5.684286, 0.49624, 10.09418], - [8.445714, 0.187358, 11.849952], - [12.981429, 0.073135, 12.814653], - [17.667143, 0.060853, 12.974083], - [46.515714, 0.025888, 13.591828], - [95.032857, 0.006561, 14.490827], - [114.488571, 0.00306, 14.823556], - [192.228571, 0.001447, 15.00822], - [309.921429, 0.000995, 15.095087], - [436.984286, 0.000322, 15.303634], - [632.861429, 0.000208, 15.353582], - [828.952857, 0.00013, 15.402544], - [976.118571, 0.000095, 15.431673], - [1005.275714, -0.000067, 15.589843], - [1024.768571, -0.000021, 15.543681], - [1049.145714, -0.000013, 15.535884] + [3.9, 1.789, 5.4283], + [5.6991, 0.3019, 11.2278], + [8.5155, 0.2111, 11.7453], + [13.1482, 0.0858, 12.8124], + [17.8909, 0.0604, 13.1472], + [46.0982, 0.0155, 13.9505], + [93.5618, 0.0046, 14.4523], + [112.5991, 0.0023, 14.6687], + [189.5555, 0.002, 14.7035], + [305.5891, 0.001, 14.887], + [431.2836, 0.0004, 15.055], + [625.0209, 0.0003, 15.1309], + [818.6909, 0.0001, 15.2112], + [963.9909, 0.0001, 15.2445], + [992.0791, -0.0005, 15.7723], + [1012.2118, 0.0007, 14.6701], + [1037.1873, 0.0005, 14.8072] ] } }, From 3382ae373feac73b69e314406a0f93228d1814fc Mon Sep 17 00:00:00 2001 From: Derek Maggio Date: Mon, 8 Apr 2024 09:31:23 -0700 Subject: [PATCH 066/194] chore: create performance metrics project (#14806) # Overview Basic setup of Performance Metrics project Closes https://opentrons.atlassian.net/browse/EXEC-380 # Test Plan - [x] Verify all makefile commands can run successfully (GH Actions will be in another PR) - [x] Build wheel, install, and verify it runs --- performance-metrics/.flake8 | 25 ++ performance-metrics/.gitignore | 1 + performance-metrics/Makefile | 28 ++ performance-metrics/Pipfile | 19 + performance-metrics/Pipfile.lock | 367 ++++++++++++++++++ performance-metrics/README.md | 3 + performance-metrics/mypy.ini | 5 + performance-metrics/pytest.ini | 3 + performance-metrics/setup.py | 91 +++++ .../src/performance_metrics/__init__.py | 1 + .../src/performance_metrics/py.typed | 0 11 files changed, 543 insertions(+) create mode 100644 performance-metrics/.flake8 create mode 100644 performance-metrics/.gitignore create mode 100644 performance-metrics/Makefile create mode 100644 performance-metrics/Pipfile create mode 100644 performance-metrics/Pipfile.lock create mode 100644 performance-metrics/README.md create mode 100644 performance-metrics/mypy.ini create mode 100644 performance-metrics/pytest.ini create mode 100755 performance-metrics/setup.py create mode 100644 performance-metrics/src/performance_metrics/__init__.py create mode 100644 performance-metrics/src/performance_metrics/py.typed diff --git a/performance-metrics/.flake8 b/performance-metrics/.flake8 new file mode 100644 index 00000000000..4aa1c02d7aa --- /dev/null +++ b/performance-metrics/.flake8 @@ -0,0 +1,25 @@ +[flake8] + +# max cyclomatic complexity +max-complexity = 9 + +extend-ignore = + # defer formatting concerns to black + # E203: space around `:` operator + # E501: maximum line length + E203, + E501, + # do not require type annotations for self nor cls + ANN101, + ANN102 + # do not require docstring for __init__, put them on the class + D107, + +# configure flake8-docstrings +# https://pypi.org/project/flake8-docstrings/ +docstring-convention = google + +noqa-require-code = true + +per-file-ignores = + setup.py:ANN,D \ No newline at end of file diff --git a/performance-metrics/.gitignore b/performance-metrics/.gitignore new file mode 100644 index 00000000000..8fb3d9a4ea5 --- /dev/null +++ b/performance-metrics/.gitignore @@ -0,0 +1 @@ +.ruff_cache/ \ No newline at end of file diff --git a/performance-metrics/Makefile b/performance-metrics/Makefile new file mode 100644 index 00000000000..cce4fd7d93a --- /dev/null +++ b/performance-metrics/Makefile @@ -0,0 +1,28 @@ +include ../scripts/python.mk + +.PHONY: lint +lint: + $(python) -m black --check . + $(python) -m flake8 . + $(python) -m mypy . + +.PHONY: format +format: + $(python) -m black . + +.PHONY: setup +setup: + $(pipenv) sync --dev + +.PHONY: teardown +teardown: + $(pipenv) --rm + +.PHONY: clean +clean: + rm -rf build dist *.egg-info .mypy_cache .pytest_cache src/performance_metrics.egg-info + +.PHONY: wheel +wheel: + $(python) setup.py $(wheel_opts) bdist_wheel + rm -rf build \ No newline at end of file diff --git a/performance-metrics/Pipfile b/performance-metrics/Pipfile new file mode 100644 index 00000000000..df5a3de89d6 --- /dev/null +++ b/performance-metrics/Pipfile @@ -0,0 +1,19 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +opentrons-shared-data = {file = "../shared-data/python", editable = true} + +[dev-packages] +pytest = "==7.2.2" +mypy = "==1.8.0" +flake8 = "==7.0.0" +flake8-annotations = "~=3.0.1" +flake8-docstrings = "~=1.7.0" +flake8-noqa = "~=1.4.0" +black = "==22.3.0" + +[requires] +python_version = "3.10" diff --git a/performance-metrics/Pipfile.lock b/performance-metrics/Pipfile.lock new file mode 100644 index 00000000000..61556f3dee9 --- /dev/null +++ b/performance-metrics/Pipfile.lock @@ -0,0 +1,367 @@ +{ + "_meta": { + "hash": { + "sha256": "fa95804888e2d45ce401c98bafc9b543cb6e1afe0a36713660d3f5517ac02b8e" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.10" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "attrs": { + "hashes": [ + "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30", + "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1" + ], + "markers": "python_version >= '3.7'", + "version": "==23.2.0" + }, + "jsonschema": { + "hashes": [ + "sha256:0f864437ab8b6076ba6707453ef8f98a6a0d512a80e93f8abdb676f737ecb60d", + "sha256:a870ad254da1a8ca84b6a2905cac29d265f805acc57af304784962a2aa6508f6" + ], + "markers": "python_version >= '3.7'", + "version": "==4.17.3" + }, + "opentrons-shared-data": { + "editable": true, + "file": "../shared-data/python", + "markers": "python_version >= '3.8'" + }, + "pydantic": { + "hashes": [ + "sha256:005655cabc29081de8243126e036f2065bd7ea5b9dff95fde6d2c642d39755de", + "sha256:0d142fa1b8f2f0ae11ddd5e3e317dcac060b951d605fda26ca9b234b92214986", + "sha256:22ed12ee588b1df028a2aa5d66f07bf8f8b4c8579c2e96d5a9c1f96b77f3bb55", + "sha256:2746189100c646682eff0bce95efa7d2e203420d8e1c613dc0c6b4c1d9c1fde4", + "sha256:28e552a060ba2740d0d2aabe35162652c1459a0b9069fe0db7f4ee0e18e74d58", + "sha256:3287e1614393119c67bd4404f46e33ae3be3ed4cd10360b48d0a4459f420c6a3", + "sha256:3350f527bb04138f8aff932dc828f154847fbdc7a1a44c240fbfff1b57f49a12", + "sha256:3453685ccd7140715e05f2193d64030101eaad26076fad4e246c1cc97e1bb30d", + "sha256:394f08750bd8eaad714718812e7fab615f873b3cdd0b9d84e76e51ef3b50b6b7", + "sha256:4e316e54b5775d1eb59187f9290aeb38acf620e10f7fd2f776d97bb788199e53", + "sha256:50f1666a9940d3d68683c9d96e39640f709d7a72ff8702987dab1761036206bb", + "sha256:51d405b42f1b86703555797270e4970a9f9bd7953f3990142e69d1037f9d9e51", + "sha256:584f2d4c98ffec420e02305cf675857bae03c9d617fcfdc34946b1160213a948", + "sha256:5e09c19df304b8123938dc3c53d3d3be6ec74b9d7d0d80f4f4b5432ae16c2022", + "sha256:676ed48f2c5bbad835f1a8ed8a6d44c1cd5a21121116d2ac40bd1cd3619746ed", + "sha256:67f1a1fb467d3f49e1708a3f632b11c69fccb4e748a325d5a491ddc7b5d22383", + "sha256:6a51a1dd4aa7b3f1317f65493a182d3cff708385327c1c82c81e4a9d6d65b2e4", + "sha256:6bd7030c9abc80134087d8b6e7aa957e43d35714daa116aced57269a445b8f7b", + "sha256:75279d3cac98186b6ebc2597b06bcbc7244744f6b0b44a23e4ef01e5683cc0d2", + "sha256:7ac9237cd62947db00a0d16acf2f3e00d1ae9d3bd602b9c415f93e7a9fc10528", + "sha256:7ea210336b891f5ea334f8fc9f8f862b87acd5d4a0cbc9e3e208e7aa1775dabf", + "sha256:82790d4753ee5d00739d6cb5cf56bceb186d9d6ce134aca3ba7befb1eedbc2c8", + "sha256:92229f73400b80c13afcd050687f4d7e88de9234d74b27e6728aa689abcf58cc", + "sha256:9bea1f03b8d4e8e86702c918ccfd5d947ac268f0f0cc6ed71782e4b09353b26f", + "sha256:a980a77c52723b0dc56640ced396b73a024d4b74f02bcb2d21dbbac1debbe9d0", + "sha256:af9850d98fc21e5bc24ea9e35dd80a29faf6462c608728a110c0a30b595e58b7", + "sha256:bbc6989fad0c030bd70a0b6f626f98a862224bc2b1e36bfc531ea2facc0a340c", + "sha256:be51dd2c8596b25fe43c0a4a59c2bee4f18d88efb8031188f9e7ddc6b469cf44", + "sha256:c365ad9c394f9eeffcb30a82f4246c0006417f03a7c0f8315d6211f25f7cb654", + "sha256:c3d5731a120752248844676bf92f25a12f6e45425e63ce22e0849297a093b5b0", + "sha256:ca832e124eda231a60a041da4f013e3ff24949d94a01154b137fc2f2a43c3ffb", + "sha256:d207d5b87f6cbefbdb1198154292faee8017d7495a54ae58db06762004500d00", + "sha256:d31ee5b14a82c9afe2bd26aaa405293d4237d0591527d9129ce36e58f19f95c1", + "sha256:d3b5c4cbd0c9cb61bbbb19ce335e1f8ab87a811f6d589ed52b0254cf585d709c", + "sha256:d573082c6ef99336f2cb5b667b781d2f776d4af311574fb53d908517ba523c22", + "sha256:e49db944fad339b2ccb80128ffd3f8af076f9f287197a480bf1e4ca053a866f0" + ], + "markers": "python_version >= '3.7'", + "version": "==1.10.15" + }, + "pyrsistent": { + "hashes": [ + "sha256:0724c506cd8b63c69c7f883cc233aac948c1ea946ea95996ad8b1380c25e1d3f", + "sha256:09848306523a3aba463c4b49493a760e7a6ca52e4826aa100ee99d8d39b7ad1e", + "sha256:0f3b1bcaa1f0629c978b355a7c37acd58907390149b7311b5db1b37648eb6958", + "sha256:21cc459636983764e692b9eba7144cdd54fdec23ccdb1e8ba392a63666c60c34", + "sha256:2e14c95c16211d166f59c6611533d0dacce2e25de0f76e4c140fde250997b3ca", + "sha256:2e2c116cc804d9b09ce9814d17df5edf1df0c624aba3b43bc1ad90411487036d", + "sha256:4021a7f963d88ccd15b523787d18ed5e5269ce57aa4037146a2377ff607ae87d", + "sha256:4c48f78f62ab596c679086084d0dd13254ae4f3d6c72a83ffdf5ebdef8f265a4", + "sha256:4f5c2d012671b7391803263419e31b5c7c21e7c95c8760d7fc35602353dee714", + "sha256:58b8f6366e152092194ae68fefe18b9f0b4f89227dfd86a07770c3d86097aebf", + "sha256:59a89bccd615551391f3237e00006a26bcf98a4d18623a19909a2c48b8e986ee", + "sha256:5cdd7ef1ea7a491ae70d826b6cc64868de09a1d5ff9ef8d574250d0940e275b8", + "sha256:6288b3fa6622ad8a91e6eb759cfc48ff3089e7c17fb1d4c59a919769314af224", + "sha256:6d270ec9dd33cdb13f4d62c95c1a5a50e6b7cdd86302b494217137f760495b9d", + "sha256:79ed12ba79935adaac1664fd7e0e585a22caa539dfc9b7c7c6d5ebf91fb89054", + "sha256:7d29c23bdf6e5438c755b941cef867ec2a4a172ceb9f50553b6ed70d50dfd656", + "sha256:8441cf9616d642c475684d6cf2520dd24812e996ba9af15e606df5f6fd9d04a7", + "sha256:881bbea27bbd32d37eb24dd320a5e745a2a5b092a17f6debc1349252fac85423", + "sha256:8c3aba3e01235221e5b229a6c05f585f344734bd1ad42a8ac51493d74722bbce", + "sha256:a14798c3005ec892bbada26485c2eea3b54109cb2533713e355c806891f63c5e", + "sha256:b14decb628fac50db5e02ee5a35a9c0772d20277824cfe845c8a8b717c15daa3", + "sha256:b318ca24db0f0518630e8b6f3831e9cba78f099ed5c1d65ffe3e023003043ba0", + "sha256:c1beb78af5423b879edaf23c5591ff292cf7c33979734c99aa66d5914ead880f", + "sha256:c55acc4733aad6560a7f5f818466631f07efc001fd023f34a6c203f8b6df0f0b", + "sha256:ca52d1ceae015859d16aded12584c59eb3825f7b50c6cfd621d4231a6cc624ce", + "sha256:cae40a9e3ce178415040a0383f00e8d68b569e97f31928a3a8ad37e3fde6df6a", + "sha256:e78d0c7c1e99a4a45c99143900ea0546025e41bb59ebc10182e947cf1ece9174", + "sha256:ef3992833fbd686ee783590639f4b8343a57f1f75de8633749d984dc0eb16c86", + "sha256:f058a615031eea4ef94ead6456f5ec2026c19fb5bd6bfe86e9665c4158cf802f", + "sha256:f5ac696f02b3fc01a710427585c855f65cd9c640e14f52abe52020722bb4906b", + "sha256:f920385a11207dc372a028b3f1e1038bb244b3ec38d448e6d8e43c6b3ba20e98", + "sha256:fed2c3216a605dc9a6ea50c7e84c82906e3684c4e80d2908208f662a6cbf9022" + ], + "markers": "python_version >= '3.8'", + "version": "==0.20.0" + }, + "typing-extensions": { + "hashes": [ + "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0", + "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a" + ], + "markers": "python_version >= '3.8'", + "version": "==4.11.0" + } + }, + "develop": { + "attrs": { + "hashes": [ + "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30", + "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1" + ], + "markers": "python_version >= '3.7'", + "version": "==23.2.0" + }, + "black": { + "hashes": [ + "sha256:06f9d8846f2340dfac80ceb20200ea5d1b3f181dd0556b47af4e8e0b24fa0a6b", + "sha256:10dbe6e6d2988049b4655b2b739f98785a884d4d6b85bc35133a8fb9a2233176", + "sha256:2497f9c2386572e28921fa8bec7be3e51de6801f7459dffd6e62492531c47e09", + "sha256:30d78ba6bf080eeaf0b7b875d924b15cd46fec5fd044ddfbad38c8ea9171043a", + "sha256:328efc0cc70ccb23429d6be184a15ce613f676bdfc85e5fe8ea2a9354b4e9015", + "sha256:35020b8886c022ced9282b51b5a875b6d1ab0c387b31a065b84db7c33085ca79", + "sha256:5795a0375eb87bfe902e80e0c8cfaedf8af4d49694d69161e5bd3206c18618bb", + "sha256:5891ef8abc06576985de8fa88e95ab70641de6c1fca97e2a15820a9b69e51b20", + "sha256:637a4014c63fbf42a692d22b55d8ad6968a946b4a6ebc385c5505d9625b6a464", + "sha256:67c8301ec94e3bcc8906740fe071391bce40a862b7be0b86fb5382beefecd968", + "sha256:6d2fc92002d44746d3e7db7cf9313cf4452f43e9ea77a2c939defce3b10b5c82", + "sha256:6ee227b696ca60dd1c507be80a6bc849a5a6ab57ac7352aad1ffec9e8b805f21", + "sha256:863714200ada56cbc366dc9ae5291ceb936573155f8bf8e9de92aef51f3ad0f0", + "sha256:9b542ced1ec0ceeff5b37d69838106a6348e60db7b8fdd245294dc1d26136265", + "sha256:a6342964b43a99dbc72f72812bf88cad8f0217ae9acb47c0d4f141a6416d2d7b", + "sha256:ad4efa5fad66b903b4a5f96d91461d90b9507a812b3c5de657d544215bb7877a", + "sha256:bc58025940a896d7e5356952228b68f793cf5fcb342be703c3a2669a1488cb72", + "sha256:cc1e1de68c8e5444e8f94c3670bb48a2beef0e91dddfd4fcc29595ebd90bb9ce", + "sha256:cee3e11161dde1b2a33a904b850b0899e0424cc331b7295f2a9698e79f9a69a0", + "sha256:e3556168e2e5c49629f7b0f377070240bd5511e45e25a4497bb0073d9dda776a", + "sha256:e8477ec6bbfe0312c128e74644ac8a02ca06bcdb8982d4ee06f209be28cdf163", + "sha256:ee8f1f7228cce7dffc2b464f07ce769f478968bfb3dd1254a4c2eeed84928aad", + "sha256:fd57160949179ec517d32ac2ac898b5f20d68ed1a9c977346efbac9c2f1e779d" + ], + "index": "pypi", + "markers": "python_full_version >= '3.6.2'", + "version": "==22.3.0" + }, + "click": { + "hashes": [ + "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", + "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de" + ], + "markers": "python_version >= '3.7'", + "version": "==8.1.7" + }, + "exceptiongroup": { + "hashes": [ + "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14", + "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68" + ], + "markers": "python_version < '3.11'", + "version": "==1.2.0" + }, + "flake8": { + "hashes": [ + "sha256:33f96621059e65eec474169085dc92bf26e7b2d47366b70be2f67ab80dc25132", + "sha256:a6dfbb75e03252917f2473ea9653f7cd799c3064e54d4c8140044c5c065f53c3" + ], + "index": "pypi", + "markers": "python_full_version >= '3.8.1'", + "version": "==7.0.0" + }, + "flake8-annotations": { + "hashes": [ + "sha256:af78e3216ad800d7e144745ece6df706c81b3255290cbf870e54879d495e8ade", + "sha256:ff37375e71e3b83f2a5a04d443c41e2c407de557a884f3300a7fa32f3c41cb0a" + ], + "index": "pypi", + "markers": "python_full_version >= '3.8.1'", + "version": "==3.0.1" + }, + "flake8-docstrings": { + "hashes": [ + "sha256:4c8cc748dc16e6869728699e5d0d685da9a10b0ea718e090b1ba088e67a941af", + "sha256:51f2344026da083fc084166a9353f5082b01f72901df422f74b4d953ae88ac75" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==1.7.0" + }, + "flake8-noqa": { + "hashes": [ + "sha256:4465e16a19be433980f6f563d05540e2e54797eb11facb9feb50fed60624dc45", + "sha256:771765ab27d1efd157528379acd15131147f9ae578a72d17fb432ca197881243" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==1.4.0" + }, + "iniconfig": { + "hashes": [ + "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", + "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374" + ], + "markers": "python_version >= '3.7'", + "version": "==2.0.0" + }, + "mccabe": { + "hashes": [ + "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", + "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e" + ], + "markers": "python_version >= '3.6'", + "version": "==0.7.0" + }, + "mypy": { + "hashes": [ + "sha256:028cf9f2cae89e202d7b6593cd98db6759379f17a319b5faf4f9978d7084cdc6", + "sha256:2afecd6354bbfb6e0160f4e4ad9ba6e4e003b767dd80d85516e71f2e955ab50d", + "sha256:2b5b6c721bd4aabaadead3a5e6fa85c11c6c795e0c81a7215776ef8afc66de02", + "sha256:42419861b43e6962a649068a61f4a4839205a3ef525b858377a960b9e2de6e0d", + "sha256:42c6680d256ab35637ef88891c6bd02514ccb7e1122133ac96055ff458f93fc3", + "sha256:485a8942f671120f76afffff70f259e1cd0f0cfe08f81c05d8816d958d4577d3", + "sha256:4c886c6cce2d070bd7df4ec4a05a13ee20c0aa60cb587e8d1265b6c03cf91da3", + "sha256:4e6d97288757e1ddba10dd9549ac27982e3e74a49d8d0179fc14d4365c7add66", + "sha256:4ef4be7baf08a203170f29e89d79064463b7fc7a0908b9d0d5114e8009c3a259", + "sha256:51720c776d148bad2372ca21ca29256ed483aa9a4cdefefcef49006dff2a6835", + "sha256:52825b01f5c4c1c4eb0db253ec09c7aa17e1a7304d247c48b6f3599ef40db8bd", + "sha256:538fd81bb5e430cc1381a443971c0475582ff9f434c16cd46d2c66763ce85d9d", + "sha256:5c1538c38584029352878a0466f03a8ee7547d7bd9f641f57a0f3017a7c905b8", + "sha256:6ff8b244d7085a0b425b56d327b480c3b29cafbd2eff27316a004f9a7391ae07", + "sha256:7178def594014aa6c35a8ff411cf37d682f428b3b5617ca79029d8ae72f5402b", + "sha256:720a5ca70e136b675af3af63db533c1c8c9181314d207568bbe79051f122669e", + "sha256:7f1478736fcebb90f97e40aff11a5f253af890c845ee0c850fe80aa060a267c6", + "sha256:855fe27b80375e5c5878492f0729540db47b186509c98dae341254c8f45f42ae", + "sha256:8963b83d53ee733a6e4196954502b33567ad07dfd74851f32be18eb932fb1cb9", + "sha256:9261ed810972061388918c83c3f5cd46079d875026ba97380f3e3978a72f503d", + "sha256:99b00bc72855812a60d253420d8a2eae839b0afa4938f09f4d2aa9bb4654263a", + "sha256:ab3c84fa13c04aeeeabb2a7f67a25ef5d77ac9d6486ff33ded762ef353aa5592", + "sha256:afe3fe972c645b4632c563d3f3eff1cdca2fa058f730df2b93a35e3b0c538218", + "sha256:d19c413b3c07cbecf1f991e2221746b0d2a9410b59cb3f4fb9557f0365a1a817", + "sha256:df9824ac11deaf007443e7ed2a4a26bebff98d2bc43c6da21b2b64185da011c4", + "sha256:e46f44b54ebddbeedbd3d5b289a893219065ef805d95094d16a0af6630f5d410", + "sha256:f5ac9a4eeb1ec0f1ccdc6f326bcdb464de5f80eb07fb38b5ddd7b0de6bc61e55" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==1.8.0" + }, + "mypy-extensions": { + "hashes": [ + "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", + "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782" + ], + "markers": "python_version >= '3.5'", + "version": "==1.0.0" + }, + "packaging": { + "hashes": [ + "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5", + "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9" + ], + "markers": "python_version >= '3.7'", + "version": "==24.0" + }, + "pathspec": { + "hashes": [ + "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", + "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712" + ], + "markers": "python_version >= '3.8'", + "version": "==0.12.1" + }, + "platformdirs": { + "hashes": [ + "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068", + "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768" + ], + "markers": "python_version >= '3.8'", + "version": "==4.2.0" + }, + "pluggy": { + "hashes": [ + "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981", + "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be" + ], + "markers": "python_version >= '3.8'", + "version": "==1.4.0" + }, + "pycodestyle": { + "hashes": [ + "sha256:41ba0e7afc9752dfb53ced5489e89f8186be00e599e712660695b7a75ff2663f", + "sha256:44fe31000b2d866f2e41841b18528a505fbd7fef9017b04eff4e2648a0fadc67" + ], + "markers": "python_version >= '3.8'", + "version": "==2.11.1" + }, + "pydocstyle": { + "hashes": [ + "sha256:118762d452a49d6b05e194ef344a55822987a462831ade91ec5c06fd2169d019", + "sha256:7ce43f0c0ac87b07494eb9c0b462c0b73e6ff276807f204d6b53edc72b7e44e1" + ], + "markers": "python_version >= '3.6'", + "version": "==6.3.0" + }, + "pyflakes": { + "hashes": [ + "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f", + "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a" + ], + "markers": "python_version >= '3.8'", + "version": "==3.2.0" + }, + "pytest": { + "hashes": [ + "sha256:130328f552dcfac0b1cec75c12e3f005619dc5f874f0a06e8ff7263f0ee6225e", + "sha256:c99ab0c73aceb050f68929bc93af19ab6db0558791c6a0715723abe9d0ade9d4" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==7.2.2" + }, + "snowballstemmer": { + "hashes": [ + "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1", + "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a" + ], + "version": "==2.2.0" + }, + "tomli": { + "hashes": [ + "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", + "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" + ], + "markers": "python_version < '3.11'", + "version": "==2.0.1" + }, + "typing-extensions": { + "hashes": [ + "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0", + "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a" + ], + "markers": "python_version >= '3.8'", + "version": "==4.11.0" + } + } +} diff --git a/performance-metrics/README.md b/performance-metrics/README.md new file mode 100644 index 00000000000..7fb20445e36 --- /dev/null +++ b/performance-metrics/README.md @@ -0,0 +1,3 @@ +# Performance Metrics + +Project to gather various performance metrics for the Opentrons Flex. diff --git a/performance-metrics/mypy.ini b/performance-metrics/mypy.ini new file mode 100644 index 00000000000..b94476cbcaa --- /dev/null +++ b/performance-metrics/mypy.ini @@ -0,0 +1,5 @@ +[mypy] +show_error_codes = True +warn_unused_configs = True +strict = True +exclude = setup.py \ No newline at end of file diff --git a/performance-metrics/pytest.ini b/performance-metrics/pytest.ini new file mode 100644 index 00000000000..49f04412746 --- /dev/null +++ b/performance-metrics/pytest.ini @@ -0,0 +1,3 @@ +[pytest] +addopts = --color=yes --strict-markers +asyncio_mode = auto diff --git a/performance-metrics/setup.py b/performance-metrics/setup.py new file mode 100755 index 00000000000..eced9a55ab9 --- /dev/null +++ b/performance-metrics/setup.py @@ -0,0 +1,91 @@ +# Inspired by: +# https://hynek.me/articles/sharing-your-labor-of-love-pypi-quick-and-dirty/ +import sys +import codecs +import os +import os.path +from setuptools import setup, find_packages + +# make stdout blocking since Travis sets it to nonblocking +if os.name == "posix": + import fcntl + + flags = fcntl.fcntl(sys.stdout, fcntl.F_GETFL) + fcntl.fcntl(sys.stdout, fcntl.F_SETFL, flags & ~os.O_NONBLOCK) + +HERE = os.path.abspath(os.path.dirname(__file__)) +sys.path.append(os.path.join(HERE, "..", "scripts")) + +from python_build_utils import normalize_version # noqa: E402 + + +def get_version(): + buildno = os.getenv("BUILD_NUMBER") + project = os.getenv("OPENTRONS_PROJECT", "robot-stack") + git_dir = os.getenv("OPENTRONS_GIT_DIR", None) + if buildno: + normalize_opts = {"extra_tag": buildno} + else: + normalize_opts = {} + return normalize_version( + "performance-metrics", project, git_dir=git_dir, **normalize_opts + ) + + +VERSION = get_version() + +DISTNAME = "performance_metrics" +LICENSE = "Apache 2.0" +AUTHOR = "Opentrons" +EMAIL = "engineering@opentrons.com" +URL = "https://github.com/Opentrons/opentrons" +DOWNLOAD_URL = "" +CLASSIFIERS = [ + "Development Status :: 5 - Production/Stable", + "Environment :: Console", + "Operating System :: OS Independent", + "Intended Audience :: Science/Research", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Topic :: Scientific/Engineering", +] +KEYWORDS = ["robots", "protocols", "synbio", "pcr", "automation", "lab"] +DESCRIPTION = "Library for working with performance metrics on the Opentrons robots" +PACKAGES = find_packages(where="src", exclude=["tests.*", "tests"]) +INSTALL_REQUIRES = [ + f"opentrons-shared-data=={VERSION}", +] + + +def read(*parts): + """ + Build an absolute path from *parts* and and return the contents of the + resulting file. Assume UTF-8 encoding. + """ + with codecs.open(os.path.join(HERE, *parts), "rb", "utf-8") as f: + return f.read() + + +if __name__ == "__main__": + setup( + python_requires="~=3.10", + name=DISTNAME, + description=DESCRIPTION, + license=LICENSE, + url=URL, + version=VERSION, + author=AUTHOR, + author_email=EMAIL, + maintainer=AUTHOR, + maintainer_email=EMAIL, + keywords=KEYWORDS, + long_description=__doc__, + packages=PACKAGES, + zip_safe=False, + classifiers=CLASSIFIERS, + install_requires=INSTALL_REQUIRES, + include_package_data=True, + package_dir={"": "src"}, + package_data={"performance-metrics": ["py.typed"]}, + ) diff --git a/performance-metrics/src/performance_metrics/__init__.py b/performance-metrics/src/performance_metrics/__init__.py new file mode 100644 index 00000000000..a92b39b6d7b --- /dev/null +++ b/performance-metrics/src/performance_metrics/__init__.py @@ -0,0 +1 @@ +"""Opentrons performance metrics library.""" diff --git a/performance-metrics/src/performance_metrics/py.typed b/performance-metrics/src/performance_metrics/py.typed new file mode 100644 index 00000000000..e69de29bb2d From d8defe546b4e4d6203572911415efc8adfcf9e9c Mon Sep 17 00:00:00 2001 From: koji Date: Mon, 8 Apr 2024 12:48:19 -0400 Subject: [PATCH 067/194] fix(shared-data, components, app): fix runtime parameter min-max range for float (#14833) * fix(shared-data, components, app): fix runtime parameter min-max range for float --- .../__tests__/ProtocolParameters.test.tsx | 2 +- app/src/pages/ProtocolDetails/Parameters.tsx | 10 +++-- .../__tests__/ParametersTable.test.tsx | 2 +- .../src/molecules/ParametersTable/index.tsx | 17 ++++----- .../formatRunTimeParameterMinMax.test.tsx | 37 +++++++++++++++++++ .../helpers/formatRunTimeParameterMinMax.ts | 11 ++++++ shared-data/js/helpers/index.ts | 1 + 7 files changed, 64 insertions(+), 16 deletions(-) create mode 100644 shared-data/js/helpers/__tests__/formatRunTimeParameterMinMax.test.tsx create mode 100644 shared-data/js/helpers/formatRunTimeParameterMinMax.ts diff --git a/app/src/organisms/ProtocolDetails/ProtocolParameters/__tests__/ProtocolParameters.test.tsx b/app/src/organisms/ProtocolDetails/ProtocolParameters/__tests__/ProtocolParameters.test.tsx index 173a03f0c7a..191329bbae8 100644 --- a/app/src/organisms/ProtocolDetails/ProtocolParameters/__tests__/ProtocolParameters.test.tsx +++ b/app/src/organisms/ProtocolDetails/ProtocolParameters/__tests__/ProtocolParameters.test.tsx @@ -122,7 +122,7 @@ describe('ProtocolParameters', () => { screen.getByText('EtoH Volume') screen.getByText('6.5 mL') - screen.getByText('1.5-10') + screen.getByText('1.5-10.0') screen.getByText('Default Module Offsets') screen.getByText('No offsets') diff --git a/app/src/pages/ProtocolDetails/Parameters.tsx b/app/src/pages/ProtocolDetails/Parameters.tsx index b8cbfa71155..b908b5b84d7 100644 --- a/app/src/pages/ProtocolDetails/Parameters.tsx +++ b/app/src/pages/ProtocolDetails/Parameters.tsx @@ -1,7 +1,10 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' import styled from 'styled-components' -import { formatRunTimeParameterDefaultValue } from '@opentrons/shared-data' +import { + formatRunTimeParameterDefaultValue, + formatRunTimeParameterMinMax, +} from '@opentrons/shared-data' import { BORDERS, COLORS, @@ -61,9 +64,8 @@ export const Parameters = (props: { protocolId: string }): JSX.Element => { const getRange = (parameter: RunTimeParameter): string => { const { type } = parameter - const min = 'min' in parameter ? parameter.min : 0 - const max = 'max' in parameter ? parameter.max : 0 const numChoices = 'choices' in parameter ? parameter.choices.length : 0 + const minMax = formatRunTimeParameterMinMax(parameter) let range: string | null = null if (numChoices === 2 && 'choices' in parameter) { range = `${parameter.choices[0].displayName}, ${parameter.choices[1].displayName}` @@ -75,7 +77,7 @@ export const Parameters = (props: { protocolId: string }): JSX.Element => { } case 'float': case 'int': { - return `${min}-${max}` + return minMax } case 'str': { return range ?? t('num_choices', { num: numChoices }) diff --git a/components/src/molecules/ParametersTable/__tests__/ParametersTable.test.tsx b/components/src/molecules/ParametersTable/__tests__/ParametersTable.test.tsx index aee232ebf8c..5cd4b59a59b 100644 --- a/components/src/molecules/ParametersTable/__tests__/ParametersTable.test.tsx +++ b/components/src/molecules/ParametersTable/__tests__/ParametersTable.test.tsx @@ -98,7 +98,7 @@ describe('ParametersTable', () => { screen.getByText('EtoH Volume') screen.getByText('6.5 mL') - screen.getByText('1.5-10') + screen.getByText('1.5-10.0') // more than 2 options screen.getByText('Default Module Offsets') diff --git a/components/src/molecules/ParametersTable/index.tsx b/components/src/molecules/ParametersTable/index.tsx index 4ca8d8a2cb0..485a5efc6e5 100644 --- a/components/src/molecules/ParametersTable/index.tsx +++ b/components/src/molecules/ParametersTable/index.tsx @@ -1,6 +1,9 @@ import * as React from 'react' import styled, { css } from 'styled-components' -import { formatRunTimeParameterDefaultValue } from '@opentrons/shared-data' +import { + formatRunTimeParameterDefaultValue, + formatRunTimeParameterMinMax, +} from '@opentrons/shared-data' import { BORDERS, COLORS } from '../../helix-design-system' import { SPACING, TYPOGRAPHY } from '../../ui-style-constants/index' import { StyledText } from '../../atoms/StyledText' @@ -23,11 +26,9 @@ export function ParametersTable({ runTimeParameters, t, }: ProtocolParameterItemsProps): JSX.Element { - const formatRange = ( - runTimeParameter: RunTimeParameter, - minMax: string - ): string => { + const formatRange = (runTimeParameter: RunTimeParameter): string => { const { type } = runTimeParameter + const minMax = formatRunTimeParameterMinMax(runTimeParameter) const choices = 'choices' in runTimeParameter ? runTimeParameter.choices : [] const count = choices.length @@ -64,8 +65,6 @@ export function ParametersTable({ {runTimeParameters.map((parameter: RunTimeParameter, index: number) => { - const min = 'min' in parameter ? parameter.min : 0 - const max = 'max' in parameter ? parameter.max : 0 return ( - - {formatRange(parameter, `${min}-${max}`)} - + {formatRange(parameter)}
) diff --git a/shared-data/js/helpers/__tests__/formatRunTimeParameterMinMax.test.tsx b/shared-data/js/helpers/__tests__/formatRunTimeParameterMinMax.test.tsx new file mode 100644 index 00000000000..07190fac23e --- /dev/null +++ b/shared-data/js/helpers/__tests__/formatRunTimeParameterMinMax.test.tsx @@ -0,0 +1,37 @@ +import { describe, it, expect } from 'vitest' +import { formatRunTimeParameterMinMax } from '../formatRunTimeParameterMinMax' + +import type { RunTimeParameter } from '../../types' + +describe('utils-formatRunTimeParameterMinMax', () => { + it('should return int min and max', () => { + const mockData = { + value: 6, + displayName: 'PCR Cycles', + variableName: 'PCR_CYCLES', + description: 'number of PCR cycles on a thermocycler', + type: 'int', + min: 1, + max: 10, + default: 6, + } as RunTimeParameter + const result = formatRunTimeParameterMinMax(mockData) + expect(result).toEqual('1-10') + }) + + it('should return value with suffix when type is float', () => { + const mockData = { + value: 6.5, + displayName: 'EtoH Volume', + variableName: 'ETOH_VOLUME', + description: '70% ethanol volume', + type: 'float', + suffix: 'mL', + min: 1.5, + max: 10.0, + default: 6.5, + } as RunTimeParameter + const result = formatRunTimeParameterMinMax(mockData) + expect(result).toEqual('1.5-10.0') + }) +}) diff --git a/shared-data/js/helpers/formatRunTimeParameterMinMax.ts b/shared-data/js/helpers/formatRunTimeParameterMinMax.ts new file mode 100644 index 00000000000..36444f89601 --- /dev/null +++ b/shared-data/js/helpers/formatRunTimeParameterMinMax.ts @@ -0,0 +1,11 @@ +import type { RunTimeParameter } from '../types' + +export const formatRunTimeParameterMinMax = ( + runTimeParameter: RunTimeParameter +): string => { + const min = 'min' in runTimeParameter ? runTimeParameter.min : 0 + const max = 'max' in runTimeParameter ? runTimeParameter.max : 0 + return runTimeParameter.type === 'int' + ? `${min}-${max}` + : `${min.toFixed(1)}-${max.toFixed(1)}` +} diff --git a/shared-data/js/helpers/index.ts b/shared-data/js/helpers/index.ts index b996606f6e8..854b82d5133 100644 --- a/shared-data/js/helpers/index.ts +++ b/shared-data/js/helpers/index.ts @@ -30,6 +30,7 @@ export * from './getAddressableAreasInProtocol' export * from './getSimplestFlexDeckConfig' export * from './formatRunTimeParameterDefaultValue' export * from './formatRunTimeParameterValue' +export * from './formatRunTimeParameterMinMax' export const getLabwareDefIsStandard = (def: LabwareDefinition2): boolean => def?.namespace === OPENTRONS_LABWARE_NAMESPACE From 3385bf1d64d6c1ff44e809c70bbc0660c170274e Mon Sep 17 00:00:00 2001 From: koji Date: Mon, 8 Apr 2024 13:00:54 -0400 Subject: [PATCH 068/194] refactor(components): update parameter table stories (#14815) * refactor(components): update parameter table stories --- components/src/atoms/Chip/Chip.stories.tsx | 6 +-- .../ParametersTable.stories.tsx | 40 ++++++++++++------- 2 files changed, 27 insertions(+), 19 deletions(-) diff --git a/components/src/atoms/Chip/Chip.stories.tsx b/components/src/atoms/Chip/Chip.stories.tsx index 2868d7246f7..027ea4cbdbe 100644 --- a/components/src/atoms/Chip/Chip.stories.tsx +++ b/components/src/atoms/Chip/Chip.stories.tsx @@ -14,27 +14,23 @@ const meta: Meta = { control: { type: 'select', }, - defaultValue: 'basic', }, hasIcon: { control: { type: 'boolean', }, - defaultValue: true, }, chipSize: { options: ['medium', 'small'], control: { type: 'select', }, - defaultValue: 'medium', }, iconName: { options: ['connection-status', 'ot-check', 'ot-alert'], control: { type: 'select', }, - defaultValue: 'ot-alert', }, }, component: Chip, @@ -57,7 +53,7 @@ type Story = StoryObj export const ChipComponent: Story = { args: { - type: 'basic', + type: 'success', text: 'Chip component', hasIcon: true, chipSize: 'medium', diff --git a/components/src/molecules/ParametersTable/ParametersTable.stories.tsx b/components/src/molecules/ParametersTable/ParametersTable.stories.tsx index 93ba92cfdd4..d68e2f80a95 100644 --- a/components/src/molecules/ParametersTable/ParametersTable.stories.tsx +++ b/components/src/molecules/ParametersTable/ParametersTable.stories.tsx @@ -1,15 +1,10 @@ -import * as React from 'react' -import { ParametersTable } from '@opentrons/components' -import type { Story, Meta } from '@storybook/react' -import type { RunTimeParameter } from '@opentrons/shared-data' - -export default { - title: 'Library/Molecules/ParametersTable', -} as Meta +import * as React from 'react-remove-scroll' +import { Flex } from '../../primitives' +import { SPACING } from '../../ui-style-constants' +import { ParametersTable } from './index' -const Template: Story> = args => ( - -) +import type { Meta, StoryObj } from '@storybook/react' +import type { RunTimeParameter } from '@opentrons/shared-data' const runTimeParameters: RunTimeParameter[] = [ { @@ -153,7 +148,24 @@ const runTimeParameters: RunTimeParameter[] = [ default: 'flex', }, ] -export const Default = Template.bind({}) -Default.args = { - runTimeParameters: runTimeParameters, + +const meta: Meta = { + title: 'Library/Molecules/ParametersTable', + component: ParametersTable, + decorators: [ + Story => ( + + + + ), + ], +} +export default meta + +type Story = StoryObj + +export const DefaultParameterTable: Story = { + args: { + runTimeParameters: runTimeParameters, + }, } From 88c3f2c3261c5bfa25ec24842e695106968b95ae Mon Sep 17 00:00:00 2001 From: koji Date: Mon, 8 Apr 2024 13:01:13 -0400 Subject: [PATCH 069/194] refactor(components): update Box stories (#14827) * refactor(components): update Box stories --- components/src/primitives/Box.stories.tsx | 34 +++++++++++++---------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/components/src/primitives/Box.stories.tsx b/components/src/primitives/Box.stories.tsx index 3d322842a0a..54fd773d125 100644 --- a/components/src/primitives/Box.stories.tsx +++ b/components/src/primitives/Box.stories.tsx @@ -1,21 +1,25 @@ -import * as React from 'react' +import { COLORS, BORDERS } from '../helix-design-system' +import { SPACING } from '../ui-style-constants' import { Box as BoxComponent } from './Box' -import type { Story, Meta } from '@storybook/react' +import type { Meta, StoryObj } from '@storybook/react' -export default { +const meta: Meta = { title: 'Library/Atoms/Box', -} as Meta + component: BoxComponent, +} + +export default meta + +type Story = StoryObj -const Template: Story> = args => ( - -) -export const Box = Template.bind({}) -Box.args = { - children: - 'This is a simple box atom that accepts all primitive styling props.', - backgroundColor: 'grey', - border: '1px solid black', - padding: '1rem', - maxWidth: '20rem', +export const Box: Story = { + args: { + children: + 'This is a simple box atom that accepts all primitive styling props.', + backgroundColor: COLORS.grey60, + border: `1px ${BORDERS.styleSolid} black`, + padding: SPACING.spacing16, + maxWidth: '20rem', + }, } From 1a5052cbb338d0f42e0587f132009a5892481f41 Mon Sep 17 00:00:00 2001 From: Derek Maggio Date: Mon, 8 Apr 2024 12:43:41 -0700 Subject: [PATCH 070/194] Exec 372 hide performance metrics project behind ff (#14811) # Overview Add feature flag for Performance Metrics project. Closes https://opentrons.atlassian.net/browse/EXEC-372 # Changelog - Add enablePerformanceMetrics feature flag in advanced settings - Add migration function - Update tests --- api/src/opentrons/config/advanced_settings.py | 22 ++++++++++++++++++ api/src/opentrons/config/feature_flags.py | 4 ++++ .../config/test_advanced_settings.py | 23 +++++++++++++------ .../test_advanced_settings_migration.py | 17 +++++++++++++- 4 files changed, 58 insertions(+), 8 deletions(-) diff --git a/api/src/opentrons/config/advanced_settings.py b/api/src/opentrons/config/advanced_settings.py index 191c0d69ccc..f4c75701901 100644 --- a/api/src/opentrons/config/advanced_settings.py +++ b/api/src/opentrons/config/advanced_settings.py @@ -240,6 +240,17 @@ class Setting(NamedTuple): robot_type=[RobotTypeEnum.FLEX], internal_only=True, ), + SettingDefinition( + _id="enablePerformanceMetrics", + title="Enable performance metrics", + description=( + "Do not enable." + " This is an Opentrons internal setting to collect performance metrics." + " Do not turn this on unless you are playing with the performance metrics system." + ), + robot_type=[RobotTypeEnum.OT2, RobotTypeEnum.FLEX], + internal_only=True, + ), ] if ( @@ -709,6 +720,16 @@ def _migrate31to32(previous: SettingsMap) -> SettingsMap: return newmap +def _migrate32to33(previous: SettingsMap) -> SettingsMap: + """Migrate to version 33 of the feature flags file. + + - Adds the enablePerformanceMetrics config element. + """ + newmap = {k: v for k, v in previous.items()} + newmap["enablePerformanceMetrics"] = None + return newmap + + _MIGRATIONS = [ _migrate0to1, _migrate1to2, @@ -742,6 +763,7 @@ def _migrate31to32(previous: SettingsMap) -> SettingsMap: _migrate29to30, _migrate30to31, _migrate31to32, + _migrate32to33, ] """ List of all migrations to apply, indexed by (version - 1). See _migrate below diff --git a/api/src/opentrons/config/feature_flags.py b/api/src/opentrons/config/feature_flags.py index 4a1161a2391..e9772a01ee8 100644 --- a/api/src/opentrons/config/feature_flags.py +++ b/api/src/opentrons/config/feature_flags.py @@ -76,3 +76,7 @@ def enable_error_recovery_experiments() -> bool: return advs.get_setting_with_env_overload( "enableErrorRecoveryExperiments", RobotTypeEnum.FLEX ) + + +def enable_performance_metrics(robot_type: RobotTypeEnum) -> bool: + return advs.get_setting_with_env_overload("enablePerformanceMetrics", robot_type) diff --git a/api/tests/opentrons/config/test_advanced_settings.py b/api/tests/opentrons/config/test_advanced_settings.py index b81b9149c67..17122fca0dd 100644 --- a/api/tests/opentrons/config/test_advanced_settings.py +++ b/api/tests/opentrons/config/test_advanced_settings.py @@ -34,6 +34,15 @@ def mock_settings_values_flex() -> Dict[str, Optional[bool]]: } +@pytest.fixture +def mock_settings_values_flex_all() -> Dict[str, Optional[bool]]: + return { + s.id: False + for s in advanced_settings.settings + if RobotTypeEnum.FLEX in s.robot_type + } + + @pytest.fixture def mock_settings_values_empty() -> Dict[str, Optional[bool]]: return {s.id: None for s in advanced_settings.settings} @@ -57,12 +66,12 @@ def mock_settings( @pytest.fixture def mock_read_settings_file_ot2( - mock_settings_values_ot2: Dict[str, Optional[bool]], + mock_settings_values_ot2_all: Dict[str, Optional[bool]], mock_settings_version: int, ) -> Generator[MagicMock, None, None]: with patch("opentrons.config.advanced_settings._read_settings_file") as p: p.return_value = advanced_settings.SettingsData( - settings_map=mock_settings_values_ot2, + settings_map=mock_settings_values_ot2_all, version=mock_settings_version, ) yield p @@ -70,12 +79,12 @@ def mock_read_settings_file_ot2( @pytest.fixture def mock_read_settings_file_flex( - mock_settings_values_flex: Dict[str, Optional[bool]], + mock_settings_values_flex_all: Dict[str, Optional[bool]], mock_settings_version: int, ) -> Generator[MagicMock, None, None]: with patch("opentrons.config.advanced_settings._read_settings_file") as p: p.return_value = advanced_settings.SettingsData( - settings_map=mock_settings_values_flex, + settings_map=mock_settings_values_flex_all, version=mock_settings_version, ) yield p @@ -168,19 +177,19 @@ def test_get_all_adv_settings_empty( async def test_set_adv_setting( mock_read_settings_file_ot2: MagicMock, - mock_settings_values_ot2: MagicMock, + mock_settings_values_ot2_all: MagicMock, mock_write_settings_file: MagicMock, mock_settings_version: int, restore_restart_required: None, ) -> None: - for k, v in mock_settings_values_ot2.items(): + for k, v in mock_settings_values_ot2_all.items(): # Toggle the advanced setting await advanced_settings.set_adv_setting(k, not v) mock_write_settings_file.assert_called_with( # Only the current key is toggled { nk: nv if nk != k else not v - for nk, nv in mock_settings_values_ot2.items() + for nk, nv in mock_settings_values_ot2_all.items() }, mock_settings_version, CONFIG["feature_flags_file"], diff --git a/api/tests/opentrons/config/test_advanced_settings_migration.py b/api/tests/opentrons/config/test_advanced_settings_migration.py index e1c3f51b651..e3269433db5 100644 --- a/api/tests/opentrons/config/test_advanced_settings_migration.py +++ b/api/tests/opentrons/config/test_advanced_settings_migration.py @@ -8,7 +8,7 @@ @pytest.fixture def migrated_file_version() -> int: - return 32 + return 33 # make sure to set a boolean value in default_file_settings only if @@ -31,6 +31,7 @@ def default_file_settings() -> Dict[str, Any]: "estopNotRequired": None, "enableErrorRecoveryExperiments": None, "enableOEMMode": None, + "enablePerformanceMetrics": None, } @@ -392,6 +393,18 @@ def v32_config(v31_config: Dict[str, Any]) -> Dict[str, Any]: return r +@pytest.fixture +def v33_config(v32_config: Dict[str, Any]) -> Dict[str, Any]: + r = v32_config.copy() + r.update( + { + "_version": 33, + "enablePerformanceMetrics": None, + } + ) + return r + + @pytest.fixture( scope="session", params=[ @@ -429,6 +442,7 @@ def v32_config(v31_config: Dict[str, Any]) -> Dict[str, Any]: lazy_fixture("v30_config"), lazy_fixture("v31_config"), lazy_fixture("v32_config"), + lazy_fixture("v33_config"), ], ) def old_settings(request: SubRequest) -> Dict[str, Any]: @@ -522,4 +536,5 @@ def test_ensures_config() -> None: "disableOverpressureDetection": None, "enableErrorRecoveryExperiments": None, "enableOEMMode": None, + "enablePerformanceMetrics": None, } From e620a8cf40ce5a84577005178ec5ecd61b23bbed Mon Sep 17 00:00:00 2001 From: koji Date: Mon, 8 Apr 2024 15:58:47 -0400 Subject: [PATCH 071/194] refactor(app): remove RTP feature flag (#14837) * refactor(app): remove RTP feature flag --- .../assets/localization/en/app_settings.json | 1 - .../__tests__/ChooseProtocolSlideout.test.tsx | 50 ++++++++++++++++--- .../ChooseProtocolSlideout/index.tsx | 11 ++-- .../organisms/ChooseRobotSlideout/index.tsx | 4 +- .../index.tsx | 5 +- app/src/organisms/ProtocolDetails/index.tsx | 10 ++-- app/src/organisms/RunTimeControl/hooks.ts | 9 +--- .../Devices/ProtocolRunDetails/index.tsx | 7 +-- app/src/pages/ProtocolDetails/index.tsx | 12 ++--- app/src/pages/ProtocolSetup/index.tsx | 29 +++++------ app/src/redux/config/constants.ts | 1 - app/src/redux/config/schema-types.ts | 1 - .../protocol-storage/__fixtures__/index.ts | 2 +- 13 files changed, 73 insertions(+), 69 deletions(-) diff --git a/app/src/assets/localization/en/app_settings.json b/app/src/assets/localization/en/app_settings.json index 4a00283f3de..18e3eef9e8a 100644 --- a/app/src/assets/localization/en/app_settings.json +++ b/app/src/assets/localization/en/app_settings.json @@ -1,6 +1,5 @@ { "__dev_internal__protocolStats": "Protocol Stats", - "__dev_internal__enableRunTimeParameters": "Enable Run Time Parameters", "__dev_internal__enableRunNotes": "Display Notes During a Protocol Run", "__dev_internal__enableQuickTransfer": "Enable Quick Transfer", "add_folder_button": "Add labware source folder", diff --git a/app/src/organisms/ChooseProtocolSlideout/__tests__/ChooseProtocolSlideout.test.tsx b/app/src/organisms/ChooseProtocolSlideout/__tests__/ChooseProtocolSlideout.test.tsx index d5b910381bd..11583264b3e 100644 --- a/app/src/organisms/ChooseProtocolSlideout/__tests__/ChooseProtocolSlideout.test.tsx +++ b/app/src/organisms/ChooseProtocolSlideout/__tests__/ChooseProtocolSlideout.test.tsx @@ -58,6 +58,7 @@ describe('ChooseProtocolSlideout', () => { screen.getByText(/choose protocol to run/i) screen.getByText(/opentrons-robot-name/i) }) + it('renders an available protocol option for every stored protocol if any', () => { render({ robot: mockConnectableRobot, @@ -70,6 +71,7 @@ describe('ChooseProtocolSlideout', () => { screen.queryByRole('heading', { name: 'No protocols found' }) ).toBeNull() }) + it('renders an empty state if no protocol options', () => { vi.mocked(getStoredProtocols).mockReturnValue([]) render({ @@ -83,22 +85,55 @@ describe('ChooseProtocolSlideout', () => { screen.getByRole('heading', { name: 'No protocols found' }) ).toBeInTheDocument() }) - it('calls createRunFromProtocolSource if CTA clicked', () => { + + // it('calls createRunFromProtocolSource if CTA clicked', () => { + // const protocolDataWithoutRunTimeParameter = { + // ...storedProtocolDataFixture, + // runTimeParameters: [], + // } + // vi.mocked(getStoredProtocols).mockReturnValue([ + // protocolDataWithoutRunTimeParameter, + // ]) + // render({ + // robot: mockConnectableRobot, + // onCloseClick: vi.fn(), + // showSlideout: true, + // }) + // const proceedButton = screen.getByRole('button', { + // name: 'Proceed to setup', + // }) + // fireEvent.click(proceedButton) + // expect(mockCreateRunFromProtocol).toHaveBeenCalledWith({ + // files: [expect.any(File)], + // protocolKey: storedProtocolDataFixture.protocolKey, + // }) + // expect(mockTrackCreateProtocolRunEvent).toHaveBeenCalled() + // }) + + it('move to the second slideout if CTA clicked', () => { + const protocolDataWithoutRunTimeParameter = { + ...storedProtocolDataFixture, + runTimeParameters: [], + } + vi.mocked(getStoredProtocols).mockReturnValue([ + protocolDataWithoutRunTimeParameter, + ]) render({ robot: mockConnectableRobot, onCloseClick: vi.fn(), showSlideout: true, }) const proceedButton = screen.getByRole('button', { - name: 'Proceed to setup', + name: 'Continue to parameters', }) fireEvent.click(proceedButton) - expect(mockCreateRunFromProtocol).toHaveBeenCalledWith({ - files: [expect.any(File)], - protocolKey: storedProtocolDataFixture.protocolKey, - }) - expect(mockTrackCreateProtocolRunEvent).toHaveBeenCalled() + screen.getByText('Step 2 / 2') + screen.getByText('number of samples') + screen.getByText('Restore default values') }) + + // ToDo (kk:04/08) update test for RTP + /* it('renders error state when there is a run creation error', () => { vi.mocked(useCreateRunFromProtocol).mockReturnValue({ runCreationError: 'run creation error', @@ -153,4 +188,5 @@ describe('ChooseProtocolSlideout', () => { fireEvent.click(link) expect(link.getAttribute('href')).toEqual('/devices/opentrons-robot-name') }) + */ }) diff --git a/app/src/organisms/ChooseProtocolSlideout/index.tsx b/app/src/organisms/ChooseProtocolSlideout/index.tsx index b2d48540ae8..fd9085e07cb 100644 --- a/app/src/organisms/ChooseProtocolSlideout/index.tsx +++ b/app/src/organisms/ChooseProtocolSlideout/index.tsx @@ -2,7 +2,6 @@ import * as React from 'react' import first from 'lodash/first' import { Trans, useTranslation } from 'react-i18next' import { Link, NavLink, useHistory } from 'react-router-dom' -import { ApiHostProvider } from '@opentrons/react-api-client' import { useSelector } from 'react-redux' import { css } from 'styled-components' @@ -14,7 +13,6 @@ import { DIRECTION_COLUMN, DIRECTION_ROW, DISPLAY_BLOCK, - DropdownOption, Flex, Icon, Link as LinkComponent, @@ -30,12 +28,12 @@ import { TYPOGRAPHY, useHoverTooltip, } from '@opentrons/components' +import { ApiHostProvider } from '@opentrons/react-api-client' import { useLogger } from '../../logger' import { OPENTRONS_USB } from '../../redux/discovery' import { getStoredProtocols } from '../../redux/protocol-storage' import { appShellRequestor } from '../../redux/shell/remote' -import { useFeatureFlag } from '../../redux/config' import { MultiSlideout } from '../../atoms/Slideout/MultiSlideout' import { Tooltip } from '../../atoms/Tooltip' import { ToggleButton } from '../../atoms/buttons' @@ -47,8 +45,10 @@ import { useCreateRunFromProtocol } from '../ChooseRobotToRunProtocolSlideout/us import { ApplyHistoricOffsets } from '../ApplyHistoricOffsets' import { useOffsetCandidatesForAnalysis } from '../ApplyHistoricOffsets/hooks/useOffsetCandidatesForAnalysis' import { getAnalysisStatus } from '../ProtocolsLanding/utils' + import type { RunTimeParameterCreateData } from '@opentrons/api-client' import type { RunTimeParameter } from '@opentrons/shared-data' +import type { DropdownOption } from '@opentrons/components' import type { Robot } from '../../redux/discovery/types' import type { StoredProtocolData } from '../../redux/protocol-storage' import type { State } from '../../redux/types' @@ -93,7 +93,6 @@ export function ChooseProtocolSlideoutComponent( ] = React.useState([]) const [currentPage, setCurrentPage] = React.useState(1) const [hasParamError, setHasParamError] = React.useState(false) - const enableRunTimeParametersFF = useFeatureFlag('enableRunTimeParameters') React.useEffect(() => { setRunTimeParametersOverrides( @@ -106,9 +105,9 @@ export function ChooseProtocolSlideoutComponent( const runTimeParametersFromAnalysis = selectedProtocol?.mostRecentAnalysis?.runTimeParameters ?? [] + console.log('runTimeParametersFromAnalysis', runTimeParametersFromAnalysis) - const hasRunTimeParameters = - enableRunTimeParametersFF && runTimeParametersFromAnalysis.length > 0 + const hasRunTimeParameters = runTimeParametersFromAnalysis.length > 0 const analysisStatus = getAnalysisStatus( false, diff --git a/app/src/organisms/ChooseRobotSlideout/index.tsx b/app/src/organisms/ChooseRobotSlideout/index.tsx index 904615b9ca5..d19a62a514d 100644 --- a/app/src/organisms/ChooseRobotSlideout/index.tsx +++ b/app/src/organisms/ChooseRobotSlideout/index.tsx @@ -51,7 +51,6 @@ import type { SlideoutProps } from '../../atoms/Slideout' import type { UseCreateRun } from '../../organisms/ChooseRobotToRunProtocolSlideout/useCreateRunFromProtocol' import type { State, Dispatch } from '../../redux/types' import type { Robot } from '../../redux/discovery/types' -import { useFeatureFlag } from '../../redux/config' import type { DropdownOption } from '../../atoms/MenuList/DropdownMenu' export const CARD_OUTLINE_BORDER_STYLE = css` @@ -142,7 +141,6 @@ export function ChooseRobotSlideout( setHasParamError, } = props - const enableRunTimeParametersFF = useFeatureFlag('enableRunTimeParameters') const dispatch = useDispatch() const isScanning = useSelector((state: State) => getScanning(state)) const [targetProps, tooltipProps] = useHoverTooltip() @@ -526,7 +524,7 @@ export function ChooseRobotSlideout(
) : null - return multiSlideout != null && enableRunTimeParametersFF ? ( + return multiSlideout != null ? ( (1) const [selectedRobot, setSelectedRobot] = React.useState(null) const { trackCreateProtocolRunEvent } = useTrackCreateProtocolRunEvent( @@ -176,8 +174,7 @@ export function ChooseRobotToRunProtocolSlideoutComponent( ) - const hasRunTimeParameters = - enableRunTimeParametersFF && runTimeParameters.length > 0 + const hasRunTimeParameters = runTimeParameters.length > 0 return ( 0 + const hasRunTimeParameters = runTimeParameters.length > 0 const [currentTab, setCurrentTab] = React.useState< 'robot_config' | 'labware' | 'liquids' | 'stats' | 'parameters' >(hasRunTimeParameters ? 'parameters' : 'robot_config') @@ -333,9 +331,7 @@ export function ProtocolDetails( stats: enableProtocolStats ? ( ) : null, - parameters: enableRunTimeParameters ? ( - - ) : null, + parameters: , } const deckMap = @@ -596,7 +592,7 @@ export function ProtocolDetails( gridGap={SPACING.spacing8} > - {enableRunTimeParameters && mostRecentAnalysis != null && ( + {mostRecentAnalysis != null && ( (null) const listRef = React.useRef(null) const [jumpedIndex, setJumpedIndex] = React.useState(null) - const enableRunTimeParameters = useFeatureFlag('enableRunTimeParameters') + React.useEffect(() => { if (jumpedIndex != null) { setTimeout(() => setJumpedIndex(null), JUMPED_STEP_HIGHLIGHT_DELAY_MS) @@ -236,9 +235,7 @@ function PageContents(props: PageContentsProps): JSX.Element { /> - {enableRunTimeParameters ? ( - - ) : null} + diff --git a/app/src/pages/ProtocolDetails/index.tsx b/app/src/pages/ProtocolDetails/index.tsx index e44e3f7015b..0503c0eae54 100644 --- a/app/src/pages/ProtocolDetails/index.tsx +++ b/app/src/pages/ProtocolDetails/index.tsx @@ -44,7 +44,6 @@ import { getApplyHistoricOffsets, getPinnedProtocolIds, updateConfigValue, - useFeatureFlag, } from '../../redux/config' import { useOffsetCandidatesForAnalysis } from '../../organisms/ApplyHistoricOffsets/hooks/useOffsetCandidatesForAnalysis' import { @@ -189,10 +188,8 @@ const ProtocolSectionTabs = ({ currentOption, setCurrentOption, }: ProtocolSectionTabsProps): JSX.Element => { - const enableRtpFF = useFeatureFlag('enableRunTimeParameters') - const options = enableRtpFF - ? protocolSectionTabOptions - : protocolSectionTabOptionsWithoutParameters + const options = protocolSectionTabOptions + return ( {options.map(option => { @@ -308,7 +305,6 @@ export function ProtocolDetails(): JSX.Element | null { 'protocol_info', 'shared', ]) - const enableRtpFF = useFeatureFlag('enableRunTimeParameters') const { protocolId } = useParams() const { missingProtocolHardware, @@ -326,9 +322,7 @@ export function ProtocolDetails(): JSX.Element | null { const [showParameters, setShowParameters] = React.useState(false) const queryClient = useQueryClient() const [currentOption, setCurrentOption] = React.useState( - enableRtpFF - ? protocolSectionTabOptions[0] - : protocolSectionTabOptionsWithoutParameters[0] + protocolSectionTabOptions[0] ) const [showMaxPinsAlert, setShowMaxPinsAlert] = React.useState(false) diff --git a/app/src/pages/ProtocolSetup/index.tsx b/app/src/pages/ProtocolSetup/index.tsx index be90fcfa80e..f2fb24feaa5 100644 --- a/app/src/pages/ProtocolSetup/index.tsx +++ b/app/src/pages/ProtocolSetup/index.tsx @@ -82,7 +82,7 @@ import { ANALYTICS_PROTOCOL_RUN_START, useTrackEvent, } from '../../redux/analytics' -import { getIsHeaterShakerAttached, useFeatureFlag } from '../../redux/config' +import { getIsHeaterShakerAttached } from '../../redux/config' import { ConfirmAttachedModal } from './ConfirmAttachedModal' import { getLatestCurrentOffsets } from '../../organisms/Devices/ProtocolRun/SetupLabwarePositionCheck/utils' import { CloseButton, PlayButton } from './Buttons' @@ -257,7 +257,6 @@ function PrepareToRun({ const { t, i18n } = useTranslation(['protocol_setup', 'shared']) const history = useHistory() const { makeSnackbar } = useToaster() - const enableRunTimeParametersFF = useFeatureFlag('enableRunTimeParameters') const hasRunTimeParameters = useProtocolHasRunTimeParameters(runId) // Watch for scrolling to toggle dropshadow const scrollRef = React.useRef(null) @@ -730,20 +729,18 @@ function PrepareToRun({ disabled={lpcDisabledReason != null} disabledReason={lpcDisabledReason} /> - {enableRunTimeParametersFF ? ( - setSetupScreen('view only parameters')} - title={t('parameters')} - detail={t( - hasRunTimeParameters - ? parametersDetail - : t('no_parameters_specified') - )} - subDetail={null} - status="general" - disabled={!hasRunTimeParameters} - /> - ) : null} + setSetupScreen('view only parameters')} + title={t('parameters')} + detail={t( + hasRunTimeParameters + ? parametersDetail + : t('no_parameters_specified') + )} + subDetail={null} + status="general" + disabled={!hasRunTimeParameters} + /> setSetupScreen('labware')} title={t('labware')} diff --git a/app/src/redux/config/constants.ts b/app/src/redux/config/constants.ts index 1dc64fea2f4..5a72622f98e 100644 --- a/app/src/redux/config/constants.ts +++ b/app/src/redux/config/constants.ts @@ -2,7 +2,6 @@ import type { DevInternalFlag } from './types' export const DEV_INTERNAL_FLAGS: DevInternalFlag[] = [ 'protocolStats', - 'enableRunTimeParameters', 'enableRunNotes', 'enableQuickTransfer', ] diff --git a/app/src/redux/config/schema-types.ts b/app/src/redux/config/schema-types.ts index e69186f5f07..5728a2e4eb1 100644 --- a/app/src/redux/config/schema-types.ts +++ b/app/src/redux/config/schema-types.ts @@ -9,7 +9,6 @@ export type DiscoveryCandidates = string[] export type DevInternalFlag = | 'protocolStats' - | 'enableRunTimeParameters' | 'enableRunNotes' | 'enableQuickTransfer' diff --git a/app/src/redux/protocol-storage/__fixtures__/index.ts b/app/src/redux/protocol-storage/__fixtures__/index.ts index 12e350efb38..56f7f4d021a 100644 --- a/app/src/redux/protocol-storage/__fixtures__/index.ts +++ b/app/src/redux/protocol-storage/__fixtures__/index.ts @@ -1,5 +1,5 @@ import { simpleAnalysisFileFixture } from '@opentrons/api-client' -import { StoredProtocolData, StoredProtocolDir } from '../types' +import type { StoredProtocolData, StoredProtocolDir } from '../types' import type { ProtocolAnalysisOutput } from '@opentrons/shared-data' From 2a717d79e85ff00b538918d31306dc3554664519 Mon Sep 17 00:00:00 2001 From: Jethary Rader <66035149+jerader@users.noreply.github.com> Date: Mon, 8 Apr 2024 16:15:39 -0400 Subject: [PATCH 072/194] feat(protocol-designer): update unused module alert to account for MoaM (#14839) closes AUTH-23 --- .../components/FileSidebar/FileSidebar.tsx | 3 + .../__tests__/FileSidebar.test.tsx | 126 +++++++++++++++--- .../src/localization/en/alert.json | 8 +- 3 files changed, 115 insertions(+), 22 deletions(-) diff --git a/protocol-designer/src/components/FileSidebar/FileSidebar.tsx b/protocol-designer/src/components/FileSidebar/FileSidebar.tsx index 3049f036b4a..e05a80e3163 100644 --- a/protocol-designer/src/components/FileSidebar/FileSidebar.tsx +++ b/protocol-designer/src/components/FileSidebar/FileSidebar.tsx @@ -129,6 +129,7 @@ function getWarningContent({ const pipettesDetails = pipettesWithoutStep .map(pipette => `${pipette.mount} ${pipette.spec.displayName}`) .join(' and ') + const modulesDetails = modulesWithoutStep .map(moduleOnDeck => t(`modules:module_long_names.${moduleOnDeck.type}`)) .join(' and ') @@ -169,12 +170,14 @@ function getWarningContent({ if (modulesWithoutStep.length) { const moduleCase = modulesWithoutStep.length > 1 ? 'unused_modules' : 'unused_module' + const slotName = modulesWithoutStep.map(module => module.slot) return { content: ( <>

{t(`export_warnings.${moduleCase}.body1`, { modulesDetails, + slotName: slotName, })}

{t(`export_warnings.${moduleCase}.body2`)}

diff --git a/protocol-designer/src/components/FileSidebar/__tests__/FileSidebar.test.tsx b/protocol-designer/src/components/FileSidebar/__tests__/FileSidebar.test.tsx index ebe86be63a7..a9d2978b981 100644 --- a/protocol-designer/src/components/FileSidebar/__tests__/FileSidebar.test.tsx +++ b/protocol-designer/src/components/FileSidebar/__tests__/FileSidebar.test.tsx @@ -1,7 +1,11 @@ import * as React from 'react' import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest' import { fireEvent, screen, cleanup } from '@testing-library/react' -import { FLEX_ROBOT_TYPE } from '@opentrons/shared-data' +import { + FLEX_ROBOT_TYPE, + LabwareDefinition2, + fixtureTiprack300ul, +} from '@opentrons/shared-data' import { renderWithProviders } from '../../../__testing-utils__' import { createFile, getRobotType } from '../../../file-data/selectors' import { @@ -17,11 +21,8 @@ import { import { toggleNewProtocolModal } from '../../../navigation/actions' import { getHasUnsavedChanges } from '../../../load-file/selectors' import { useBlockingHint } from '../../Hints/useBlockingHint' -import { - getUnusedEntities, - getUnusedStagingAreas, - getUnusedTrash, -} from '../utils' +import { getUnusedStagingAreas } from '../utils/getUnusedStagingAreas' +import { getUnusedTrash } from '../utils/getUnusedTrash' import { FileSidebar } from '../FileSidebar' vi.mock('../../../step-forms/selectors') @@ -30,15 +31,14 @@ vi.mock('../../../navigation/actions') vi.mock('../../../navigation/selectors') vi.mock('../../../file-data/selectors') vi.mock('../../Hints/useBlockingHint') -vi.mock('../utils') - +vi.mock('../utils/getUnusedStagingAreas') +vi.mock('../utils/getUnusedTrash') const render = () => { return renderWithProviders(, { i18nInstance: i18n })[0] } describe('FileSidebar', () => { beforeEach(() => { - vi.mocked(getUnusedEntities).mockReturnValue([]) vi.mocked(getUnusedStagingAreas).mockReturnValue([]) vi.mocked(getUnusedTrash).mockReturnValue({ trashBinUnused: false, @@ -91,19 +91,54 @@ describe('FileSidebar', () => { fireEvent.click(screen.getByRole('button', { name: 'Export' })) screen.getByText('Your protocol has no steps') }) - it('renders the unused pipette and module warning', () => { - vi.mocked(getUnusedEntities).mockReturnValue([ - { - mount: 'left', - name: 'p1000_96', - id: 'pipetteId', - tiprackDefURI: 'mockURI', - spec: { - name: 'mock pip name', - displayName: 'mock display name', + it('renders the unused pipette warning', () => { + vi.mocked(getInitialDeckSetup).mockReturnValue({ + modules: {}, + pipettes: { + pipetteId: { + mount: 'left', + name: 'p1000_96', + id: 'pipetteId', + tiprackLabwareDef: [fixtureTiprack300ul as LabwareDefinition2], + tiprackDefURI: ['mockDefUri'], + spec: { + displayName: 'mock display name', + } as any, + }, + }, + additionalEquipmentOnDeck: {}, + labware: {}, + }) + render() + fireEvent.click(screen.getByRole('button', { name: 'Export' })) + screen.getByText('Unused pipette') + }) + it('renders the unused pieptte and module warning', () => { + vi.mocked(getInitialDeckSetup).mockReturnValue({ + modules: { + moduleId: { + slot: 'A1', + moduleState: {} as any, + id: 'moduleId', + type: 'temperatureModuleType', + model: 'temperatureModuleV2', + }, + }, + pipettes: { + pipetteId: { + mount: 'left', + name: 'p1000_96', + id: 'pipetteId', + tiprackLabwareDef: [fixtureTiprack300ul as LabwareDefinition2], + tiprackDefURI: ['mockDefUri'], + spec: { + displayName: 'mock display name', + } as any, }, }, - ]) + additionalEquipmentOnDeck: {}, + labware: {}, + }) render() fireEvent.click(screen.getByRole('button', { name: 'Export' })) screen.getByText('Unused pipette and module') @@ -140,4 +175,55 @@ describe('FileSidebar', () => { fireEvent.click(screen.getByRole('button', { name: 'Export' })) screen.getByText('Unused gripper') }) + it('renders the unused module warning', () => { + vi.mocked(getInitialDeckSetup).mockReturnValue({ + modules: { + moduleId: { + slot: 'A1', + moduleState: {} as any, + id: 'moduleId', + type: 'temperatureModuleType', + model: 'temperatureModuleV2', + }, + }, + pipettes: {}, + additionalEquipmentOnDeck: {}, + labware: {}, + }) + render() + fireEvent.click(screen.getByRole('button', { name: 'Export' })) + screen.getByText('Unused module') + screen.getByText( + 'The Temperature module specified in your protocol in Slot A1 is not currently used in any step. In order to run this protocol you will need to power up and connect the module to your robot.' + ) + }) + it('renders the unused modules warning', () => { + vi.mocked(getInitialDeckSetup).mockReturnValue({ + modules: { + moduleId: { + slot: 'A1', + moduleState: {} as any, + id: 'moduleId', + type: 'temperatureModuleType', + model: 'temperatureModuleV2', + }, + moduleId2: { + slot: 'B1', + moduleState: {} as any, + id: 'moduleId2', + type: 'temperatureModuleType', + model: 'temperatureModuleV2', + }, + }, + pipettes: {}, + additionalEquipmentOnDeck: {}, + labware: {}, + }) + render() + fireEvent.click(screen.getByRole('button', { name: 'Export' })) + screen.getByText('Unused modules') + screen.getByText( + 'One or more modules specified in your protocol in Slot(s) A1,B1 are not currently used in any step. In order to run this protocol you will need to power up and connect the modules to your robot.' + ) + }) }) diff --git a/protocol-designer/src/localization/en/alert.json b/protocol-designer/src/localization/en/alert.json index 272e51a9363..4548d19e57c 100644 --- a/protocol-designer/src/localization/en/alert.json +++ b/protocol-designer/src/localization/en/alert.json @@ -49,6 +49,10 @@ "title": "Missing labware", "body": "Your module has no labware on it. We recommend you add labware before proceeding." }, + "multiple_modules_without_labware": { + "title": "Missing labware", + "body": "One or more module has no labware on it. We recommend you add labware before proceeding" + }, "export_v8_protocol_7_1": { "title": "Robot and app update may be required", "body1": "This protocol can only run on app and robot server version", @@ -256,12 +260,12 @@ }, "unused_module": { "heading": "Unused module", - "body1": "The {{modulesDetails}} specified in your protocol are not currently used in any step. In order to run this protocol you will need to power up and connect the module to your robot.", + "body1": "The {{modulesDetails}} specified in your protocol in Slot {{slotName}} is not currently used in any step. In order to run this protocol you will need to power up and connect the module to your robot.", "body2": "If you don't intend to use the module, please consider removing it from your protocol." }, "unused_modules": { "heading": "Unused modules", - "body1": "The {{modulesDetails}} specified in your protocol are not currently used in any step. In order to run this protocol you will need to power up and connect the modules to your robot.", + "body1": "One or more modules specified in your protocol in Slot(s) {{slotName}} are not currently used in any step. In order to run this protocol you will need to power up and connect the modules to your robot.", "body2": "If you don't intend to use these modules, please consider removing them from your protocol." }, "unused_gripper": { From 75acb0559d029ac8c8835cb3cecf7e45b9b80dc6 Mon Sep 17 00:00:00 2001 From: Sanniti Pimpley Date: Mon, 8 Apr 2024 18:50:29 -0400 Subject: [PATCH 073/194] feat(robot server): add a POST method on the analyses endpoint (#14828) Closes AUTH-255 # Overview Adds a POST method to the existing `/protocols/{protocolId}/analyses` endpoint in order to post a new analysis for an existing protocol. This endpoint will take a request body with two optional fields: - `runTimeParameterValues` - `forceReAnalyze` The new method can affect the analyses in three ways: 1. When the request is sent with `forceReAnalyze=True`, the server will unconditionally start a new analysis for the protocol using any RTP data sent along with it. It will return a 201 CREATED status and respond with a list of analysis summaries of all the analyses (ordered oldest first), including the newly started analysis. 2. When the request is sent without the `forceReAnalyze` field (or with `forceReAnalyze=False`), then the server will check the last analysis of the protocol - if the RTP values used for it were **different** from the RTP values sent with the current request, then the server will start a new analysis using the new RTP values. It will return a 201 CREATED status and respond with a list of analysis summaries of all the analyses, including the newly started analysis. - if the RTP values used for it were the **same** as the RTP values sent with the current request, then the server will **NOT** start a new analysis. It will return a 200 OK status, and simply return the existing list of analysis summaries. This request requires the last analysis of the protocol to have been completed before handling this request. If the last analysis is pending, it will return a 503 error. # Test Plan Test out the above three cases and anything else you can think might affect the behavior. It's pretty well tested in unit & integration tests and it is also the same logic used for handling analyses from `POST /protocols`, so it is expected to work well when used as tested in integration tests. # Review requests Usual review for code sanity check. # Risk assessment Low. New HTTP API not yet used anywhere. --- .../robot_server/protocols/analysis_models.py | 14 +- robot-server/robot_server/protocols/router.py | 166 ++++++++++++++---- .../protocols/test_analyses.tavern.yaml | 19 ++ ...lyses_with_run_time_parameters.tavern.yaml | 23 ++- .../tests/protocols/test_protocols_router.py | 132 +++++++++++++- 5 files changed, 320 insertions(+), 34 deletions(-) diff --git a/robot-server/robot_server/protocols/analysis_models.py b/robot-server/robot_server/protocols/analysis_models.py index c5827e577da..c8b11f2db25 100644 --- a/robot-server/robot_server/protocols/analysis_models.py +++ b/robot-server/robot_server/protocols/analysis_models.py @@ -2,7 +2,7 @@ # TODO(mc, 2021-08-25): add modules to simulation result from enum import Enum -from opentrons.protocol_engine.types import RunTimeParameter +from opentrons.protocol_engine.types import RunTimeParameter, RunTimeParamValuesType from opentrons_shared_data.robot.dev_types import RobotType from pydantic import BaseModel, Field from typing import List, Optional, Union, NamedTuple @@ -40,6 +40,18 @@ class AnalysisResult(str, Enum): NOT_OK = "not-ok" +class AnalysisRequest(BaseModel): + """Model for analysis request body.""" + + runTimeParameterValues: RunTimeParamValuesType = Field( + default={}, + description="Key-value pairs of run-time parameters defined in a protocol.", + ) + forceReAnalyze: bool = Field( + False, description="Whether to force start a new analysis." + ) + + class AnalysisSummary(BaseModel): """Base model for an analysis of a protocol.""" diff --git a/robot-server/robot_server/protocols/router.py b/robot-server/robot_server/protocols/router.py index 8ae9365de36..d3375f535d4 100644 --- a/robot-server/robot_server/protocols/router.py +++ b/robot-server/robot_server/protocols/router.py @@ -4,8 +4,9 @@ from textwrap import dedent from datetime import datetime from pathlib import Path -from typing import List, Optional, Union +from typing import List, Optional, Union, Tuple +from opentrons.protocol_engine.types import RunTimeParamValuesType from opentrons_shared_data.robot import user_facing_robot_type from typing_extensions import Literal @@ -32,13 +33,14 @@ SimpleEmptyBody, MultiBodyMeta, PydanticResponse, + RequestModel, ) from .protocol_auto_deleter import ProtocolAutoDeleter from .protocol_models import Protocol, ProtocolFile, Metadata from .protocol_analyzer import ProtocolAnalyzer from .analysis_store import AnalysisStore, AnalysisNotFoundError, AnalysisIsPendingError -from .analysis_models import ProtocolAnalysis +from .analysis_models import ProtocolAnalysis, AnalysisRequest, AnalysisSummary from .protocol_store import ( ProtocolStore, ProtocolResource, @@ -162,7 +164,7 @@ class ProtocolLinks(BaseModel): status.HTTP_503_SERVICE_UNAVAILABLE: {"model": ErrorBody[LastAnalysisPending]}, }, ) -async def create_protocol( # noqa: C901 +async def create_protocol( files: List[UploadFile] = File(...), # use Form because request is multipart/form-data # https://fastapi.tiangolo.com/tutorial/request-forms-and-files/ @@ -238,35 +240,18 @@ async def create_protocol( # noqa: C901 if cached_protocol_id is not None: resource = protocol_store.get(protocol_id=cached_protocol_id) - analyses = analysis_store.get_summaries_by_protocol( - protocol_id=cached_protocol_id - ) try: - if ( - # Unexpected situations, like powering off the robot after a protocol upload - # but before the analysis is complete, can leave the protocol resource - # without an associated analysis. - len(analyses) == 0 - or - # The most recent analysis was done using different RTP values - not await analysis_store.matching_rtp_values_in_analysis( - analysis_summary=analyses[-1], new_rtp_values=parsed_rtp - ) - ): - # This protocol exists in database but needs to be (re)analyzed - task_runner.run( - protocol_analyzer.analyze, - protocol_resource=resource, - analysis_id=analysis_id, - run_time_param_values=parsed_rtp, - ) - analyses.append( - analysis_store.add_pending( - protocol_id=cached_protocol_id, - analysis_id=analysis_id, - ) - ) + analysis_summaries, _ = await _start_new_analysis_if_necessary( + protocol_id=cached_protocol_id, + analysis_id=analysis_id, + rtp_values=parsed_rtp, + force_reanalyze=False, + protocol_store=protocol_store, + analysis_store=analysis_store, + protocol_analyzer=protocol_analyzer, + task_runner=task_runner, + ) except AnalysisIsPendingError as error: raise LastAnalysisPending(detail=str(error)).as_error( status.HTTP_503_SERVICE_UNAVAILABLE @@ -278,7 +263,7 @@ async def create_protocol( # noqa: C901 protocolType=resource.source.config.protocol_type, robotType=resource.source.robot_type, metadata=Metadata.parse_obj(resource.source.metadata), - analysisSummaries=analyses, + analysisSummaries=analysis_summaries, key=resource.protocol_key, files=[ ProtocolFile(name=f.path.name, role=f.role) @@ -357,6 +342,53 @@ async def create_protocol( # noqa: C901 ) +async def _start_new_analysis_if_necessary( + protocol_id: str, + analysis_id: str, + force_reanalyze: bool, + rtp_values: RunTimeParamValuesType, + protocol_store: ProtocolStore, + analysis_store: AnalysisStore, + protocol_analyzer: ProtocolAnalyzer, + task_runner: TaskRunner, +) -> Tuple[List[AnalysisSummary], bool]: + """Check RTP values and start a new analysis if necessary. + + Returns a tuple of the latest list of analysis summaries (including any newly + started analysis) and whether a new analysis was started. + """ + resource = protocol_store.get(protocol_id=protocol_id) + analyses = analysis_store.get_summaries_by_protocol(protocol_id=protocol_id) + started_new_analysis = False + if ( + force_reanalyze + or + # Unexpected situations, like powering off the robot after a protocol upload + # but before the analysis is complete, can leave the protocol resource + # without an associated analysis. + len(analyses) == 0 + or + # The most recent analysis was done using different RTP values + not await analysis_store.matching_rtp_values_in_analysis( + analysis_summary=analyses[-1], new_rtp_values=rtp_values + ) + ): + task_runner.run( + protocol_analyzer.analyze, + protocol_resource=resource, + analysis_id=analysis_id, + run_time_param_values=rtp_values, + ) + started_new_analysis = True + analyses.append( + analysis_store.add_pending( + protocol_id=protocol_id, + analysis_id=analysis_id, + ) + ) + return analyses, started_new_analysis + + @PydanticResponse.wrap_route( protocols_router.get, path="/protocols", @@ -519,6 +551,78 @@ async def delete_protocol_by_id( ) +@PydanticResponse.wrap_route( + protocols_router.post, + path="/protocols/{protocolId}/analyses", + summary="Analyze the protocol", + description=dedent( + """ + Generate an analysis for the protocol, based on last analysis and current request data. + """ + ), + status_code=status.HTTP_201_CREATED, + responses={ + status.HTTP_200_OK: {"model": SimpleMultiBody[AnalysisSummary]}, + status.HTTP_201_CREATED: {"model": SimpleMultiBody[AnalysisSummary]}, + status.HTTP_404_NOT_FOUND: {"model": ErrorBody[ProtocolNotFound]}, + status.HTTP_503_SERVICE_UNAVAILABLE: {"model": ErrorBody[LastAnalysisPending]}, + }, +) +async def create_protocol_analysis( + protocolId: str, + request_body: Optional[RequestModel[AnalysisRequest]] = None, + protocol_store: ProtocolStore = Depends(get_protocol_store), + analysis_store: AnalysisStore = Depends(get_analysis_store), + protocol_analyzer: ProtocolAnalyzer = Depends(get_protocol_analyzer), + task_runner: TaskRunner = Depends(get_task_runner), + analysis_id: str = Depends(get_unique_id, use_cache=False), +) -> PydanticResponse[SimpleMultiBody[AnalysisSummary]]: + """Start a new analysis for the given existing protocol. + + Starts a new analysis for the protocol along with the provided run-time parameter + values (if any), and appends it to the existing analyses. + + If the last analysis in the existing analyses used the same RTP values, then a new + analysis is not created. + + If `forceAnalyze` is True, this will always start a new analysis. + + Returns: List of analysis summaries available for the protocol, ordered as + most recently started analysis last. + """ + if not protocol_store.has(protocolId): + raise ProtocolNotFound(detail=f"Protocol {protocolId} not found").as_error( + status.HTTP_404_NOT_FOUND + ) + try: + ( + analysis_summaries, + started_new_analysis, + ) = await _start_new_analysis_if_necessary( + protocol_id=protocolId, + analysis_id=analysis_id, + rtp_values=request_body.data.runTimeParameterValues if request_body else {}, + force_reanalyze=request_body.data.forceReAnalyze if request_body else False, + protocol_store=protocol_store, + analysis_store=analysis_store, + protocol_analyzer=protocol_analyzer, + task_runner=task_runner, + ) + except AnalysisIsPendingError as error: + raise LastAnalysisPending(detail=str(error)).as_error( + status.HTTP_503_SERVICE_UNAVAILABLE + ) from error + return await PydanticResponse.create( + content=SimpleMultiBody.construct( + data=analysis_summaries, + meta=MultiBodyMeta(cursor=0, totalLength=len(analysis_summaries)), + ), + status_code=status.HTTP_201_CREATED + if started_new_analysis + else status.HTTP_200_OK, + ) + + @PydanticResponse.wrap_route( protocols_router.get, path="/protocols/{protocolId}/analyses", diff --git a/robot-server/tests/integration/http_api/protocols/test_analyses.tavern.yaml b/robot-server/tests/integration/http_api/protocols/test_analyses.tavern.yaml index a756ea10e1b..0451b3eebc4 100644 --- a/robot-server/tests/integration/http_api/protocols/test_analyses.tavern.yaml +++ b/robot-server/tests/integration/http_api/protocols/test_analyses.tavern.yaml @@ -84,3 +84,22 @@ stages: # We need to make sure we get the Content-Type right because FastAPI won't do it for us. Content-Type: application/json json: !force_format_include '{analysis_data}' + + + - name: Check that a new analysis is started with forceReAnalyze + request: + url: '{ot2_server_base_url}/protocols/{protocol_id}/analyses' + method: POST + json: + data: + forceReAnalyze: true + response: + strict: + - json:off + status_code: 201 + json: + data: + - id: '{analysis_id}' + status: completed + - id: !anystr + status: pending diff --git a/robot-server/tests/integration/http_api/protocols/test_analyses_with_run_time_parameters.tavern.yaml b/robot-server/tests/integration/http_api/protocols/test_analyses_with_run_time_parameters.tavern.yaml index 3ad017a546d..fa37eadc20c 100644 --- a/robot-server/tests/integration/http_api/protocols/test_analyses_with_run_time_parameters.tavern.yaml +++ b/robot-server/tests/integration/http_api/protocols/test_analyses_with_run_time_parameters.tavern.yaml @@ -177,4 +177,25 @@ stages: description: What pipette to use during the protocol. commands: # Check for this command's presence as a smoke test that the analysis isn't empty. - - commandType: loadPipette \ No newline at end of file + - commandType: loadPipette + + - name: Check that a new analysis is started for the protocol because of new RTP values + request: + url: '{ot2_server_base_url}/protocols/{protocol_id}/analyses' + method: POST + json: + data: + runTimeParameterValues: + sample_count: 2 + response: + strict: + - json:off + status_code: 201 + json: + data: + - id: '{analysis_id}' + status: completed + - id: '{analysis_id2}' + status: completed + - id: !anystr + status: pending \ No newline at end of file diff --git a/robot-server/tests/protocols/test_protocols_router.py b/robot-server/tests/protocols/test_protocols_router.py index ffb02d929b1..88605f81a3b 100644 --- a/robot-server/tests/protocols/test_protocols_router.py +++ b/robot-server/tests/protocols/test_protocols_router.py @@ -7,6 +7,7 @@ from fastapi import UploadFile from pathlib import Path +from opentrons.protocol_engine.types import RunTimeParamValuesType from opentrons.protocols.api_support.types import APIVersion from opentrons.protocol_reader import ( @@ -23,7 +24,7 @@ ) from robot_server.errors.error_responses import ApiError -from robot_server.service.json_api import SimpleEmptyBody, MultiBodyMeta +from robot_server.service.json_api import SimpleEmptyBody, MultiBodyMeta, RequestModel from robot_server.service.task_runner import TaskRunner from robot_server.protocols.analysis_store import ( AnalysisStore, @@ -38,6 +39,7 @@ CompletedAnalysis, PendingAnalysis, AnalysisResult, + AnalysisRequest, ) from robot_server.protocols.protocol_models import ( @@ -56,6 +58,7 @@ from robot_server.protocols.router import ( ProtocolLinks, create_protocol, + create_protocol_analysis, get_protocols, get_protocol_ids, get_protocol_by_id, @@ -1393,3 +1396,130 @@ async def test_get_protocol_analysis_as_document_analysis_not_found( assert exc_info.value.status_code == 404 assert exc_info.value.content["errors"][0]["id"] == "AnalysisNotFound" + + +async def test_create_protocol_analyses_with_same_rtp_values( + decoy: Decoy, + protocol_store: ProtocolStore, + analysis_store: AnalysisStore, + protocol_analyzer: ProtocolAnalyzer, + task_runner: TaskRunner, +) -> None: + """It should not start a new analysis for the new rtp values.""" + rtp_values: RunTimeParamValuesType = {"vol": 123, "dry_run": True, "mount": "left"} + analysis_summaries = [ + AnalysisSummary( + id="analysis-id", + status=AnalysisStatus.COMPLETED, + ), + ] + decoy.when(protocol_store.has(protocol_id="protocol-id")).then_return(True) + decoy.when( + analysis_store.get_summaries_by_protocol(protocol_id="protocol-id") + ).then_return(analysis_summaries) + decoy.when( + await analysis_store.matching_rtp_values_in_analysis( + analysis_summaries[-1], rtp_values + ) + ).then_return(True) + + result = await create_protocol_analysis( + protocolId="protocol-id", + request_body=RequestModel( + data=AnalysisRequest(runTimeParameterValues=rtp_values) + ), + protocol_store=protocol_store, + analysis_store=analysis_store, + protocol_analyzer=protocol_analyzer, + task_runner=task_runner, + analysis_id="analysis-id-2", + ) + assert result.content.data == analysis_summaries + assert result.status_code == 200 + + +async def test_update_protocol_analyses_with_new_rtp_values( + decoy: Decoy, + protocol_store: ProtocolStore, + analysis_store: AnalysisStore, + protocol_analyzer: ProtocolAnalyzer, + task_runner: TaskRunner, +) -> None: + """It should start a new analysis for the new rtp values.""" + rtp_values: RunTimeParamValuesType = {"vol": 123, "dry_run": True, "mount": "left"} + analysis_summaries = [ + AnalysisSummary( + id="analysis-id", + status=AnalysisStatus.COMPLETED, + ), + ] + decoy.when(protocol_store.has(protocol_id="protocol-id")).then_return(True) + decoy.when( + analysis_store.get_summaries_by_protocol(protocol_id="protocol-id") + ).then_return(analysis_summaries) + decoy.when( + await analysis_store.matching_rtp_values_in_analysis( + analysis_summaries[-1], rtp_values + ) + ).then_return(False) + decoy.when(analysis_store.add_pending("protocol-id", "analysis-id-2")).then_return( + AnalysisSummary(id="analysis-id-2", status=AnalysisStatus.PENDING) + ) + result = await create_protocol_analysis( + protocolId="protocol-id", + request_body=RequestModel( + data=AnalysisRequest(runTimeParameterValues=rtp_values) + ), + protocol_store=protocol_store, + analysis_store=analysis_store, + protocol_analyzer=protocol_analyzer, + task_runner=task_runner, + analysis_id="analysis-id-2", + ) + assert result.content.data == [ + AnalysisSummary(id="analysis-id", status=AnalysisStatus.COMPLETED), + AnalysisSummary(id="analysis-id-2", status=AnalysisStatus.PENDING), + ] + assert result.status_code == 201 + + +async def test_update_protocol_analyses_with_forced_reanalysis( + decoy: Decoy, + protocol_store: ProtocolStore, + analysis_store: AnalysisStore, + protocol_analyzer: ProtocolAnalyzer, + task_runner: TaskRunner, +) -> None: + """It should start a new analysis for the protocol, regardless of rtp values.""" + analysis_summaries = [ + AnalysisSummary( + id="analysis-id", + status=AnalysisStatus.COMPLETED, + ), + ] + decoy.when(protocol_store.has(protocol_id="protocol-id")).then_return(True) + decoy.when( + analysis_store.get_summaries_by_protocol(protocol_id="protocol-id") + ).then_return(analysis_summaries) + decoy.when( + await analysis_store.matching_rtp_values_in_analysis( + analysis_summary=analysis_summaries[-1], new_rtp_values={} + ) + ).then_return(True) + decoy.when(analysis_store.add_pending("protocol-id", "analysis-id-2")).then_return( + AnalysisSummary(id="analysis-id-2", status=AnalysisStatus.PENDING) + ) + result = await create_protocol_analysis( + protocolId="protocol-id", + request_body=RequestModel(data=AnalysisRequest(forceReAnalyze=True)), + protocol_store=protocol_store, + analysis_store=analysis_store, + protocol_analyzer=protocol_analyzer, + task_runner=task_runner, + analysis_id="analysis-id-2", + ) + assert result.content.data == [ + AnalysisSummary(id="analysis-id", status=AnalysisStatus.COMPLETED), + AnalysisSummary(id="analysis-id-2", status=AnalysisStatus.PENDING), + ] + assert result.status_code == 201 From 3643bc7f669d05b179edad1676f9788c9c2001c7 Mon Sep 17 00:00:00 2001 From: Jethary Rader <66035149+jerader@users.noreply.github.com> Date: Tue, 9 Apr 2024 09:19:40 -0400 Subject: [PATCH 074/194] feat(protocol-designer): temperature form multiple module support (#14835) closes AUTH-2 --- .../LabwareOverlays/LabwareHighlight.tsx | 15 +- .../src/components/Hints/index.tsx | 2 + .../StepEditForm/forms/TemperatureForm.tsx | 100 +++-- .../forms/__tests__/TemperatureForm.test.tsx | 95 +++++ .../components/steplist/ModuleStepItems.tsx | 57 ++- .../src/components/steplist/StepItem.tsx | 8 +- .../src/containers/ConnectedStepItem.tsx | 18 +- .../__tests__/ConnectedStepItem.test.tsx | 378 +++++++++++++++++- .../src/localization/en/application.json | 1 + .../src/steplist/generateSubstepItem.ts | 1 + .../steplist/test/generateSubsteps.test.ts | 3 + protocol-designer/src/steplist/types.ts | 7 +- protocol-designer/src/tutorial/index.ts | 1 + protocol-designer/src/ui/modules/selectors.ts | 37 +- protocol-designer/src/ui/modules/utils.ts | 85 +++- .../addAndSelectStepWithHints.test.ts | 81 +++- .../src/ui/steps/actions/thunks/index.ts | 15 +- protocol-designer/src/ui/steps/selectors.ts | 12 +- 18 files changed, 768 insertions(+), 148 deletions(-) create mode 100644 protocol-designer/src/components/StepEditForm/forms/__tests__/TemperatureForm.test.tsx diff --git a/protocol-designer/src/components/DeckSetup/LabwareOverlays/LabwareHighlight.tsx b/protocol-designer/src/components/DeckSetup/LabwareOverlays/LabwareHighlight.tsx index e0a8500c4c8..320d1074977 100644 --- a/protocol-designer/src/components/DeckSetup/LabwareOverlays/LabwareHighlight.tsx +++ b/protocol-designer/src/components/DeckSetup/LabwareOverlays/LabwareHighlight.tsx @@ -3,11 +3,14 @@ import cx from 'classnames' import { useSelector } from 'react-redux' import { Icon } from '@opentrons/components' import { getHoveredStepLabware, getHoveredStepId } from '../../../ui/steps' -import { getSavedStepForms } from '../../../step-forms/selectors' +import { + getLabwareEntities, + getSavedStepForms, +} from '../../../step-forms/selectors' import { THERMOCYCLER_PROFILE } from '../../../constants' import styles from './LabwareOverlays.module.css' -import { LabwareOnDeck } from '../../../step-forms' +import type { LabwareOnDeck } from '../../../step-forms' interface LabwareHighlightProps { labwareOnDeck: LabwareOnDeck @@ -17,8 +20,14 @@ export const LabwareHighlight = ( props: LabwareHighlightProps ): JSX.Element | null => { const { labwareOnDeck } = props + const labwareEntities = useSelector(getLabwareEntities) + const adapterId = + labwareEntities[labwareOnDeck.slot] != null + ? labwareEntities[labwareOnDeck.slot].id + : null + const highlighted = useSelector(getHoveredStepLabware).includes( - labwareOnDeck.id + adapterId ?? labwareOnDeck.id ) let isTcProfile = false diff --git a/protocol-designer/src/components/Hints/index.tsx b/protocol-designer/src/components/Hints/index.tsx index af77a54193b..6f5bafd2527 100644 --- a/protocol-designer/src/components/Hints/index.tsx +++ b/protocol-designer/src/components/Hints/index.tsx @@ -74,12 +74,14 @@ export const Hints = (): JSX.Element | null => {

{t(`hint.${hintKey}.body3`)}

) + case 'multiple_modules_without_labware': case 'module_without_labware': return ( <>

{t(`alert:hint.${hintKey}.body`)}

) + case 'thermocycler_lid_passive_cooling': return ( <> diff --git a/protocol-designer/src/components/StepEditForm/forms/TemperatureForm.tsx b/protocol-designer/src/components/StepEditForm/forms/TemperatureForm.tsx index c14b358dc0c..bcd35a1636f 100644 --- a/protocol-designer/src/components/StepEditForm/forms/TemperatureForm.tsx +++ b/protocol-designer/src/components/StepEditForm/forms/TemperatureForm.tsx @@ -4,16 +4,17 @@ import { useTranslation } from 'react-i18next' import { FormGroup } from '@opentrons/components' import { selectors as uiModuleSelectors } from '../../../ui/modules' import { StepFormDropdown, RadioGroupField, TextField } from '../fields' -import styles from '../StepEditForm.module.css' import type { StepFormProps } from '../types' -export const TemperatureForm = (props: StepFormProps): JSX.Element => { +import styles from '../StepEditForm.module.css' + +export function TemperatureForm(props: StepFormProps): JSX.Element { const { t } = useTranslation(['application', 'form']) const moduleLabwareOptions = useSelector( uiModuleSelectors.getTemperatureLabwareOptions ) - const temperatureModuleId = useSelector( - uiModuleSelectors.getSingleTemperatureModuleId + const temperatureModuleIds = useSelector( + uiModuleSelectors.getTemperatureModuleIds ) const { propsForFields } = props @@ -36,56 +37,47 @@ export const TemperatureForm = (props: StepFormProps): JSX.Element => { options={moduleLabwareOptions} /> - {/* TODO (ka 2020-1-6): - moduleID dropdown will autoselect when creating a new step, - but this will not be the case when returning to a never saved form. - Rather than defaulting to one or the other when null, - display a message (copy, design, etc TBD) that you need to select a module to continue - */} - - {moduleId === null && ( -

- Please ensure a compatible module is present on the deck and - selected to create a temperature step. -

- )} - {moduleId === temperatureModuleId && temperatureModuleId != null && ( - <> -
- - {setTemperature === 'true' && ( - - )} -
-
- -
- - )} + {temperatureModuleIds != null + ? temperatureModuleIds.map(id => + id === moduleId ? ( + +
+ + {setTemperature === 'true' && ( + + )} +
+
+ +
+
+ ) : null + ) + : null}
) diff --git a/protocol-designer/src/components/StepEditForm/forms/__tests__/TemperatureForm.test.tsx b/protocol-designer/src/components/StepEditForm/forms/__tests__/TemperatureForm.test.tsx new file mode 100644 index 00000000000..a32894d3b84 --- /dev/null +++ b/protocol-designer/src/components/StepEditForm/forms/__tests__/TemperatureForm.test.tsx @@ -0,0 +1,95 @@ +import * as React from 'react' +import { describe, it, vi, beforeEach } from 'vitest' +import { fireEvent, screen } from '@testing-library/react' +import { renderWithProviders } from '../../../../__testing-utils__' +import { i18n } from '../../../../localization' +import { + getTemperatureLabwareOptions, + getTemperatureModuleIds, +} from '../../../../ui/modules/selectors' +import { TemperatureForm } from '../TemperatureForm' + +vi.mock('../../../../ui/modules/selectors', async importOriginal => { + const actualFields = await importOriginal< + typeof import('../../../../ui/modules/selectors') + >() + return { + ...actualFields, + getTemperatureLabwareOptions: vi.fn(), + getTemperatureModuleIds: vi.fn(), + } +}) +const render = (props: React.ComponentProps) => { + return renderWithProviders(, { + i18nInstance: i18n, + })[0] +} + +describe('TemperatureForm', () => { + let props: React.ComponentProps + + beforeEach(() => { + props = { + formData: { + id: 'formId', + stepType: 'temperature', + moduleId: 'mockId', + setTemperature: true, + } as any, + focusHandlers: { + blur: vi.fn(), + focus: vi.fn(), + dirtyFields: [], + focusedField: null, + }, + propsForFields: { + moduleId: { + onFieldFocus: vi.fn(), + onFieldBlur: vi.fn(), + errorToShow: null, + disabled: false, + name: 'setTemperature', + updateValue: vi.fn(), + value: 'mockId', + }, + setTemperature: { + onFieldFocus: vi.fn(), + onFieldBlur: vi.fn(), + errorToShow: null, + disabled: false, + name: 'setTemperature', + updateValue: vi.fn(), + value: true, + }, + targetTemperature: { + onFieldFocus: vi.fn(), + onFieldBlur: vi.fn(), + errorToShow: null, + disabled: false, + name: 'targetTemperature', + updateValue: vi.fn(), + value: null, + }, + }, + } + + vi.mocked(getTemperatureModuleIds).mockReturnValue(['mockId']) + vi.mocked(getTemperatureLabwareOptions).mockReturnValue([ + { + name: 'mock module', + value: 'mockId', + }, + ]) + }) + + it('renders a temperature module', () => { + render(props) + screen.getByText('temperature') + screen.getByText('module') + const change = screen.getByText('Change to temperature') + screen.getByText('Deactivate module') + fireEvent.click(change) + const changeTempInput = screen.getByRole('combobox', { name: '' }) + fireEvent.change(changeTempInput, { target: { value: 40 } }) + }) +}) diff --git a/protocol-designer/src/components/steplist/ModuleStepItems.tsx b/protocol-designer/src/components/steplist/ModuleStepItems.tsx index f3e91c1b73d..548caf2964d 100644 --- a/protocol-designer/src/components/steplist/ModuleStepItems.tsx +++ b/protocol-designer/src/components/steplist/ModuleStepItems.tsx @@ -9,8 +9,9 @@ import { } from '@opentrons/components' import { PDListItem } from '../lists' import { LabwareTooltipContents } from './LabwareTooltipContents' +import type { ModuleType } from '@opentrons/shared-data' + import styles from './StepItem.module.css' -import { ModuleType } from '@opentrons/shared-data' export interface ModuleStepItemRowProps { label?: string | null @@ -31,44 +32,64 @@ export const ModuleStepItemRow = ( ) -interface Props { - action?: string +interface ModuleStepItemsProps { moduleType: ModuleType actionText: string - labwareNickname?: string | null - message?: string | null + moduleSlot?: string + action?: string children?: React.ReactNode hideHeader?: boolean + labwareNickname?: string | null + message?: string | null } -export const ModuleStepItems = (props: Props): JSX.Element => { - const { t } = useTranslation('modules') +export function ModuleStepItems(props: ModuleStepItemsProps): JSX.Element { + const { + moduleType, + actionText, + moduleSlot, + action, + hideHeader, + labwareNickname, + children, + message, + } = props + const { t } = useTranslation(['modules', 'application']) const [targetProps, tooltipProps] = useHoverTooltip({ placement: 'bottom-start', strategy: TOOLTIP_FIXED, }) + const moduleLongName = t(`module_long_names.${moduleType}`) + return ( <> - {!props.hideHeader && ( + {!Boolean(hideHeader) ? (
  • - {t(`module_long_names.${props.moduleType}`)} - {props.action} + + {moduleSlot != null + ? t('application:module_and_slot', { + moduleLongName, + slotName: moduleSlot, + }) + : moduleLongName} + + {action}
  • - )} + ) : null} - + - {props.children} - {props.message && ( + {children} + {message != null ? ( - "{props.message}" + "{message}" - )} + ) : null} ) } diff --git a/protocol-designer/src/components/steplist/StepItem.tsx b/protocol-designer/src/components/steplist/StepItem.tsx index c51502348a2..0fbb338cc0f 100644 --- a/protocol-designer/src/components/steplist/StepItem.tsx +++ b/protocol-designer/src/components/steplist/StepItem.tsx @@ -25,6 +25,7 @@ import { makeTemperatureText, makeTimerText, } from '../../utils' +import { InitialDeckSetup } from '../../step-forms' import { PDListItem, TitledStepList } from '../lists' import { TitledListNotes } from '../TitledListNotes' import { AspirateDispenseHeader } from './AspirateDispenseHeader' @@ -121,11 +122,10 @@ export interface StepItemContentsProps { rawForm: FormData | null | undefined stepType: StepType substeps: SubstepItemData | null | undefined - ingredNames: WellIngredientNames labwareNicknamesById: { [labwareId: string]: string } additionalEquipmentEntities: AdditionalEquipmentEntities - + modules: InitialDeckSetup['modules'] highlightSubstep: (substepIdentifier: SubstepIdentifier) => unknown hoveredSubstep: SubstepIdentifier | null | undefined } @@ -293,6 +293,7 @@ export const StepItemContents = ( props: StepItemContentsProps ): JSX.Element | JSX.Element[] | null => { const { + modules, rawForm, stepType, substeps, @@ -326,6 +327,8 @@ export const StepItemContents = ( if (substeps && substeps.substepType === 'temperature') { const temperature = makeTemperatureText(substeps.temperature, t) + const moduleSlot = + substeps.moduleId != null ? modules[substeps.moduleId].slot : '' return ( ) } diff --git a/protocol-designer/src/containers/ConnectedStepItem.tsx b/protocol-designer/src/containers/ConnectedStepItem.tsx index a6b4ceb1f26..a3ebcb05f41 100644 --- a/protocol-designer/src/containers/ConnectedStepItem.tsx +++ b/protocol-designer/src/containers/ConnectedStepItem.tsx @@ -24,7 +24,6 @@ import { SelectMultipleStepsAction, } from '../ui/steps' import { selectors as fileDataSelectors } from '../file-data' - import { StepItem, StepItemContents, @@ -38,12 +37,15 @@ import { ConfirmDeleteModal, DeleteModalType, } from '../components/modals/ConfirmDeleteModal' +import { + getAdditionalEquipmentEntities, + getInitialDeckSetup, +} from '../step-forms/selectors' -import { SubstepIdentifier } from '../steplist/types' -import { StepIdType } from '../form-types' -import { BaseState, ThunkAction } from '../types' -import { getAdditionalEquipmentEntities } from '../step-forms/selectors' -import { ThunkDispatch } from 'redux-thunk' +import type { ThunkDispatch } from 'redux-thunk' +import type { SubstepIdentifier } from '../steplist/types' +import type { StepIdType } from '../form-types' +import type { BaseState, ThunkAction } from '../types' export interface ConnectedStepItemProps { stepId: StepIdType @@ -86,7 +88,7 @@ export const ConnectedStepItem = ( const hasWarnings = hasTimelineWarningsPerStep[stepId] || hasFormLevelWarningsPerStep[stepId] - + const initialDeckSetup = useSelector(getInitialDeckSetup) const collapsed = useSelector(getCollapsedSteps)[stepId] const hoveredSubstep = useSelector(getHoveredSubstep) const hoveredStep = useSelector(getHoveredStepId) @@ -217,6 +219,7 @@ export const ConnectedStepItem = ( } const stepItemContentsProps: StepItemContentsProps = { + modules: initialDeckSetup.modules, rawForm: step, stepType: step.stepType, substeps, @@ -236,7 +239,6 @@ export const ConnectedStepItem = ( return CLOSE_STEP_FORM_WITH_CHANGES } } - return ( <> {showConfirmation && ( diff --git a/protocol-designer/src/containers/__tests__/ConnectedStepItem.test.tsx b/protocol-designer/src/containers/__tests__/ConnectedStepItem.test.tsx index 4d03b5c16ac..cce62e03887 100644 --- a/protocol-designer/src/containers/__tests__/ConnectedStepItem.test.tsx +++ b/protocol-designer/src/containers/__tests__/ConnectedStepItem.test.tsx @@ -1,5 +1,379 @@ -import { describe, it } from 'vitest' +import * as React from 'react' +import { describe, it, beforeEach, vi } from 'vitest' +import { screen } from '@testing-library/react' +import { fixture96Plate } from '@opentrons/shared-data' +import { renderWithProviders } from '../../__testing-utils__' +import { i18n } from '../../localization' +import { + getAdditionalEquipmentEntities, + getArgsAndErrorsByStepId, + getBatchEditFormHasUnsavedChanges, + getCurrentFormCanBeSaved, + getCurrentFormHasUnsavedChanges, + getInitialDeckSetup, + getOrderedStepIds, + getSavedStepForms, +} from '../../step-forms/selectors' +import { selectors as labwareIngredSelectors } from '../../labware-ingred/selectors' +import { getErrorStepId, getSubsteps } from '../../file-data/selectors' +import { getHasTimelineWarningsPerStep } from '../../top-selectors/timelineWarnings' +import { getHasFormLevelWarningsPerStep } from '../../dismiss/selectors' +import { + getCollapsedSteps, + getHoveredSubstep, + getIsMultiSelectMode, + getMultiSelectItemIds, + getMultiSelectLastSelected, + getSelectedStepId, +} from '../../ui/steps' +import { getLabwareNicknamesById } from '../../ui/labware/selectors' +import { ConnectedStepItem } from '../ConnectedStepItem' +import type { LabwareDefinition2 } from '@opentrons/shared-data' +vi.mock('../../step-forms/selectors') +vi.mock('../../file-data/selectors') +vi.mock('../../top-selectors/timelineWarnings') +vi.mock('../../dismiss/selectors') +vi.mock('../../ui/steps') +vi.mock('../../labware-ingred/selectors') +vi.mock('../../ui/labware/selectors') + +const render = (props: React.ComponentProps) => { + return renderWithProviders(, { + i18nInstance: i18n, + }) +} +const pauseStepId = 'pauseId' +const magnetStepId = 'magnetStepId' +const heaterShakerStepId = 'hsStepId' +const thermocyclerStepId = 'tcStepId' +const temperatureStepId = 'tempStepId' +const moveLabwareStepId = 'moveLabwareId' + +// TODO(jr, 4/8/24): add test coverage for mix and moveLiquid!!! describe('ConnectedStepItem', () => { - it.todo('replace deprecated enzyme test') + let props: React.ComponentProps + beforeEach(() => { + props = { + stepId: pauseStepId, + stepNumber: 2, + onStepContextMenu: vi.fn(), + } + vi.mocked(getSavedStepForms).mockReturnValue({ + [pauseStepId]: { + stepType: 'pause', + id: pauseStepId, + pauseHour: '1', + pauseMinute: '10', + pauseSecond: '5', + pauseMessage: 'mock message', + pauseTemperature: '10', + }, + [magnetStepId]: { + stepType: 'magnet', + id: magnetStepId, + }, + [heaterShakerStepId]: { + stepType: 'heaterShaker', + id: heaterShakerStepId, + }, + [thermocyclerStepId]: { + stepType: 'thermocycler', + id: thermocyclerStepId, + }, + [temperatureStepId]: { + stepType: 'temperature', + id: temperatureStepId, + }, + [moveLabwareStepId]: { + stepType: 'moveLabware', + id: moveLabwareStepId, + }, + }) + vi.mocked(getArgsAndErrorsByStepId).mockReturnValue({ + [pauseStepId]: { + errors: false, + stepArgs: null, + }, + [magnetStepId]: { + errors: false, + stepArgs: null, + }, + [heaterShakerStepId]: { + errors: false, + stepArgs: null, + }, + [thermocyclerStepId]: { + errors: false, + stepArgs: null, + }, + [temperatureStepId]: { + errors: false, + stepArgs: null, + }, + [moveLabwareStepId]: { + errors: false, + stepArgs: null, + }, + }) + vi.mocked(getErrorStepId).mockReturnValue(null) + vi.mocked(getHasTimelineWarningsPerStep).mockReturnValue({ + [pauseStepId]: false, + [magnetStepId]: false, + [heaterShakerStepId]: false, + [thermocyclerStepId]: false, + [temperatureStepId]: false, + [moveLabwareStepId]: false, + }) + vi.mocked(getHasFormLevelWarningsPerStep).mockReturnValue({ + [pauseStepId]: false, + [magnetStepId]: false, + [heaterShakerStepId]: false, + [thermocyclerStepId]: false, + [temperatureStepId]: false, + [moveLabwareStepId]: false, + }) + vi.mocked(getInitialDeckSetup).mockReturnValue({ + pipettes: {}, + modules: { + thermocyclerId: { + id: 'thermocyclerId', + type: 'thermocyclerModuleType', + model: 'thermocyclerModuleV2', + slot: 'B1', + moduleState: {} as any, + }, + temperatureId: { + id: 'temperatureId', + type: 'temperatureModuleType', + model: 'temperatureModuleV2', + slot: 'C3', + moduleState: {} as any, + }, + heaterShakerId: { + id: 'heaterShakerId', + type: 'heaterShakerModuleType', + model: 'heaterShakerModuleV1', + slot: 'D1', + moduleState: {} as any, + }, + magnetId: { + id: 'magnetId', + type: 'magneticModuleType', + model: 'magneticModuleV2', + slot: 'C1', + moduleState: {} as any, + }, + }, + additionalEquipmentOnDeck: { + stagingAreaId: { + name: 'stagingArea', + location: 'B3', + id: 'stagingAreaId', + }, + }, + labware: { + labwareId: { + id: 'labwareId', + labwareDefURI: `opentrons/fixture_96_plate/1`, + slot: 'A2', + def: fixture96Plate as LabwareDefinition2, + }, + }, + }) + vi.mocked(getCollapsedSteps).mockReturnValue({ + [pauseStepId]: false, + [magnetStepId]: true, + [heaterShakerStepId]: true, + [thermocyclerStepId]: true, + [temperatureStepId]: true, + [moveLabwareStepId]: true, + }) + vi.mocked(getHoveredSubstep).mockReturnValue(null) + vi.mocked(getSelectedStepId).mockReturnValue(pauseStepId) + vi.mocked(getOrderedStepIds).mockReturnValue([ + pauseStepId, + magnetStepId, + heaterShakerStepId, + thermocyclerStepId, + moveLabwareStepId, + temperatureStepId, + ]) + vi.mocked(getMultiSelectItemIds).mockReturnValue(null) + vi.mocked(getMultiSelectLastSelected).mockReturnValue(null) + vi.mocked(getIsMultiSelectMode).mockReturnValue(false) + vi.mocked(getSubsteps).mockReturnValue({ + [pauseStepId]: { + substepType: 'pause', + pauseStepArgs: { + commandCreatorFnName: 'delay', + wait: 10, + name: 'pause', + description: '', + meta: { hours: 1, minutes: 10, seconds: 15 }, + }, + }, + [magnetStepId]: { + substepType: 'magnet', + engage: true, + labwareNickname: 'mockLabware', + message: 'engaging height', + }, + [heaterShakerStepId]: { + substepType: 'heaterShaker', + labwareNickname: 'mockLabware', + targetHeaterShakerTemperature: 20, + targetSpeed: 200, + latchOpen: false, + heaterShakerTimerMinutes: 5, + heaterShakerTimerSeconds: 11, + }, + [thermocyclerStepId]: { + substepType: 'thermocyclerProfile', + blockTargetTempHold: 30, + labwareNickname: 'mockLabware', + lidOpenHold: false, + lidTargetTempHold: 32, + meta: { rawProfileItems: [] }, + profileSteps: [ + { holdTime: 7, temperature: 87 }, + { holdTime: 2, temperature: 55 }, + ], + profileTargetLidTemp: 40, + profileVolume: 21, + }, + [temperatureStepId]: { + substepType: 'temperature', + temperature: 18, + labwareNickname: 'mockLabware', + moduleId: 'temperatureId', + message: 'mock message', + }, + [moveLabwareStepId]: { + substepType: 'moveLabware', + moveLabwareArgs: { + commandCreatorFnName: 'moveLabware', + name: 'move labware', + description: '', + labware: 'labwareId', + useGripper: false, + newLocation: { slotName: 'B2' }, + }, + }, + }) + vi.mocked(labwareIngredSelectors.getLiquidNamesById).mockReturnValue({}) + vi.mocked(getLabwareNicknamesById).mockReturnValue({}) + vi.mocked(getAdditionalEquipmentEntities).mockReturnValue({ + stagingAreaId: { name: 'stagingArea', location: 'B3', id: 'stagingArea' }, + }) + vi.mocked(getCurrentFormCanBeSaved).mockReturnValue(true) + vi.mocked(getCurrentFormHasUnsavedChanges).mockReturnValue(false) + vi.mocked(getBatchEditFormHasUnsavedChanges).mockReturnValue(false) + }) + it('renders an expanded step item for pause', () => { + render(props) + screen.getByText('2. pause') + screen.getByText('Pause for Time') + screen.getByText('1 h') + screen.getByText('10 m') + screen.getByText('15 s') + }) + it('renders an expanded step item for magnet', () => { + vi.mocked(getCollapsedSteps).mockReturnValue({ + [pauseStepId]: true, + [magnetStepId]: false, + [heaterShakerStepId]: false, + [thermocyclerStepId]: true, + [temperatureStepId]: true, + [moveLabwareStepId]: true, + }) + vi.mocked(getSelectedStepId).mockReturnValue(magnetStepId) + props.stepId = magnetStepId + render(props) + screen.getByText('2. magnet') + screen.getByText('Magnetic module') + screen.getByText('mockLabware') + screen.getByText('engage') + }) + it('renders an expanded step item for heater-shaker', () => { + vi.mocked(getCollapsedSteps).mockReturnValue({ + [pauseStepId]: true, + [magnetStepId]: true, + [heaterShakerStepId]: false, + [thermocyclerStepId]: true, + [temperatureStepId]: true, + [moveLabwareStepId]: true, + }) + vi.mocked(getSelectedStepId).mockReturnValue(heaterShakerStepId) + props.stepId = heaterShakerStepId + render(props) + screen.getByText('2. heater-shaker') + screen.getByText('Heater-Shaker module') + screen.getByText('go to') + screen.getByText('mockLabware') + screen.getByText('20 °C') + screen.getByText('Labware Latch') + screen.getByText('Closed and Locked') + screen.getByText('Shaker') + screen.getByText('200 rpm') + screen.getByText('Deactivate after') + }) + it('renders an expanded step item for thermocycler', () => { + vi.mocked(getCollapsedSteps).mockReturnValue({ + [pauseStepId]: true, + [magnetStepId]: true, + [heaterShakerStepId]: false, + [thermocyclerStepId]: false, + [temperatureStepId]: true, + [moveLabwareStepId]: true, + }) + vi.mocked(getSelectedStepId).mockReturnValue(thermocyclerStepId) + props.stepId = thermocyclerStepId + render(props) + screen.getByText('2. thermocycler') + screen.getByText('Thermocycler module') + screen.getByText('profile') + screen.getByText('mockLabware') + screen.getByText('cycling') + screen.getByText('Lid (closed)') + screen.getByText('40 °C') + screen.getByText('Profile steps (0+ min)') + screen.getByText('Ending hold') + }) + it('renders an expanded step item for a temperature module', () => { + vi.mocked(getCollapsedSteps).mockReturnValue({ + [pauseStepId]: true, + [magnetStepId]: true, + [heaterShakerStepId]: true, + [thermocyclerStepId]: true, + [temperatureStepId]: false, + [moveLabwareStepId]: true, + }) + vi.mocked(getSelectedStepId).mockReturnValue(temperatureStepId) + props.stepId = temperatureStepId + render(props) + screen.getByText('2. temperature') + screen.getByText('Temperature module in Slot C3') + screen.getByText('go to') + screen.getByText('mockLabware') + screen.getByText('18 °C') + screen.getByText('"mock message"') + }) + it('renders an expanded step for move labware', () => { + vi.mocked(getCollapsedSteps).mockReturnValue({ + [pauseStepId]: true, + [magnetStepId]: true, + [heaterShakerStepId]: true, + [thermocyclerStepId]: true, + [temperatureStepId]: true, + [moveLabwareStepId]: false, + }) + vi.mocked(getSelectedStepId).mockReturnValue(moveLabwareStepId) + props.stepId = moveLabwareStepId + render(props) + screen.getByText('2. move labware') + screen.getByText('Manually') + screen.getByText('labware') + screen.getByText('new location') + }) }) diff --git a/protocol-designer/src/localization/en/application.json b/protocol-designer/src/localization/en/application.json index dfa905ea70c..79625a33d51 100644 --- a/protocol-designer/src/localization/en/application.json +++ b/protocol-designer/src/localization/en/application.json @@ -23,6 +23,7 @@ "next": "Next", "no_batch_edit_shared_settings": "Batch editing of settings is only available for Transfer or Mix steps", "manually": "Manually", + "module_and_slot": "{{moduleLongName}} in Slot {{slotName}}", "stepType": { "mix": "mix", "moveLabware": "move labware", diff --git a/protocol-designer/src/steplist/generateSubstepItem.ts b/protocol-designer/src/steplist/generateSubstepItem.ts index f16b48f412c..edfac2fd19e 100644 --- a/protocol-designer/src/steplist/generateSubstepItem.ts +++ b/protocol-designer/src/steplist/generateSubstepItem.ts @@ -411,6 +411,7 @@ export function generateSubstepItem( temperature: temperature, labwareNickname: labwareNames?.nickname, message: stepArgs.message, + moduleId: stepArgs.module, } } diff --git a/protocol-designer/src/steplist/test/generateSubsteps.test.ts b/protocol-designer/src/steplist/test/generateSubsteps.test.ts index df8c3f5c334..1c2483e0487 100644 --- a/protocol-designer/src/steplist/test/generateSubsteps.test.ts +++ b/protocol-designer/src/steplist/test/generateSubsteps.test.ts @@ -622,6 +622,7 @@ describe('generateSubstepItem', () => { temperature: 45, labwareNickname: 'temp nickname', message: null, + moduleId: 'tempId', }) }) @@ -652,6 +653,7 @@ describe('generateSubstepItem', () => { temperature: 0, labwareNickname: 'temp nickname', message: null, + moduleId: 'tempId', }) }) @@ -680,6 +682,7 @@ describe('generateSubstepItem', () => { temperature: null, labwareNickname: 'temp nickname', message: null, + moduleId: 'tempId', }) }) diff --git a/protocol-designer/src/steplist/types.ts b/protocol-designer/src/steplist/types.ts index 273fe87afdc..297c13e7194 100644 --- a/protocol-designer/src/steplist/types.ts +++ b/protocol-designer/src/steplist/types.ts @@ -5,9 +5,9 @@ import { PauseArgs, ThermocyclerProfileStepArgs, } from '@opentrons/step-generation' -import { ModuleType } from '@opentrons/shared-data' -import { StepIdType } from '../form-types' -import { FormError } from './formLevel/errors' +import type { ModuleType } from '@opentrons/shared-data' +import type { StepIdType } from '../form-types' +import type { FormError } from './formLevel/errors' // timeline start and end export const START_TERMINAL_ITEM_ID: '__initial_setup__' = '__initial_setup__' export const END_TERMINAL_ITEM_ID: '__end__' = '__end__' @@ -105,6 +105,7 @@ export interface TemperatureSubstepItem { substepType: 'temperature' temperature: number | null labwareNickname: string | null | undefined + moduleId: string | null message?: string } export interface PauseSubstepItem { diff --git a/protocol-designer/src/tutorial/index.ts b/protocol-designer/src/tutorial/index.ts index a0eee9ffff3..58a0f522c60 100644 --- a/protocol-designer/src/tutorial/index.ts +++ b/protocol-designer/src/tutorial/index.ts @@ -2,6 +2,7 @@ import * as actions from './actions' import { rootReducer, RootState } from './reducers' import * as selectors from './selectors' type HintKey = // normal hints + | 'multiple_modules_without_labware' | 'add_liquids_and_labware' | 'deck_setup_explanation' | 'module_without_labware' diff --git a/protocol-designer/src/ui/modules/selectors.ts b/protocol-designer/src/ui/modules/selectors.ts index 75057c88dfa..1d5ec7bdb08 100644 --- a/protocol-designer/src/ui/modules/selectors.ts +++ b/protocol-designer/src/ui/modules/selectors.ts @@ -1,4 +1,5 @@ import { createSelector } from 'reselect' +import mapValues from 'lodash/mapValues' import { getLabwareDisplayName, MAGNETIC_MODULE_TYPE, @@ -6,7 +7,6 @@ import { THERMOCYCLER_MODULE_TYPE, HEATERSHAKER_MODULE_TYPE, } from '@opentrons/shared-data' -import mapValues from 'lodash/mapValues' import { getInitialDeckSetup } from '../../step-forms/selectors' import { getLabwareNicknamesById } from '../labware/selectors' import { @@ -15,10 +15,14 @@ import { getModuleOnDeckByType, getModuleHasLabware, getMagnetLabwareEngageHeight as getMagnetLabwareEngageHeightUtil, + getModulesOnDeckByType, + getModulesHaveLabware, + ModuleAndLabware, } from './utils' -import { Options } from '@opentrons/components' -import { Selector } from '../../types' -import { LabwareNamesByModuleId } from '../../steplist/types' +import type { Options } from '@opentrons/components' +import type { Selector } from '../../types' +import type { LabwareNamesByModuleId } from '../../steplist/types' + export const getLabwareNamesByModuleId: Selector = createSelector( getInitialDeckSetup, getLabwareNicknamesById, @@ -84,16 +88,18 @@ export const getSingleMagneticModuleId: Selector< getModuleOnDeckByType(initialDeckSetup, MAGNETIC_MODULE_TYPE)?.id || null ) -/** Get single temperature module (assumes no multiples) */ -export const getSingleTemperatureModuleId: Selector< - string | null +/** Get all temperature modules */ +export const getTemperatureModuleIds: Selector< + string[] | null > = createSelector( getInitialDeckSetup, initialDeckSetup => - getModuleOnDeckByType(initialDeckSetup, TEMPERATURE_MODULE_TYPE)?.id || null + getModulesOnDeckByType(initialDeckSetup, TEMPERATURE_MODULE_TYPE)?.map( + module => module.id + ) || null ) -/** Get single temperature module (assumes no multiples) */ +/** Get single thermocycler module (assumes no multiples) */ export const getSingleThermocyclerModuleId: Selector< string | null > = createSelector( @@ -111,13 +117,12 @@ export const getMagnetModuleHasLabware: Selector = createSelector( } ) -/** Returns boolean if temperature module has labware */ -export const getTemperatureModuleHasLabware: Selector = createSelector( - getInitialDeckSetup, - initialDeckSetup => { - return getModuleHasLabware(initialDeckSetup, TEMPERATURE_MODULE_TYPE) - } -) +/** Returns all moduleIds and if they have labware for MoaM */ +export const getTemperatureModulesHaveLabware: Selector< + ModuleAndLabware[] +> = createSelector(getInitialDeckSetup, initialDeckSetup => { + return getModulesHaveLabware(initialDeckSetup, TEMPERATURE_MODULE_TYPE) +}) /** Returns boolean if thermocycler module has labware */ export const getThermocyclerModuleHasLabware: Selector = createSelector( diff --git a/protocol-designer/src/ui/modules/utils.ts b/protocol-designer/src/ui/modules/utils.ts index fcd1ddb5f43..e49e8ad7b33 100644 --- a/protocol-designer/src/ui/modules/utils.ts +++ b/protocol-designer/src/ui/modules/utils.ts @@ -20,12 +20,25 @@ export function getModuleOnDeckByType( (moduleOnDeck: ModuleOnDeck) => moduleOnDeck.type === type ) } +export function getModulesOnDeckByType( + initialDeckSetup: InitialDeckSetup, + type: ModuleType +): ModuleOnDeck[] | null | undefined { + return values(initialDeckSetup.modules).filter( + (moduleOnDeck: ModuleOnDeck) => moduleOnDeck.type === type + ) +} export function getLabwareOnModule( initialDeckSetup: InitialDeckSetup, moduleId: string ): LabwareOnDeck | null | undefined { return values(initialDeckSetup.labware).find( - (lab: LabwareOnDeck) => lab.slot === moduleId + (labware: LabwareOnDeck) => + labware.slot === moduleId || + // acccount for adapter! + values(initialDeckSetup.labware).find( + adapter => adapter.id === labware.slot && adapter.slot === moduleId + ) ) } export function getModuleUnderLabware( @@ -81,28 +94,39 @@ export function getModuleLabwareOptions( nicknamesById: Record, type: ModuleType ): Options { - const moduleOnDeck = getModuleOnDeckByType(initialDeckSetup, type) - const labware = - moduleOnDeck && getLabwareOnModule(initialDeckSetup, moduleOnDeck.id) + const labwares = initialDeckSetup.labware + const modulesOnDeck = getModulesOnDeckByType(initialDeckSetup, type) const module = getModuleShortNames(type) let options: Options = [] - if (moduleOnDeck) { - if (labware) { - options = [ - { - name: `${nicknamesById[labware.id]} in ${module}`, + if (modulesOnDeck != null) { + options = modulesOnDeck.map(moduleOnDeck => { + const labware = getLabwareOnModule(initialDeckSetup, moduleOnDeck.id) + if (labware) { + const labwareOnAdapterId = + labwares[labware.id] != null ? labwares[labware.id].id : null + if (labwareOnAdapterId != null) { + return { + name: `${nicknamesById[labwareOnAdapterId]} in ${ + nicknamesById[labware.id] + } in ${module} in slot ${moduleOnDeck.slot}`, + value: moduleOnDeck.id, + } + } else { + return { + name: `${nicknamesById[labware.id]} in ${module} in slot ${ + moduleOnDeck.slot + }`, + value: moduleOnDeck.id, + } + } + } else { + return { + name: `No labware in ${module} in slot ${moduleOnDeck.slot}`, value: moduleOnDeck.id, - }, - ] - } else { - options = [ - { - name: `${module} No labware on module`, - value: moduleOnDeck.id, - }, - ] - } + } + } + }) } return options @@ -116,6 +140,29 @@ export function getModuleHasLabware( moduleOnDeck && getLabwareOnModule(initialDeckSetup, moduleOnDeck.id) return Boolean(moduleOnDeck) && Boolean(labware) } + +export interface ModuleAndLabware { + moduleId: string + hasLabware: boolean +} + +export function getModulesHaveLabware( + initialDeckSetup: InitialDeckSetup, + type: ModuleType +): ModuleAndLabware[] { + const modulesOnDeck = getModulesOnDeckByType(initialDeckSetup, type) + const moduleAndLabware: ModuleAndLabware[] = [] + modulesOnDeck?.forEach(module => { + const labwareHasModule = getLabwareOnModule(initialDeckSetup, module.id) + + moduleAndLabware.push({ + moduleId: module.id, + hasLabware: labwareHasModule != null, + }) + }) + return moduleAndLabware +} + export const getMagnetLabwareEngageHeight = ( initialDeckSetup: InitialDeckSetup, magnetModuleId: string | null diff --git a/protocol-designer/src/ui/steps/actions/__tests__/addAndSelectStepWithHints.test.ts b/protocol-designer/src/ui/steps/actions/__tests__/addAndSelectStepWithHints.test.ts index 2a087d4ac31..56046da6a98 100644 --- a/protocol-designer/src/ui/steps/actions/__tests__/addAndSelectStepWithHints.test.ts +++ b/protocol-designer/src/ui/steps/actions/__tests__/addAndSelectStepWithHints.test.ts @@ -19,15 +19,13 @@ beforeEach(() => { vi.mocked(addHint).mockReturnValue('addHintReturnValue' as any) vi.mocked(labwareIngredSelectors.getDeckHasLiquid).mockReturnValue(true) vi.mocked(uiModuleSelectors.getMagnetModuleHasLabware).mockReturnValue(false) - vi.mocked(uiModuleSelectors.getTemperatureModuleHasLabware).mockReturnValue( - false + vi.mocked(uiModuleSelectors.getTemperatureModulesHaveLabware).mockReturnValue( + [] ) vi.mocked(uiModuleSelectors.getThermocyclerModuleHasLabware).mockReturnValue( false ) - vi.mocked(uiModuleSelectors.getSingleTemperatureModuleId).mockReturnValue( - null - ) + vi.mocked(uiModuleSelectors.getTemperatureModuleIds).mockReturnValue(null) vi.mocked(uiModuleSelectors.getSingleThermocyclerModuleId).mockReturnValue( null ) @@ -89,10 +87,11 @@ describe('addAndSelectStepWithHints', () => { stepType: 'magnet' as StepType, selectorValues: { getMagnetModuleHasLabware: false, - getTemperatureModuleHasLabware: false, + getTemperatureModulesHaveLabware: [], getThermocyclerModuleHasLabware: false, getSingleTemperatureModuleId: null, getSingleThermocyclerModuleId: null, + getTemperatureModuleIds: [], }, }, { @@ -100,10 +99,13 @@ describe('addAndSelectStepWithHints', () => { stepType: 'temperature' as StepType, selectorValues: { getMagnetModuleHasLabware: false, - getTemperatureModuleHasLabware: false, + getTemperatureModulesHaveLabware: [ + { moduleId: 'mockId', hasLabware: false }, + ], getThermocyclerModuleHasLabware: false, getSingleTemperatureModuleId: 'something', getSingleThermocyclerModuleId: null, + getTemperatureModuleIds: ['mockId'], }, }, { @@ -111,10 +113,11 @@ describe('addAndSelectStepWithHints', () => { stepType: 'temperature' as StepType, selectorValues: { getMagnetModuleHasLabware: false, - getTemperatureModuleHasLabware: false, + getTemperatureModulesHaveLabware: [], getThermocyclerModuleHasLabware: false, getSingleTemperatureModuleId: null, getSingleThermocyclerModuleId: 'something', + getTemperatureModuleIds: [], }, }, ].forEach(({ testName, stepType, selectorValues }) => { @@ -123,14 +126,14 @@ describe('addAndSelectStepWithHints', () => { selectorValues.getMagnetModuleHasLabware ) vi.mocked( - uiModuleSelectors.getTemperatureModuleHasLabware - ).mockReturnValue(selectorValues.getTemperatureModuleHasLabware) + uiModuleSelectors.getTemperatureModulesHaveLabware + ).mockReturnValue(selectorValues.getTemperatureModulesHaveLabware) vi.mocked( uiModuleSelectors.getThermocyclerModuleHasLabware ).mockReturnValue(selectorValues.getThermocyclerModuleHasLabware) - vi.mocked( - uiModuleSelectors.getSingleTemperatureModuleId - ).mockReturnValue(selectorValues.getSingleTemperatureModuleId) + vi.mocked(uiModuleSelectors.getTemperatureModuleIds).mockReturnValue( + selectorValues.getTemperatureModuleIds + ) vi.mocked( uiModuleSelectors.getSingleThermocyclerModuleId ).mockReturnValue(selectorValues.getSingleThermocyclerModuleId) @@ -159,4 +162,56 @@ describe('addAndSelectStepWithHints', () => { }) }) }) + describe('ADD_HINT "multiple_modules_without_labware"', () => { + ;[ + { + testName: 'temperature step, when temperature module has no labware', + stepType: 'temperature' as StepType, + selectorValues: { + getMagnetModuleHasLabware: false, + getTemperatureModulesHaveLabware: [ + { moduleId: 'mockId', hasLabware: false }, + { moduleId: 'mockId2', hasLabware: true }, + ], + getThermocyclerModuleHasLabware: false, + getSingleTemperatureModuleId: 'something', + getSingleThermocyclerModuleId: null, + getTemperatureModuleIds: ['mockId', 'mockId2'], + }, + }, + ].forEach(({ testName, stepType, selectorValues }) => { + it(`should be dispatched (after addStep thunk is dispatched) for ${testName}`, () => { + vi.mocked( + uiModuleSelectors.getTemperatureModulesHaveLabware + ).mockReturnValue(selectorValues.getTemperatureModulesHaveLabware) + + vi.mocked(uiModuleSelectors.getTemperatureModuleIds).mockReturnValue( + selectorValues.getTemperatureModuleIds + ) + + const payload = { + stepType, + } + addAndSelectStepWithHints(payload)(dispatch, getState) + expect(vi.mocked(addHint).mock.calls).toEqual([ + ['multiple_modules_without_labware'], + ]) + expect(dispatch.mock.calls).toEqual([ + [ + { + type: 'ADD_STEP', + payload: { + id: PRESAVED_STEP_ID, + stepType, + }, + meta: { + robotStateTimeline: 'mockGetRobotStateTimelineValue', + }, + }, + ], + ['addHintReturnValue'], + ]) + }) + }) + }) }) diff --git a/protocol-designer/src/ui/steps/actions/thunks/index.ts b/protocol-designer/src/ui/steps/actions/thunks/index.ts index c6d8be20159..9cc31de8ab8 100644 --- a/protocol-designer/src/ui/steps/actions/thunks/index.ts +++ b/protocol-designer/src/ui/steps/actions/thunks/index.ts @@ -40,18 +40,21 @@ export const addAndSelectStepWithHints: (arg: { const magnetModuleHasLabware = uiModuleSelectors.getMagnetModuleHasLabware( state ) - const temperatureModuleHasLabware = uiModuleSelectors.getTemperatureModuleHasLabware( + const temperatureModulesHaveLabware = uiModuleSelectors.getTemperatureModulesHaveLabware( state ) const thermocyclerModuleHasLabware = uiModuleSelectors.getThermocyclerModuleHasLabware( state ) - const temperatureModuleOnDeck = uiModuleSelectors.getSingleTemperatureModuleId( + const temperatureModuleOnDeck = uiModuleSelectors.getTemperatureModuleIds( state ) const thermocyclerModuleOnDeck = uiModuleSelectors.getSingleThermocyclerModuleId( state ) + const tempHasNoLabware = temperatureModulesHaveLabware.some( + module => !module.hasLabware + ) // TODO: Ian 2019-01-17 move out to centralized step info file - see #2926 const stepNeedsLiquid = ['mix', 'moveLiquid'].includes(payload.stepType) const stepMagnetNeedsLabware = ['magnet'].includes(payload.stepType) @@ -59,15 +62,17 @@ export const addAndSelectStepWithHints: (arg: { const stepModuleMissingLabware = (stepMagnetNeedsLabware && !magnetModuleHasLabware) || (stepTemperatureNeedsLabware && - ((temperatureModuleOnDeck && !temperatureModuleHasLabware) || - (thermocyclerModuleOnDeck && !thermocyclerModuleHasLabware))) + thermocyclerModuleOnDeck && + !thermocyclerModuleHasLabware) || + (temperatureModuleOnDeck?.length === 1 && tempHasNoLabware) if (stepNeedsLiquid && !deckHasLiquid) { dispatch(tutorialActions.addHint('add_liquids_and_labware')) } - if (stepModuleMissingLabware) { dispatch(tutorialActions.addHint('module_without_labware')) + } else if (temperatureModuleOnDeck && tempHasNoLabware) { + dispatch(tutorialActions.addHint('multiple_modules_without_labware')) } } export interface ReorderSelectedStepAction { diff --git a/protocol-designer/src/ui/steps/selectors.ts b/protocol-designer/src/ui/steps/selectors.ts index f9a228366d3..8ed2eeb20dd 100644 --- a/protocol-designer/src/ui/steps/selectors.ts +++ b/protocol-designer/src/ui/steps/selectors.ts @@ -136,10 +136,11 @@ export const getHoveredStepLabware = createSelector( // only 1 labware return [stepArgs.labware] } - // @ts-expect-error(sa, 2021-6-15): type narrow stepArgs.module - if (stepArgs.module) { - // @ts-expect-error(sa, 2021-6-15): this expect error should not be necessary after type narrowing above - const labware = getLabwareOnModule(initialDeckState, stepArgs.module) + if ('module' in stepArgs) { + const labware = getLabwareOnModule( + initialDeckState, + stepArgs.module ?? '' + ) return labware ? [labware.id] : [] } @@ -150,8 +151,9 @@ export const getHoveredStepLabware = createSelector( // step types that have no labware that gets highlighted if (!(stepArgs.commandCreatorFnName === 'delay')) { - // TODO Ian 2018-05-08 use assert here console.warn( + // @ts-expect-error: should only reach this warning when new step is added and + // highlighted wells is not yet implemented `getHoveredStepLabware does not support step type "${stepArgs.commandCreatorFnName}"` ) } From cc084a47d1322bd7152416c86495e45dc24a924e Mon Sep 17 00:00:00 2001 From: Jethary Rader <66035149+jerader@users.noreply.github.com> Date: Tue, 9 Apr 2024 10:26:44 -0400 Subject: [PATCH 075/194] fix(shared-data): format rtp float and int choices to include suffix (#14842) closes AUTH-311 --- .../formatRunTimeParameterValue.test.ts | 60 +++++++++++++++++-- .../formatRunTimeParameterDefaultValue.ts | 22 ++++--- .../js/helpers/formatRunTimeParameterValue.ts | 21 ++++--- 3 files changed, 81 insertions(+), 22 deletions(-) diff --git a/shared-data/js/helpers/__tests__/formatRunTimeParameterValue.test.ts b/shared-data/js/helpers/__tests__/formatRunTimeParameterValue.test.ts index a405d5845d3..2f78d99e11c 100644 --- a/shared-data/js/helpers/__tests__/formatRunTimeParameterValue.test.ts +++ b/shared-data/js/helpers/__tests__/formatRunTimeParameterValue.test.ts @@ -41,11 +41,11 @@ describe('utils-formatRunTimeParameterDefaultValue', () => { expect(result).toEqual('6.5 mL') }) - it('should return value with suffix when type is str', () => { + it('should return value when type is str', () => { const mockData = { value: 'left', displayName: 'pipette mount', - variableName: 'mont', + variableName: 'mount', description: 'pipette mount', type: 'str', choices: [ @@ -64,7 +64,59 @@ describe('utils-formatRunTimeParameterDefaultValue', () => { expect(result).toEqual('Left') }) - it('should return value with suffix when type is boolean true', () => { + it('should return value when type is int choice with suffix', () => { + const mockData = { + value: 5, + displayName: 'num', + variableName: 'number', + description: 'its just number', + type: 'int', + suffix: 'mL', + min: 1, + max: 10, + choices: [ + { + displayName: 'one', + value: 1, + }, + { + displayName: 'six', + value: 6, + }, + ], + default: 5, + } as RunTimeParameter + const result = formatRunTimeParameterDefaultValue(mockData, mockTFunction) + expect(result).toEqual('5 mL') + }) + + it('should return value when type is float choice with suffix', () => { + const mockData = { + value: 5.0, + displayName: 'num', + variableName: 'number', + description: 'its just number', + type: 'float', + suffix: 'mL', + min: 1.0, + max: 10.0, + choices: [ + { + displayName: 'one', + value: 1.0, + }, + { + displayName: 'six', + value: 6.0, + }, + ], + default: 5.0, + } as RunTimeParameter + const result = formatRunTimeParameterDefaultValue(mockData, mockTFunction) + expect(result).toEqual('5 mL') + }) + + it('should return value when type is boolean true', () => { const mockData = { value: true, displayName: 'Deactivate Temperatures', @@ -77,7 +129,7 @@ describe('utils-formatRunTimeParameterDefaultValue', () => { expect(result).toEqual('On') }) - it('should return value with suffix when type is boolean false', () => { + it('should return value when type is boolean false', () => { const mockData = { value: false, displayName: 'Dry Run', diff --git a/shared-data/js/helpers/formatRunTimeParameterDefaultValue.ts b/shared-data/js/helpers/formatRunTimeParameterDefaultValue.ts index 78de4e78f02..aa7d16a256f 100644 --- a/shared-data/js/helpers/formatRunTimeParameterDefaultValue.ts +++ b/shared-data/js/helpers/formatRunTimeParameterDefaultValue.ts @@ -9,6 +9,18 @@ export const formatRunTimeParameterDefaultValue = ( 'suffix' in runTimeParameter && runTimeParameter.suffix != null ? runTimeParameter.suffix : null + + if ('choices' in runTimeParameter && runTimeParameter.choices != null) { + const choice = runTimeParameter.choices.find( + choice => choice.value === defaultValue + ) + if (choice != null) { + return suffix != null + ? `${choice.displayName} ${suffix}` + : choice.displayName + } + } + switch (type) { case 'int': case 'float': @@ -21,15 +33,7 @@ export const formatRunTimeParameterDefaultValue = ( } else { return Boolean(defaultValue) ? 'On' : 'Off' } - case 'str': - if ('choices' in runTimeParameter && runTimeParameter.choices != null) { - const choice = runTimeParameter.choices.find( - choice => choice.value === defaultValue - ) - if (choice != null) { - return choice.displayName - } - } + default: break } return '' diff --git a/shared-data/js/helpers/formatRunTimeParameterValue.ts b/shared-data/js/helpers/formatRunTimeParameterValue.ts index 0aa0b72a194..a75bee5fd68 100644 --- a/shared-data/js/helpers/formatRunTimeParameterValue.ts +++ b/shared-data/js/helpers/formatRunTimeParameterValue.ts @@ -9,6 +9,17 @@ export const formatRunTimeParameterValue = ( 'suffix' in runTimeParameter && runTimeParameter.suffix != null ? runTimeParameter.suffix : null + + if ('choices' in runTimeParameter && runTimeParameter.choices != null) { + const choice = runTimeParameter.choices.find( + choice => choice.value === value + ) + if (choice != null) { + return suffix != null + ? `${choice.displayName} ${suffix}` + : choice.displayName + } + } switch (type) { case 'int': case 'float': @@ -18,15 +29,7 @@ export const formatRunTimeParameterValue = ( case 'bool': { return Boolean(value) ? t('on') : t('off') } - case 'str': - if ('choices' in runTimeParameter && runTimeParameter.choices != null) { - const choice = runTimeParameter.choices.find( - choice => choice.value === value - ) - if (choice != null) { - return choice.displayName - } - } + default: break } return '' From 19d88ce2a61c9861b34f028031b728907afa31bd Mon Sep 17 00:00:00 2001 From: Jethary Rader <66035149+jerader@users.noreply.github.com> Date: Tue, 9 Apr 2024 11:07:52 -0400 Subject: [PATCH 076/194] fix(protocol-designer): magnetic form correct engage height ranges (#14841) closes RQA-2490, AUTH-269 --- .../StepEditForm/StepEditForm.module.css | 27 ----- .../StepEditForm/forms/MagnetForm.tsx | 57 +++++----- .../forms/__tests__/HeaterShakerForm.test.tsx | 2 +- .../forms/__tests__/MagnetForm.test.tsx | 97 +++++++++++++++++- protocol-designer/src/constants.ts | 8 +- .../modules/engage_height_animation_gen1.gif | Bin 22908 -> 0 bytes .../modules/engage_height_animation_gen2.gif | Bin 23678 -> 0 bytes .../modules/engage_height_static_gen1.png | Bin 7597 -> 0 bytes .../modules/engage_height_static_gen2.png | Bin 8173 -> 0 bytes .../src/localization/en/application.json | 2 + 10 files changed, 131 insertions(+), 62 deletions(-) delete mode 100644 protocol-designer/src/images/modules/engage_height_animation_gen1.gif delete mode 100644 protocol-designer/src/images/modules/engage_height_animation_gen2.gif delete mode 100644 protocol-designer/src/images/modules/engage_height_static_gen1.png delete mode 100644 protocol-designer/src/images/modules/engage_height_static_gen2.png diff --git a/protocol-designer/src/components/StepEditForm/StepEditForm.module.css b/protocol-designer/src/components/StepEditForm/StepEditForm.module.css index 5e27c4358fb..439dccbdf8c 100644 --- a/protocol-designer/src/components/StepEditForm/StepEditForm.module.css +++ b/protocol-designer/src/components/StepEditForm/StepEditForm.module.css @@ -269,33 +269,6 @@ and when that is implemented. margin: 1rem 0 2rem 14rem; } -.engage_height_diagram { - width: 90%; - padding-top: calc(40 / 540 * 90%); - background-repeat: no-repeat; - background-size: cover; - - &:hover { - cursor: pointer; - } -} - -.engage_height_diagram_gen1 { - background-image: url('../../images/modules/engage_height_static_gen1.png'); - - &:hover { - background-image: url('../../images/modules/engage_height_animation_gen1.gif'); - } -} - -.engage_height_diagram_gen2 { - background-image: url('../../images/modules/engage_height_static_gen2.png'); - - &:hover { - background-image: url('../../images/modules/engage_height_animation_gen2.gif'); - } -} - .tc_step_group { margin: 1rem 0; } diff --git a/protocol-designer/src/components/StepEditForm/forms/MagnetForm.tsx b/protocol-designer/src/components/StepEditForm/forms/MagnetForm.tsx index 8873c10eb52..1976767e7e5 100644 --- a/protocol-designer/src/components/StepEditForm/forms/MagnetForm.tsx +++ b/protocol-designer/src/components/StepEditForm/forms/MagnetForm.tsx @@ -1,27 +1,31 @@ import * as React from 'react' -import cx from 'classnames' import { useTranslation } from 'react-i18next' import { useSelector } from 'react-redux' import { FormGroup } from '@opentrons/components' import { MAGNETIC_MODULE_V1 } from '@opentrons/shared-data' import { selectors as uiModuleSelectors } from '../../../ui/modules' -import { selectors as stepFormSelectors } from '../../../step-forms' -import { maskField } from '../../../steplist/fieldLevel' +import { getModuleEntities } from '../../../step-forms/selectors' +import { + MAX_ENGAGE_HEIGHT_V1, + MAX_ENGAGE_HEIGHT_V2, + MIN_ENGAGE_HEIGHT_V1, + MIN_ENGAGE_HEIGHT_V2, +} from '../../../constants' import { TextField, RadioGroupField } from '../fields' -import styles from '../StepEditForm.module.css' +import type { StepFormProps } from '../types' -import { StepFormProps } from '../types' +import styles from '../StepEditForm.module.css' -export const MagnetForm = (props: StepFormProps): JSX.Element => { +export function MagnetForm(props: StepFormProps): JSX.Element { const moduleLabwareOptions = useSelector( uiModuleSelectors.getMagneticLabwareOptions ) + const moduleEntities = useSelector(getModuleEntities) const { t } = useTranslation(['application', 'form']) + const { propsForFields, formData } = props + const { magnetAction, moduleId } = formData - const moduleEntities = useSelector(stepFormSelectors.getModuleEntities) - const { magnetAction, moduleId } = props.formData - const moduleModel = moduleId ? moduleEntities[moduleId]?.model : null - + const moduleModel = moduleEntities[moduleId].model const moduleOption: string | null | undefined = moduleLabwareOptions[0] ? moduleLabwareOptions[0].name : 'No magnetic module' @@ -29,12 +33,21 @@ export const MagnetForm = (props: StepFormProps): JSX.Element => { const defaultEngageHeight = useSelector( uiModuleSelectors.getMagnetLabwareEngageHeight ) - - const engageHeightCaption = defaultEngageHeight - ? `Recommended: ${String(maskField('engageHeight', defaultEngageHeight))}` - : null - - const { propsForFields } = props + const engageHeightMinMax = + moduleModel === MAGNETIC_MODULE_V1 + ? t('magnet_height_caption', { + low: MIN_ENGAGE_HEIGHT_V1, + high: MAX_ENGAGE_HEIGHT_V1, + }) + : t('magnet_height_caption', { + low: MIN_ENGAGE_HEIGHT_V2, + high: MAX_ENGAGE_HEIGHT_V2, + }) + const engageHeightDefault = + defaultEngageHeight != null + ? t('magnet_recommended', { default: defaultEngageHeight }) + : '' + const engageHeightCaption = `${engageHeightMinMax} ${engageHeightDefault}` return (
    @@ -91,18 +104,6 @@ export const MagnetForm = (props: StepFormProps): JSX.Element => { )}
    - {magnetAction === 'engage' && ( -
    -
    -
    - )}
    ) } diff --git a/protocol-designer/src/components/StepEditForm/forms/__tests__/HeaterShakerForm.test.tsx b/protocol-designer/src/components/StepEditForm/forms/__tests__/HeaterShakerForm.test.tsx index 6ddefc3af74..dbc5bb5a408 100644 --- a/protocol-designer/src/components/StepEditForm/forms/__tests__/HeaterShakerForm.test.tsx +++ b/protocol-designer/src/components/StepEditForm/forms/__tests__/HeaterShakerForm.test.tsx @@ -31,7 +31,7 @@ vi.mock('../../fields', async importOriginal => { const render = (props: React.ComponentProps) => { return renderWithProviders(, { - i18nInstance: i18n as any, + i18nInstance: i18n, })[0] } diff --git a/protocol-designer/src/components/StepEditForm/forms/__tests__/MagnetForm.test.tsx b/protocol-designer/src/components/StepEditForm/forms/__tests__/MagnetForm.test.tsx index 736294018a9..34146989405 100644 --- a/protocol-designer/src/components/StepEditForm/forms/__tests__/MagnetForm.test.tsx +++ b/protocol-designer/src/components/StepEditForm/forms/__tests__/MagnetForm.test.tsx @@ -1,5 +1,98 @@ -import { describe, it } from 'vitest' +import * as React from 'react' +import { describe, it, afterEach, vi, beforeEach } from 'vitest' +import '@testing-library/jest-dom/vitest' +import { cleanup, fireEvent, screen } from '@testing-library/react' +import { renderWithProviders } from '../../../../__testing-utils__' +import { i18n } from '../../../../localization' +import { getMagneticLabwareOptions } from '../../../../ui/modules/selectors' +import { getModuleEntities } from '../../../../step-forms/selectors' +import { getMagnetLabwareEngageHeight } from '../../../../ui/modules/utils' +import { MagnetForm } from '../MagnetForm' + +vi.mock('../../../../ui/modules/utils') +vi.mock('../../../../ui/modules/selectors') +vi.mock('../../../../step-forms/selectors') +const render = (props: React.ComponentProps) => { + return renderWithProviders(, { + i18nInstance: i18n, + })[0] +} describe('MagnetForm', () => { - it.todo('replace deprecated enzyme test') + let props: React.ComponentProps + + beforeEach(() => { + props = { + formData: { + id: 'magnet', + stepType: 'magnet', + moduleId: 'magnetId', + magnetAction: 'engage', + } as any, + focusHandlers: { + blur: vi.fn(), + focus: vi.fn(), + dirtyFields: [], + focusedField: null, + }, + propsForFields: { + magnetAction: { + onFieldFocus: vi.fn(), + onFieldBlur: vi.fn(), + errorToShow: null, + disabled: false, + name: 'magnetAction', + updateValue: vi.fn(), + value: 'engage', + }, + engageHeight: { + onFieldFocus: vi.fn(), + onFieldBlur: vi.fn(), + errorToShow: null, + disabled: false, + name: 'engage height', + updateValue: vi.fn(), + value: 10, + }, + }, + } + vi.mocked(getMagneticLabwareOptions).mockReturnValue([ + { name: 'mock name', value: 'mockValue' }, + ]) + vi.mocked(getModuleEntities).mockReturnValue({ + magnetId: { + id: 'magnetId', + model: 'magneticModuleV2', + type: 'magneticModuleType', + }, + }) + vi.mocked(getMagnetLabwareEngageHeight).mockReturnValue(null) + }) + afterEach(() => { + vi.restoreAllMocks() + cleanup() + }) + + it('renders the text and radio buttons for v2', () => { + render(props) + screen.getByText('magnet') + screen.getByText('module') + screen.getByText('mock name') + screen.getByText('Magnet action') + const engage = screen.getByText('Engage') + screen.getByText('Disengage') + fireEvent.click(engage) + screen.getByText('Must be between -2.5 to 25.') + }) + it('renders the input text for v1', () => { + vi.mocked(getModuleEntities).mockReturnValue({ + magnetId: { + id: 'magnetId', + model: 'magneticModuleV1', + type: 'magneticModuleType', + }, + }) + render(props) + screen.getByText('Must be between 0 to 45.') + }) }) diff --git a/protocol-designer/src/constants.ts b/protocol-designer/src/constants.ts index bae70d17d7f..b92192565c2 100644 --- a/protocol-designer/src/constants.ts +++ b/protocol-designer/src/constants.ts @@ -65,10 +65,10 @@ export const DEFAULT_MM_BLOWOUT_OFFSET_FROM_TOP = 0 export const DEFAULT_DELAY_SECONDS = 1 export const DEFAULT_WELL_ORDER_FIRST_OPTION: 't2b' = 't2b' export const DEFAULT_WELL_ORDER_SECOND_OPTION: 'l2r' = 'l2r' -export const MIN_ENGAGE_HEIGHT_V1 = -5 -export const MAX_ENGAGE_HEIGHT_V1 = 40 -export const MIN_ENGAGE_HEIGHT_V2 = -4 -export const MAX_ENGAGE_HEIGHT_V2 = 19 +export const MIN_ENGAGE_HEIGHT_V1 = 0 +export const MAX_ENGAGE_HEIGHT_V1 = 45 +export const MIN_ENGAGE_HEIGHT_V2 = -2.5 +export const MAX_ENGAGE_HEIGHT_V2 = 25 export const MIN_TEMP_MODULE_TEMP = 4 export const MAX_TEMP_MODULE_TEMP = 95 export const MIN_HEATER_SHAKER_MODULE_TEMP = 37 diff --git a/protocol-designer/src/images/modules/engage_height_animation_gen1.gif b/protocol-designer/src/images/modules/engage_height_animation_gen1.gif deleted file mode 100644 index eb0bedae5735d1b76b2cbc9ed83827b5716febc7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 22908 zcmeIacUY6z(?6UBDL@DqI!cK2W`cAJ9l;7#M3f*MQL3Hfjv;sG7&=lz?+}_)F@OSs z1+byJDt6bhi@IxH`P~7c^62+@uj_q&?;r1VJ^r)Gl9@AeJ~L;|%;#i{i<5<=Hy)t_ zdji1X&&-^=^Y!7whjcpKr*bB1;Onzz&!(oPmM>p^<;s=ayLZ2MJM-tCe+mSGhK7c` zyu8lN&gSOkJ9q9}d^mIQ;>C>{Hy%8A@WY1>F)=Y38XDc*-I*`D=B$IY1~%$b|dX83~-(t2JWD1UkP z>DzNRrVH9{uStH&X`QC!zo_ZE_wLiomET`I{qtQ+`Pl8-w~rKDsvY@zXlSUnx3_)b z-D=L0*RNlXjg2w+e|`G&slUJf=Kbk+@7`U%e*K?+{<(GQ*3+j?zkK=f@4x?^IdkUg z*RR*ET^k%6JbU)+Z@>LEJw5&Y{rl0;(Ytr=!a`?0|2y;Y<;&LA)|uHq>N64>9k*_= zw{>+iHliTmFc=K_k^2T$l7n}YH}v0?)u^siOqz^JG&t7Xd$A3kh!c)3w%M8Gnt zrKROEit#dIV?*dS3?q*oi1I#Scp!4ANP>f3q)$XpSX5Bx0g_On_ukNhQLD(%O6N@2 zANEb`fk>lSbBugK_b)r*9kz^WL=l=K`o#?!{`+6uzyI5BM@DV-`@Y^^3Xa@*G|X?= zX1~bLgAqP{(628QstU8RkMQ%33XRwr8X6+k#U5aKGPyDA8wqzkincA3rOE!~QTxvx8qm&>=rxr-;z~q*=4Ag8t)L zY#sljy!p@i{>Qc0I{nAbE`t`cOt`r}Y_{k}5Q2n%{RUm=AK$?5cK||s1O#pP%-4Uv zeE#(D!~1vt{QdUL>*>G#eD(6h^FN+Fee(Fx!w2`L?%lof`){{zP2Rk5{o2)u@v$qH zFI^lRxiCC5c>df#f1luN@0p(NuFj73(`~IS%}xBqhWfhNn(C^`it@73l44#_VL^Ug z?x~#Ytjvt`v{dfNl;otug!s5v4#AwZQbVVCc64@i(hX>51$}1?=*p-2hY9qs^TYikG&&c(d(^!9lniXeRE{-hOX8>m@%Du93SnlDMI>}8(TT|WMg`b|#_>%Z(P-u>-@ zd<}n>&eigcEtEN-a3#0rXba=omK~FOOp89;JL7auT_JNv`XP)~70>hc#c8*^?!2ZB zu6?-eksF!S)Sj=-`3UE!C#-T$vtM*ztM~q8>S^8wRhPW;R!=x#Uc!D?aAb;Q@uX^L zaI$3Ju&cDG#jqP=>=4;w;^<`T3_M}%iI2L?-SmNG$s^{`xYJCKfx%uDmQz;@U#jY9fI-S)`ulMDllNoclC8}R zYVNF0*;x~~y@8puK~KNhbvW7Ob?e8Yo!vRlvm+0%UV36KSu2nW$DUc*8J+V_>9a$` zgoT+7CQgn=s4r3IhlQ|Bc+F}GCSy#s1A#OaJglrqTrrgY=1upT2QGXy+)c@4c`wNi zRPQ!o)0T~izgzV;4KGnw-N}+nF%W=_b%yH4Pr9^~`LJPqdxKq>|A-yrQU}Bi^JQYy zBttagu-K~b6KeY@JSW61B^z<2h>Vc_KMjY}Z4vsz;nbJSi~8-PdLl{AQs)}O1ElI2 z1>LIq1n|$2hwD=9a1p=1{xYK)Xg}#{+&9QS?Ji38#lt3RNh5tY*mbs zV5;Y4ITQZ$ZH%hG6)7$_0l&2ii6oJFkrV-p5NE$cHVlqs3#7Cxyq9c#gvMylLU{M> zk%}~gRFArJNEj0JQUI3{@THgY2PExi9Jz}$$pZw3zhF)r)4F>6!6SE^@XWUNJ8^*o z*2K14Q|p_rt}qCZ(W9Ts-P{(ruR_`HPONEo?J4I^*6k9rr%V z$v)D4cUzU)A2_Btv>DO?-hjfeR-9#2m?XhDM)hkm0RJ9`Gs`Q>>qziNFeIV&M9YTczdr!=yt;L$6YYO3_ud|!8Rk<| ze2QJdb_QvOK}YXDg^{n6RA1b7?(uk@@=5omm-bFK=iS$&j|v8En|kGiM%bvJDY7=uShTF z#;6X%PADmoDj4pz7lH{B9zEf*_cES6tSNU6secm2{y?;T^}cSU<9pZY3)c=jyJkIl zKAZp}o;xIU>yADBChY;3|4{jb`4`^G;?KjF#EbW(q?qZS|15)JRcT^+lJ#oJ zHgIFU?Ot^Xd`67K=jdrMCDCz6HC#09F}y9|%kC2@!741mSd>(k>A=wKF!4phX%esg zK^i|CgPA$oAX{RZ?i8ww+v#jPA^CZi##4R4+wBTH_=)ZL49~kR)aUML2G0tVu{?3_ z_IifGYiIW3Om7K@u=cB`&; zjipZaNvT`F)FdhBlyKPPo63Cb^DNFvX8|mHN}za~z*3&JMcgFbQgIl3dj3erK~@+t z?@|~H+Yu1I;?D@0_~;NxrEXn3sNwly=Q-_r>6h;B{U;T@)_v(;nS|qH)Pb+hD?Z-O zJ<)ag#**?Gt$WK>*TykHfSoQ!|Mr}ZPXg^uY>CG zY{NTC)*U6(8JpX;Bp&F$v2TQWz&y>iHQ(aA%Ff1PnXtD~is9>?-NI~gkh)BXC9-Xf zfi&d6Dvwv&Ci~9b`>ee5E@_>kCn+f{&RO$_J}XS?8M|<&KGaXaE@<5j zQvFQ{Kck1K&p-e9@p|?m{u|TXX9%lqG%2XV_HcWig>A|{8{&YG4}WUE->~}zHuF$d z$+H)@Wbpm^rs zLwt4T(!Gxz*OM-9YmCg+-sc*%yX)lkluvdQh4&I)?R!KkIx?{Pz`LR9RWCxljMCmY zyag>zU}W#Z#GaKRqTJ6(b^Q^mTGua$A;rLa`6u9{DJgQ;>OYqH9otp#@9&$;KR0Rn z?z1(}Oj+vLmYRHX6bE^`~W;xz044XFs*#hQZ2O`#PjFxE6u zc}m=PY}X$X@I7&2F?`tSF?k(6jLnCcP%y`*z&+2PXNop=;CSgDb!14|FI&OD->or8On6+LQ>(GEjWNytzV8ySYgOv9vk2uB2R^eF;K zgIP|&^lT7}Fa(AM$CF@6e0W<`xEw#REf98y3s)pTKS$~_VTxmD85k08GXwYH!lemF zC4o2thSZ=Tt-0_{9pt5QnDi7HHw9x+U`DQV6&urIVJMOSwT8x7XM@`R8nKvyaiJg_ z2^>ur$_9pDz+lkfD{&5EuP&9zjcTyPj&Khh{3liIsCapfAK3*nfA z!Vyz2jO$v=R1$***QS8hT(T?|e&Jc-)+2~LJ~1(_@PD46^#q9z%+Wir^g1Mx#e@C( z3~fZCQ^t(xVVS#*W;2*D>NfZlcJ}tk#IHJ-RZN7<)C}5%%Uwl*!?`FHA7(ekWsISY zQ{anWu_;Qh{8L;P0?bDTvzUZhO@U_woyz$GU3m(HvBc;w5$q|XtR==>fbv(w%7jO~ zo6hUv!lVQU!WgVKNZNM{Mi3yRxG(|MCx8d@#@Q6d(N6ri0}j(eYU(h=Nk|44j^@JEM_|TN z;$bkJ2B}1wpM~SXttq7+bTD$H8G~&Wu-KQGyO0MlB+sP-DntHZW8eior)MwSg@qpcvB#5+Au{8gYCIxs-_@Q7R;+ z5e}qEhAYel23rq9SimawuB{LwRM@y8u)3I~uDL7{YK0B*I0b`pg~=bqah0pntg16S zs# z25+l@$x}cjE<&K0rCd`Q>k4neu%$^1Us9GFOxedK`<-GUbt-YpqwM1c7K@a#CIN1i za8ku4Ys=ah42ghp_du=Za@ba!MHGbJ^C|`fS}_bZg_aPY*kkaQIF!!^xVR-^!xVba zG=kxZu$ba3p;Vc;A`-1Ia0Zh|rrtXg)$q z^AV%_$Osx-kA~#d25BVt8Z2htOEoYEE2g~a`i{94P^gaw!HI;A;*(EE9CC!I zE}cfa?Lw-qhi!m@C>ph%gj&h(5aT1mZBQf|SZFS)dIe(XRMiF;YS&s={ZUli3d~9v zDt>*1pB`$@#lg^*gMkL4ZK^|IzYm^XKN#sb)ca-BYdeBwk=p8lfrsTWUn` zI;)zT&NwGyw~2<$vz#vsidwR*zOo_vNoNw)wKiI zQ`>N+VJ{fE$rUikJFt4SN&KQbU~%Y%)d_(cHsl4ZjsqPRxosE)F2#E+GU3N}Cs>?I z*xJ_nx<~+qJGt5Ug>K)r%^rYfY;_|)mdns{tvlCOTm~buTt4jF`B-c%T+_7`rRqMG zXrr@ruiWMdhjF&C`(2Hi2abiKYyQUBrjpA?5Sj@Yq~N`ex8nz;NyFA#w`#p9K9^v#$-AaUwOi*&Eq3$BGvhbDaCz$XnI1%o@hj;J zEpML2g?0b9=;qsdG=D+fe6d0=J|oUxFFW{@p7kQzkGhZ5ZjDkENDZg(mA7_izH+*? zqoV#!)*7WOrC@mpIcqQ}Zar)ABHLi&qE$gkgT|#)$s5Ej0k31)`dqT~vL(@-5sxmO z)IPj)Fo&pV_}1Ij;kN^DbN96#F2U_C=q|UcofjTF#co`-E!RQaS7s$y-x}$@;qQ&( zZ2@c7Z-FgK8iv`6`GnEnl9J>qQVd$FgBBxe_iTzSvl?TM47YW2f`Po(C@JiQcWac9 z-tUJ9o!iDw5VvPg*|KmHAzQ((_3)UzNgb6dXV^4waznhN%uaQ)L)*r-?MzX0w^c{F zW6CoChg(w_JGQ%&XX<)#=94JUDZW=LBby1b2T< zxP%W2Z~=F3*|KYkI}qs1t*WlEVsoq#Om-EJOAAB+KS;})nyg< zXa2LWH)}?S8YOF`en+^D*eu>vC^tc^cg@iEf1ub!7TaB{8#sBs+2r+P?Xg7)lUA*c zGO_9L&4jYRm)l0|cYFNvtc03hW%vrW=k+rqT(<`@DCduoQeC55pd#>sF(`iBzRT|4 zPS!ceA&K>Qr+YqXliRXPrfw}aIlm-VcS)~h&We$BBl70EGdgpf-Y!ntxTWk|{I+zT z{8dM#PN{d7Z_CYEmFsAg>8De9v~u#4OO*F$Va)Mk8Z5ocqSkn~;5LT`4vF&SZmH}g zugof!MJ80dUguk@yP64< z@b__N<(1W>*^~Do>_G81Pu)aqC@hR?!tAp3GQR>R)?!#g)DXnxT&y>f!+X~Lc&gwgtth%GiY@6nPa4$(U9YaN~lda_SGm-{rKb#Jkh{*2(3e{70= zw$;tB>bFl>;*LR6Uo2ZT2yg`}RlX?gG(lLI)8(nmNGE%Q9eP-x+U@*gLiyv@Yva5p zDR~&F8RfAHw*?!Ei9RTH2W{`haiy}92dQD|C)n-bfABkC>Z#&U`%>gL!v4OJM?`#W z9d|t4a?!-#V%fhB{a+QWF2I>u#2;1zm0CAH(mfU>sX9>RrX4ZRzr<=V>*MsTEN3aV zMMTx=V$Uik)>E4T`_QlXAJzW2(X<9A)MA}lv0p&A^ps=4HdmkQ3HLet&mF3&G6nWN zHl0RsWC@;k3(kTyTyn zTJex>#Fiym!{au=JdBO(Qmrq4JmJ;%j1R*1pKHN*p|1(Fi@D7|!Yvg>h7MLFbXHj~ zy`q)_03s@m&54TvW8+hj5>6&3MyZ1HqbrP(9%!fpl$2Fe0p*pYbv2FkwdG309}2eb z!pxX>asU7XlNSmjONxZaVdOz+OhqXqsjQ-;uC}_qsz(2pqAZw-+U5WtrM*y;ww6eg zCC(7U<1`ko3n%+aVP;G{IVu3i$So8mcT^+{sZ9n6+6&i({-r1jrnRv=0FaH9gwBF| zCiE6JMLIqhoFDZ`QwboET3H5ER6+i#ud8XOK2mAAK$tq@eCTF`_Fy8=rVNG;df~dz z3dR(is;UvCD6gO>H@}cqURqpHRzkrlQ8Yls(q=|$(dmw+mbNpUXT6kVe8Hpi;q#24 z3xmV-k&9!OFOAY=e156U;s4e4fY>~l_T0{nu2VhTy{CRD*oi5RlLi0+ZlN7Ggvh`0 zdMY4YZ=ru>^JHaYa@#vkb$4}~?E%}tpF|7sxpL0m(n`ikLT5uhhrFGXdNOV~n5&qZ zR{-W06&I8io}6`hT{Tc!UsG4r+|=08z!zU2kTmb}JPZtSBj~fwr)YPsA8Qur=;H&FJWA?`%8U+au^c6Ra$Q1!ZL} zk1{S@xp;Zx+ST#vV-pMgE>tPB!JJ7&;sHP%8OH_)^2z34QVKU&{$yfCR_bzaemsoS z&Z22oL689}tLtlP8i59_pO*iFKW6c#Vw`N5m~4C!faGOmBzu8?PyXzvxg>&!2E~cB zRX|N$b!}x6zoEIl1{<_MpxV4Ywte@9C*4dXg-lOMQJMD#{+mBc(a2JY2D-Akw!XBn zs;RC;3JL?rCuh1CJ-yv$ItTg%=g#&8gO@H}85^It`jaN%(yhLSG+-pBAtx{NLt^sD z)U1rO1aNLjUTlcOgI>s*N}#&7s-}Y9Sl?9Fu>Y6h?3fseHUOwljAKFoOG=6y2#HFe zaFa5#)1)DB1&T#LaZy=e1+M^#GSCNo^k_ZMh^}im-QL>S($>`m-A2F7@>5|;=KbNv zB;bh*hAcjg=t-xmq~JY0ql6C0$f6k zLFr`clR#<;5RzqW$xV&|=U4>gDrRIUFoG;y1L~@@(3@IR+JNR(Fp^5`0HV7WCcwq> zMui9y4FCue4GP{b2D&xoG96p;(YPir4%_|UwLSk8|xZMtqJO_G8n*; z!B7E>^*rg8ARtFSKXhRfyfk=aWL&zJ0p92ZB_$6@M*nu_-u=5%!0(Um-&F%GC4tBNht-$#Dstv`#_$CKCb}6dKpkvU40}n zYB`7q3*&bdJR4yZ4@GgVhK_U&m4?dn)C)J$`iC?qBQjAX9W>N{iko>8g;_8XSzQ4Tsv#UfnGiT? zh?8#w3_BO*LZL}QnJ!FdivYlo0s%x(BvU&bw1j{U!J|+^zc8<$B)APFZnj zQFU1~-m*$nuLufd2181_+uV~b(+ve!&p=;0&B*dR!bn3K(ZK*GI>6}b*KPsVuL5-1 zOvYAglf}z7m)6b(IS9r087Rj4&YT^Bu-}g~ z6&kA8A3O_tQwa7!^vJsegI_Cm`ysB87;Ec!E6w9F0ci{r+C{wCZLBDgFZwaO`OOXO z0i8_&(SSi0Xr@h;tSmY^TeN_j=OXE1`WH67*Y&pT7huR_cQ{ZZghAPyT)KxE0K7kXL0^K_RLvBVEMPHaNK%8n(Nl|Hmukchmbo6Kq7)gurss-vB zPj~PEEV>i)B1bH&b>!y#weQ=p#^#Xak-{}|W4!3X{O=`iBm#ap4yyjjp{!p~UyCE+ zYZ_JCuybeHS*j-;iNRR+FnR@N&UW{kUmAvXQx?2A$>`UYJ&3q9H7QQ_obsg~Vk`)5 zk^T)#s9XX7yft)hu*P8K^hW|_kbZKqEI7xTV-+Q)_AYpYd}rE_2? z=nOJ75>~jS1Lr$~>3xG>tv)zD!SJNtpsQcKJ#jty_Vxc_nMm)NA4+~NBu;M*4(SjY zq*C_J*Se)hsM?`v*HlK!SjuV4SG=H1d$SNQrG5Pa(Z9CI>BlRm@jM_-rbFdoFmwaR zoAp}p+zkM#qnAff$IvOvM_Q)8f}=+l^Ou(dfzn)8xeZnIvUW0933;izAXvc(FMNp^Xhqjb-DmPL4 zTc=|#F$(>Tu@?Egx>mL*->akh)M0?Q&@m#hHcY6U1tr2C8Yo7PSlmHT14WoBq-Gmb zavI;oY8BQ6g4hYY|NDqxL_7Ya1?5;;Ja&!EUGDZ^69!uDZ(V^u>% zi>V5E-!J7^Kn1ZJ06@3DZ%7j^kP;x0cSuqdRFD+zlNsT=L;Zgc29YGaR@X$6=R ztO6zhd0~cW;_LxUUp5EMPhYs;uGF17lO)npJ>o&P&7jA|jqE_T)XJ)Aj>qmDP;08G zIqB3%kBUUqwt}JrD>h(aWd*F-wKH&+i-*xL#r?L@uMqd-h!{bryj)TELvPXP2ENClZdsSIo*@09hh(`)9|m| zSY^jG83wy9I@>k}B+46p7>rT^eQ{E59>?52SG>XXS<6efm31^3EDdtpW~JU8&^Kc3 zKNGe7k8Oc}UNw}+58M61b#t3_5U*k9=h$0n-L6V&JzT5So!q}E*~m27VuWP`2ClcW zdmlz-`44uyyl%d~RCY-cQTah}+jHx>-Hitdj-3vfZ1oz-l6`!?Q8&+1eLVb?`lQhl z$qVNXfyn#T`^VANj*ERiCbuLcAS*VNm)b10 z!MzQPFWC5MXj917ua9rs1DqstcBy$00(t1o&XL2!b9F6i6hfnDH*r|SY;)ofvT}2V%hDjw#Xs*%0QTU`5cQ()S08*-m;U?F&6)eJ)E{V; z-B(-R@w9cyN{_hC?`}+LzxYz@BOgKc#W``VX@Eq{RcmtalVMw~uQjuEysW*4b#&7a zcboI47OmZFa^6gWk6x0sQ)5j5+3O5-F?}C%TiVVzd6t-VV9vk^-F?x2T+sqG`nB%*(vDoPxzD9ky z@?LRmljx5HZt+^RAxo!scEnLH3*2u+W)w^;)l@R7t#1w-&U9VNkwh0S^4NcFz1`We z3Hu1{7;W&hiWuU1LrjSBh=#op~o%h+|^ZyIf<)z3EMkdwr>Ya)_TL&N&stDZE$rm0XO;aUH15Ga*^1? z02XC&HcyV&1(r_1w{6?R^L}1+e9wzbcEF#v61FaSVFCWJX>pI=$A|WJ(zSE`)Qv$r zjyK-oDk=5a*g&$@`sG(i>_dZxOMLiNVXFUBj>Jl={UnD&f;aivYZ2>RS!-QlwYFv) zeJ#UW+hZ)zV^iEW;n!=tp~p9u_IQ`pGBLhlJS)auMsHK@N~O)l`tm_%ewR~vr*5$0 z@a1U9^v#Z#I_p(434DzT)Y?Y7LwQyGlP@k%8%EQJYh^We%B@?>vAtjQ{C<(g$)=3g ztO}Pc%Qk(L{CstD;uUKbW4Vq|OF>bS`{0{@hPKr&zhuEP@jgDB&=sesuCUqIVRt5( zQ4SV+Kc+d1zNjLe(j^i@@1>}3+WTw;?%EQmBFU?c@2>udX0R1`WBzOfJ6mt3E%_%NCd=yh4|;`d|9d-=^x2Whds{&B$v+^M-TR;z4df+~p3%p$(jG*+%9V?Jsh3o|Uz+rZ9eUh3sil(;5SKWG`sV~O+(8CRF?M5v zj8)It*|}lL>fY+^+@o;{jxRZ*24%{!ch@qN_Htxa5shipNvi%&{d0YZE{fItYlC6i zDJaLPMB>=K@GheVNsXn!4j^e zsFi@N(2;-$9`G!V+6o8I7G65F5`}W$sl8~Aedti|z(FEGbKOfeuc)HH z30m;nB!LXi;T!^fB;%PIO%6tI*|V`DCv<4S-)>K|&jb>NQ(muS(s!#5nTpln%G$tR zZtAxC%Zh-6jXc&1Uo^v9Pm*UdyX@T8vUE%ksAcvxxDU?P@7L}0UGw3==Pl>Z>133F zsk;0T{w8}(E9a8b{rs)97IFW=Wq9`S=6%Ndx1z|Y9?HR&PD`a(zzAwk^^)WIek{?A zIKzvp2s|f)Sb-smuX!}Emgw;A@};bu!*QiONlSO6Sf-R|kZeM{5*}Q!CobdD^nBMJ z`OVITowKH|6qc@8ySq16<^84;OQU3%All@M49anJ60={A!yJoOzqG+8K1T48u-Shx zlf?EfUDbgx1Ty$A_+N`R@9|v})v||LlR23hhL&+f(2rkua%Df!a09tz$9unK z5ki@;6r0uZHX8@ovHw!-B5t!aH~R0Y-y&FI{;xCMKS%R^PL>PN^{x!bH)bH=mDuZk zbRwMZr{47Hb-&Ze#SzlB70y@t3?0982xJ#)^ykC^CVg4ai%^dw@h}2yk?E>vm;hxr zHkD0S2~)pSoS^#gM1mNpd5E@>Nm~S4Ha6w09qt^A!LNVmjgew;u&~&r8JeRFU|`(# zy$LkiQI!>{7xdXvC{nH#>WkO~v|!2T3)y8%7z&f1$6A6p>#bf-j_3%e|NHarFH?V2y8eIdpC3zOg%BSo^~&!5mU<0RwX~A-V=Qh+}}OI$fooBd`ci+}AgPr}eHXTo$VzA$y9)d%_~in-14axmFFH zH3dE>Qr7qvsIGtR|NG8#vRF4qSMvUam=IdAlIPc{CWm@WjeYveGvmBJujC?qoc?uj z9g&qtspoH^1s0_F-qv8WI0^Et z4+t*W?$8mtOeIVV>-N|&;VVHCMFTUZfr5#=%U@ov^p{LLls4fUkxm;^Y}qHKfa_}w zZdxB=qlBZn@m21Bz_nq$Hy&E5MA0KE!%Dl~S^s_}^shpC&yndzXJf6ee%OTEw20hkA_qPw>FfGlQEWM%FRn(rcK?=I*atMto#DK9P}S>8 zHgoa5^Qrx~ewJwkbRM+C8>r$)6WX;0oxdznzfLAE>cRnIKXhTwfCNWMx#?z@XS6DO8`bY9N0^c{sB>hY{7-cZZbozCsNP_AQCW9YJ9*uq{PA(6?!Y&L!CX&)!i3?Eb^3n_=~S5L zuD*ha`M-L#wqgSR-QB;iX#VD3QU=}pzhBi6wZFdk+x!o^6hGY9=WG@dYyKblDZ>85 zTt`LNPL$J_YbQ20)z)hj=c{*}DfkaU|3|%_UpHxNnP$>b0H7~T{?>DgCkQDn(V~`^ zaO9{$p&09xS5mZvqdpR|Vwu~Ps*R_1&A!wc#s$@nM%8VrUJ z2$70Fh+9-vl1ulhssz1^0f=u@LTPR-YH#ZTPvgV7TS~BVy||Gf-~t(c@xtiv$l$fB zKlkE<%|l@aN3@T>ZXV9EI~Mj0g-xN_W~eXS-X`oDE^xW1BkA~an{&2d2=zI0^B}fM zK~Z5*aUOB5*(qfISj$1f0f+%~x{cF>F71u%?dxCYA(3RepZzZ6ap}h|@H^F*QVL2} zRYP2^vIeNDj zA_Z(c4Yoiegx-!8Dg4XqoDKj0M4kF^I*^{mNj@xU($7r-gi%$c4f(9GIM3wZ@fIholRS!rJMqO^?s5MzqQK8P6CRNvS{!TCVn zEx>PwyKa9)^OV_0OsdGe$06y*?C7*B>2X5sl0ix=b{sas6+nG&og| zhx6f;m6ue6Rg{*^O>3GP0pYX;;`%_dNl>P@RXD6EA1+hBaPd;v0_#Ld9KLgJzK@`w z(Z*~jiAF)QyaHoNMUGKT9YlSqY-((6XaS9C^-Fut3JxvY4w2x%?`~sz(G!B`LLA45MycF>&|Zs18Dxez$($`ZljPU1!WjKgatg(aBi>$hCpT!kt&DgM|WjH zHWc2vRuM#ElrVE_H0yeR+|fnFU=EfQVuAjF;d6lMLiFRfHNl|qd^5R5eSNreSh$hJ6^yd4>AR&z_gVPq09*Sna)1`b4 z7Y(%l8J7YLbL$#vK=V#MSgVz45A6Ya`}uxh$NI(g0HV4TjS7dA#I}_fTsODo!#yIJ+keETn0g0!D2hvM%B$vBL9PXi^N@lO9X=VkahXqLb+-AtD00!LyYyod2qKuJCAIGuzlL6r5lIF%l8Yk+m1juWgWzi6FJ23OIL1OW{gb)(V>Yx z8Hqo%K(~kwvi3W&0lJEc^!x_VAVLPxY!=ytJ`m%ih&V^}gEn5ep{A|{+Imp&Gz4_( zgBAS)MFl+dg+u|7_|SRNps_WAos4=cfBiuEoZ$w}rhYH#*Xv~`;Bw1vDX7UhQyT}N_Hc(tz0cKg2mCTK_Ai9wGJY5Jx zcAvGDTgWsNafRlRk|#Zy9iI>bMQnl>odqQ~7DQjjP?0YO8rB4t3JxAD>Msg8qD)^%CJ-q&FpqR06xa|4ijANXS#x2WYs4YvX5|!R zGfw3(WFZPk7J^XyEsi^caoo}3bGjENXzvur_gX`#iAcbwGY*c9ap)?O5GOZmQswsV zcc;P(6@{GKJ1PrPqmZ{L1iIr7u}qw|k&TvCG*1Dfa~7OZX4X)$HN^1cfe;lw21+5# z0QIyUfNjGlFW5N|BXogrXgV#9fgr@PgvXO8iu1Eion+e!~BT(DDDz6 zth#22wu1h1g9GQgAfoN4%=b7pQ=wAHjgWQp2lwvZdHVPce!&uioZ7kE3$YFW0BaaW z@C2gbAl_OE7aD=j?m9?_GX;4@JmI&FRhB_;%EvX-0gBcw+8|_Gi*;L5S5G_nC$YlZ zyZ%>5C*sZUc<^Z4&j*xVEyWsateq!7*HX(0P5VqCyZQzP!1KRU@H6Ozd`UVR!TEk( zNo$lFd3ut|HH0H+vYec?wv=y;hSV(dq1qTPfXIr1!E?~p%FW`QXn1WLpef!On!L^a zUGcYDQ+IELEYK}vcME-H{{zic**uv51|*vcIo4chs-ZMD*>|2}T>(+IK{7_E8Zeb^ zP?G02*F)Kd-?IAblTRVTRVpsw2RLXOmuS#=CB|hHrRBx7H7fI1 zXcyLF?GUpbqUiRu5A>WD3`vQAAUvwB-GENv+t+`e9AB8ph3tKyJuAQQX3Rb3N%as5 z9^%!fWoM?d!L-6z&iFj5e&O?og={mCK+7Mpf|*J(dG@V>%IsSOyeQAjB?TzYRY4c$ z^2UZber0D#p2%(K)%s`4BJlRB4G985=-hvK#~^xSVfkb3|F4fM0M+@25wp)MYM^z4 z)wPZFbxi<&VY(N+60!XOC;BqU>a%W;nRNr=n;YgZQVu~B@#RG1qUYTKyW0@63#Gppo%HQM!aYd`E5vz6*LJbf)KQ&o3pf zn{}%2%NOGy6tN5`+++sy5QLkQm6@KMmVr%_^HG)0h6t@CW!wtLr&X1dD7={$Yq^=Z zkn|66fKH!1(=X`lFk5)j5xtNzgBn!xwQnaR{S3cHfvG< diff --git a/protocol-designer/src/images/modules/engage_height_animation_gen2.gif b/protocol-designer/src/images/modules/engage_height_animation_gen2.gif deleted file mode 100644 index 2865ccb11181a39bbade818e894cdd5367f91997..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 23678 zcmeIacT`j9*EXD!goF|xgpR187o`^gDWRi?V#iJt6cD7Us3a$p6S{=nA)$9duR`d> zhG0Qa2RpX;q2oB}IP#tVQF&(OeZTd+&-&K-)_VMBlp*^*d*9dIXW!Sp?l^98Ffk>I zAk`7iK}1aV0);{ueDIB2ws8IW^{-#Q&d$z${P@v6W8v1VTfhGL>+9bZ8XFsPb93wJ z>Q0|Nedy4kty{NtbaXs;@Zj>Jg}J#o4u^B)$`vIgrKqT=PoF*^0~XTL)4RI5F0?Gn zzFqk3x8F*yd?TFuHa0fKWHKcsCH?*VckI}q1}!8dB@GP?wY9bB$1gm6`gC$~GB!5$ z_U+rRUcK`0@JLNfwY0QEp-{hl`Zn`=!N9;^f6)Siv(PiMP+3`7Qc}XGyY21mef{ae z=dTNPc6N<77tE6v!fM~tPA+5*Eo{$UI90RYlKS@f{6fdoXaD)Qus8E%;n>3Mxo>M| z3;FH${7T-`4m@hVv+(f6Zv(gI$$76F5?H(yLS^46aD@DU%q^K^5n_yzyBWHJb(A@-N%m~_xARF z`}Xa{ix-zJUAlbv^32T4{QUgz@bJTj5C8mY0pY*!=FOYt=H`XPKMD&%TkUu4w6%7% z*VooX0SE*F{*jxDE76V|LWciqZWS6pK6cVGgm~2Rg!d^6#s75mDiXatEEM+|x#+qC z*m`<-JDm^m+O=glNO5B-2Xx3u4(HZ)jAcd#Is=M8GJ0rJ0Qf{{}hpHk$lwubclr_{G>}R_yv47 zdn#Ca(H-q${(d^4^IokI@sA6 zIye~eK5OTH?6jZfsSw_0J$`)F_Fq5Cg@a!Jyt18VkoOr+4~HOsKjNa>X5Rn)ET;cz zZ~3zx|Nbo2|N2=S_+fOooBPvd^L_*)i2K*?(1ri;9sHiBV8jQ(&;}O1{q^+%gxEo%FIYl zOHD~;Bwb8Qh>weniKatQkrClEkV*-=aQUW?rmCW>q^Lk#yJofgDmhshX(>qwae^3LR74mjBq+d-#qgm~NVq^Kd`9!3 z5FlbbTq_XVGLFD5(5MlSQ4l>kCN?fUA@O1oBbiP~OV7y6%FfBn%P%NovWkjJO3Roj zl~vU>wRQFEhQ_8UiYK|fqqBpu&a;=(*Sn6gy0w3lKnWQd?H~7~5U%wNUzxhe8M<_r zGdS_UbNtHcM-!8eZqGfM>3Q_a$b)h3uHVTwnm>Q}^Xp&V7QTLAc;9psF81>HqPsbZ zDOzYx{?h$Y{J2=5rS_MD?#k@hzKcSC8irM<7p;zy((^gkTvps?|Cm5G#}k-Wb4`L@ z-T&<5u9O{*E(>f|C)*jv%gK={yI5a#QV0Tfo8-2f^wyiA5-+Qpy&O*GKJZDEn<(O7=!XrI88uT%s})9x^?@R{7y> zJZiR=RM+gOeiwL4-|EizYM0LDm$b-^!}d=zUDl$Gmuf4%j@B*`qD;8jOEmO5HIVa$ zrA!a7-rBaGj<>w|=UK(x{_3)=&3`uk{(8De$?V#N-xcnVm974hTK4h7Ce%CNV=6fWT{>gLGjthKo6VHz+kRJZAW{1P=z1xE}eY{;_vyBi@!id11dvehu!~6Nc z7praVQX>w2d}zC#p!hgGGHuQ=GcvSURgrX8znC6(<>euTGmkZ53fz(VS9$r<&pao*5c)K^N*DZDY56B=sOgTRuzR+ zy{ahb{+Odwnz?3Jde>jiJRP}=f-M)?SF~R40 zRh@cBtLFZgh%{Z#E_*I$W73FN^(*%A);lJuw{ElaBG704eALf(;3OUc=+*>sRt4Sf zNGEDBwa*F(Dw9ZKk|oIwNb%7dHgej{2RUdoQ$BKS!vOJYf!>>~@1CJc-ZXUzoam-Xwap z;Y|f$wgzG~j@@F+0ff6E@1hM!*mMD;NT@3ZjkBTYyVj%5BCKWaF%bfzTB3qk1LT-; zU}FLkff;=yqGpfalbJoH$EG2tNzuq|1yQ~^gdB+&zOk7ls@rah)MjI(IS64^u=N@Y z+0wdX8~&Wrfb1^;(PAO)B7$=W{*TimRv3M)_1!ZI>M{Hn<8=}BJBdmIzi&9HP_#MJ zCS~oKP5h#pQx@kd@Dr+d1*bvUti3qv^?+FMiYYC<>#%)G!oQ7=_ zu;J5b&cX2O0@m$JzToMcwPtuTVWu_mq!Vgng6M3-288?N(Ws0zWY%3EH?Vy`+IpU# zug(-Ecs&3}zuDU`9)%ijTq|eBz;2wdPI~zv64+u#Gi@gG)kY3{KDD7yLb3>jbA67M z;kZKjhO7uTFI%NA6Hz7BMfl$%?WTg%orlzR#uTUKYreQlFOBgh5)v7favijWs;xP} zE}PU&@3@*?y#pDRwa(?3jnyE44kg)Y4KOj%_}MUBH|sg{jc_ykY$cY6kXOcHh0`7h zdoSP%kvD0bk~IpLz$}`e`_#>Hg8Mn@XcnAOucG&>o~iQU&sD&`nuzuJz)HWW?jS&9T-}id$ZPF* z8pmhgV;`X7T3o!h^MjLu{W_~+Bdt^SgZJ;y7x2O#t@Vpi-t<>me5>!>m)_^i!i8ke zRKB^$2m9Hr8wY{vU%P9GC7XAD+(j#v&hi1e@6pBP`z$mW+6p&cMV}%tOWv0JvU>lC zX_r^O3lGT;^4py$|1JHk?^_6URPwUh$c_z|P*;y_xVW98yHAp%XCs)8lLH&GU;W00C3(5fl+eTHnb*TkxW++<5POL;f*n;u1$$xMguoG&hC2CvY~!-=YV|1 zA9NMeN4geqpy_?r5%A;Q?3==22 zX5qe>mDhQE(YKQge1R{MSI%;n_Ye4 zx3<1a@&Cgv<}ShJfzA6D95^sHh4MAlMUO=%z zAi~nE`>Kvg`!lB;le6{b`uTcZ#xy+3Pq>_hua|Sb^s60*?ow`jD^|Q(|65gB)7K}o zhW04L))v7EKvYvETv*^8y->R4Zgu;>^>E7o2MI)N(*$RSf>`qA^^L7=YKHB$mi*@j zKH>|NwVn=oj@2}EPnMxAC4RB>JIP>_5jNwW#>bLesA#HX6qi?n)q0Vi+9M z2ZhKR1By7HfFoo=`?Z79^x=-Fg<^NXueiLbqc)q{Z;k!gIsnblR%NMhXh* z{LX<3CzLBdH8*WdFCne=z|5lX^r`UD-H{g~RjX#fC|9fw1L;4D6+r+ZctrGek0%Bw ztz!rg_90m&M0E!+B%}dzo6bB^P#Q32AazK_VGN|KJKmp$vf=oYQgiGWsLTnZ*%MPOw(F6`g$`v9Ykn#-RdOyaKgV-~TNzXdQM+3IEi?7v< zJFLs^jNtQ2MjMm(h1hY>1bQu7&>4@eq=iSbC6WQ%apDQ-cQMA|!uU4KI39B*5aH1f zl;ybQjF^iwQiac2{3Kp%0!`|03h3fD>ROS&IVo?(NkbWer?Y=a5Mv}xaqz*@X6%Eag=X07s zQ{B;PCZx90(j(Xibv6QGV(nO0a!sIT11y$jFUPw42n64UO+d3uA(8};@bNB0M1?y4S~iM= zK)b~je$n9Hz(yHQq({yI-kbOlH1u(AlrydHr~n_59Vg$3*f`Hpc!fPoL>`qD5PNj? z<9tyk2_eNs65Ii8p9N8GcYwe~O5v?^a}YigKu-XmD~ItXVQ#o$JVtJEUzNCOmg=4d3^ai~`%6s65jJ=j4ni6M z$TQdcM61N}h^0FQ_Ws2tI#j3A*BB4Vh_Jj?VLDjuPs%P-?9rpDaU zz8TvZS6#mu5s0tpc!f1&8#Q){zqwnW7=YTQi?HS(QMw3IM!Gr!yfe=l_)y|Vvhiy1EY5471oSVYez&Leu-5NU`CPvI}&ib1EoG+ ztH?mf#UZUpY|KW4fO9-50PS;5P~t#?)P)9_+y=Q^0drTB5duiV18e?lP}tZ=oWUx- zZV)30=<7BT27Fb@2wI)Bt(iZq8+FzjJrr#%}ouK&l+0Jm38|)$O`7^2Q9PKi_RW zkHEWPMO{%CPLePaX@gG}aYbGkLp{Gxput3nk($Xnkjn0WJo}_fEGopc2g5|j;CpQt z)mHAv$2*a@d5k6vAaLLxaw2qF-GuVwAYk1DLM8x#Gp~_6*h-y8x#fzd%+zkg*HRJn zYuIQPHn5(9ROb{#Id_r*&`xZ`HbkNGe8KLQ3s@&6N{E4SWFsyF^lNm(*x>&n$f9*2 zZpk4x5gRgJV>coSsj~or$Zv;0ZzQ23+*v!iQTx0zq^5^V9wSuo!?FQbfQT}OUqVVi zgk}u$I8u5Fql#E{#vN%D0Cc=U+{#5Xe@0$^f)$=chWILC<`KukOhg$pDfm;HFjn)Z z8<%6w%;uWssFl6MD$Vx}NsOJ8G&Yz(<{UhncOQ9jzQRk0rRa_#zQMX6>JPJloQ?eJ z@$nA?(bhDi=`3(`4CzS3`n|?V(2yu5>ioobea(2v#zE@D<-=^$#sJg>Jl{k&V8=!s zywbK~%g`SAes9yOR6H{O^}^7s{Iw0+5r(q?l^mdDtj2|n{!MP^h7T%eRmXW60)a;x zyLUER=_hfJNDlL>CgS`oV2wj;A_1Wtm50~j#djpn=-MR=4+q(piH}O zQiS;I7`3SRRwvp`I)X3S8Hm2n>1Ba%B>|NeItj0_C=#$HfuE=~rT2DHOL$5re-hN1 z4!Z;x`c1{;@0#u=bVkrJAlDl^67F0iwKcO*r>m#4^6%y(^1qRTO9QgZncuG(<#YwP z@RENU@p1zm8Ps*R?yq~XZ~1Hk&OmI?A!u#T3D^A5>#moL zNimSK#2(lBvCGgA32^HO*paZ!zWlJzSkoR->f8T-N8W=cH#c?v#pr=PsDI`RU0u8W z)cw59$AgKf>#*hR*+^WRxYo`!+N2o!TUi?W^R|5MpHLXo6E64Ad+~HYQGF*fuZTFA zSb2Kuhl#Y%rkGTlQ^TKD-Q#y>Y3vo$4m|quI>(B{mVf$wBq>#8f0Xv8Y_FHrna6f% zd`3#f=dI6{$}&AZ;Kd@kE%c0jRdU!AZY3y0?g$+I)%3uHg#=L!#wR}p-WPn%J_os}#?^E1=+(q`uNc9jCN>lU6_3~u#ZGOL#iuC(0&Lj=I;ZKY(h78rGlNOSw-#))RdxVDgqUH`RNU|r9IDwWdXP0Td6#2t$ zfv&-BB(nWcsT5R{{Qjs+A^e^YeKPcv6f(5`L3VMB@0+{yRep2#jkhKyq`BIniL`l= zFzNlA5@X}XPsbPz1dXSxWP6c)k$lOw^slW&dXa4%cDfqnh`aIcMw{4&1@PI}t6TSF z9g=!9VkfTG;+YBF*X6T`lH7sbop7HyF58(9UcTx+ei-sQMGBlqGyT=g39(uG-rU** z%*%=rnAE8h@t+ocMG|>7dNscCwH9K|}0)No`iu zYc1=8dUS-gY9m7WpliULx>40!C*qH4%SG*xWz-50g+K^2K0;rj`{TA)#~nVIAQ#18 zxIl+?>~KF|44&AMQ(nPvk2AJR2r8^h+)`Ez@@?4x5y7B6dk#a!d-A{|d`I@s5BDDF zmfI5x3Eeo-H!uY4;cJDO6Sf@awDtq{6py7oeO~-LJH8pcJMk(`RMov?y~z_%=0Ny^`Ar5NhDM|h?Fr+@sa$hpFqGOV&`K{v zCG})W^Te7;1!SBdj+{dgy+}dk&%d`=e}>( zJ-s0)W1;=od&_2(V$pSm3%ch)iMwBvkfsq~`Kd%kruMG}ds7V6`o`ux zt~ceZP{L0K1kzR9wXs zrzc9H>57>O>_g-vQTH)L2h|Oh`X0$ibAiL6k|)AOoWvFPTg#>FG9&P>#0aml`9<)7 zkR0)QWfO>sl}n8~>Bq>yW^gMTPAQZiEe8hcGOKIA)hE+D%i}ebYU9XoWvgBX1qpxUS90WSHYROF;Le(fh^t1hrL`*6pVWNpKI?y zK0;IX*QB0a0*RyG%CX5Yly*%Iv_5fdt&qoLf~(RCk$hc`@hhh8eNYY>RIT8jBHQJf z3a$D!wo^4xP~h2|hjWM{ggg*7KD$XNoRn|oDfl#Dy6GC9S=6M=+(`3{UK>qKobOcM zj_56d>&-oORtx|#;@{-eiz{66qGNEh?k{T9d6L-+BDzkk&%IelpL@(c4eM)br3G%h zPbR5by9;@JlX04A4pi*&`GoswOQ6$@aO>S-o?Nyu9>l!*8h-5c>k1Lj;pvTlJ{tW> z?p}G)A^tLBvvGdB*Q28^A6kWtN}NB2)s(Z;q!<5L93iTC;NmJPzosINN0_qEhkYIj zQ?`b1N67x_cy;nFxSlW?>3^Zfh(Ne2wd#m^p)t-ZgfUTW`e6fE@9e=Ku)E=4FxZTj zQ4+wjZ?4swd+x3v_UfZMByOYr2wJtHVg^`;H^8nY3-2PS%HbUxWcVZ)Zf7APoh~OB z`Wtq&U{>UQ8SVSY!fV!desYu-xvFq)aqDOIS9RKrq#f79<82j! zyBjdFQWZCBPK6BiJ>kEi!eB?>Fo|jGiHU?fJE>spG*8-OzDdmfFMq6r75vV*DglMB!daL85vA>`UTo5G8+UbbmaQB?22BmX}sllo?5z$iT6X>1d_4ceZu3^!9Xf zx-JQD!!3@(iCm^$y?$eA;>xu49J24CabUnmS3)P8|I+|OtD;{_4a*X!= zh!L4$ZXhqOOo8pDz;;u!GA`nlqg0C5!vLj5Y8WoWOB<@1>RP1WeS#y^+fD7^TLM*j?GrDCKa2EotGef-SyRAChek}X^mY!qu@^dK)xT{T!+ z52t5yQ$q{8QEr9FHOt{3(+DEcRI0d0j5L`-k-vx|lRYoWGg8trJT+tic`5HydGvBj zs_ODCq9JnfnhO zKK$FN+`y1%-%A!KZ<7c>!ihFXVDd$VVYU?`F%Vi-o|r33K?2G(m7s@w0|kOtZIExO zZ|`hbSqE1yyW;n~(&&*XF|kneMS5Zs)k9g?!kj|Ujo}rXGCUfmT4p5X?JZgZds5#} zWpD`e!JMfz5?o8G(z<jmK zU9s^x!oLU z>*#7%?riPt>&C6HEx#N@FK+FmQsI}J51Hb@vwcojR6f#|Ae=3@j0HG|B0w3dxU{^k zridt7V~DFNlhzm0fN)}c8u9Wr!vL8qJZ!_ogRtS0m6Z2on?PvhJ~f^)`{?nLr_Ub$ zZSf_?Y)k+UR5pQ2iTvW)Nr`cyGBOvzbQ6C#YWp&$zr4&YA_=5+TAU!wN*K{_%McnZaU69rA*jYs=GApi0JhQ}|z zprzIkq`aU0<0EYS3H25{_kdIguz}yz%asz2J`DLvA2R4sSz2CS23Axhsfvo~K!KHl zMS0%jfxLl1Xt<_m_-95f86o$5`*?5LA330hrFf=7IE(ME+!F5QmB8H ziqiwl(;#6y5DbBEXHp1f5KzJ@F03li6DeU5^$X#=rBbC-yDi8RTsNFZmj?UWJ$0;x zeRc4vz8zF(ssjqUed`{0`zD6sdG{tr(d+of)wldp{^|>YvT2ZL%wqP!WjwQlRfMxD zDOyTj_|4YX#BObFXahyGyG3vqx$2?ek?OFOJ%!A7_kK!Z5e*%U<=$74vMx1QL{y%a z#pPTelylpl`dU0Dzo|sE{6`X7^j+#5?xAw}u@sFlNY)U>=$&a826rDkq*?Q9D;ZT``$Ee)C=0A?TqKucb@Ka*DvGWw3UHs65$Um5**!#ccnDwWgeJw!Wi#Ilh63xot>7XkY~}9v(y!S@z&o= zQ!~^7L32a+EMRD2vb26mOd6=1D9$T(aBBsLi@?QB7yxw8<&~w4RgiR3bqHW_7Ov$W zxpy$X&ue8Nmsobs<9jm`MaL{5f$LssoSrg{hlC%kJl<2Js)7g-Ns> zK`PwMQ~EAJMJn3R)J-axGEE7+b^qp_u=~HP^ozH!!%ywJ2Y}aKfb0;!9^%ROr` zTnXV0rm7soS*fYjz-Qg6G6>gN!^KH4Q;9RcS`jjyX_p@zJzYjc<$K8JiQMyLZf<@7 ztFS1fhzX&W+WGn#&|gYk8SaN`>#HIEt_E*;3btqn@)#Z)Ul}#=<*0dnZ|iP+^={7+E}`R8&MeyW0mZDN(vE!LLIZZD?`|?)~mi zly1&U2~hNAd?`^YeB-U`_#=Dhs!Pekh=k!lPxR%zqm~-|3S1c+)lv{<2r5HC%?h9j zK2NF!vkE*og<+h(+2&>@_f*b1BEoSKUGge5g`TL(v%M5B`2AN7dtFvh!mjiHgBz;+ z<#8)dL4@T%9i#~x5U5mXgC8eZ0>uL+Mhx_p!^FxjWWmSsqViI_NJvFl$6iYh5X8tfMX2RV}6T=^2k7rzj_i~Y^%atm~UV5u?F)j+g9UhGG#)j%z zK~|ne=cQcfznkS^#EfPl1D}{R{wtv+y=*qHARBHe;G+cBEZk2lrcP61b?wRqg!`0S zb0#!U1@1(3;FsCYY@&)1&-tZ1V@8@h zV_t%Oa8t`QX1;ncl7d8bePuP@Pw?Vi=J=->G72G#=tKy?kGw!L-f{$<`s_WiJoRBf zhmwyTO%Sq729LyodorLsyLJiXB%Rn(R#lyR^ zJn1t`yR8St=HewZGd|E*ZJVxSZUD$5`ujkt$ASZ+R(w zDY8mZW!J^hM{Xp8Sn`ffNd-Dqj#7VmdOeaZb?GFF8?f0zLg0%Uw3@$}&hmgv6APVRGi_Gz-*KVaqZybTpx0u}a>&6?7T4=CNxcoe<%-ln#r z@tTe5591&IZjDJWJmp%Tw_C$uNbb;R@-?aJZ`uy1<*H~5Ey#ApNZ3gqZuj8hZ&r9I z8eyjK*nQ&>{05e=q?y-f$|XI;1e>R6p%^)(2sgPyi8XVM3PE*F8*!NgN%6tGR6KlIT-z6P#M{#Ai3?4fLQgQxSRc$6u=jkcK#`$o1=CcB zsZk|kXOw!wp}@;+%oz-;DD(4H^}a)<-=#g0y>oES>+%BrKOI-?3G&?DFcQ{KxU+b! zVcOH0e=bcTLp@%>)KU_O45*YhN|t=>#Ija9AJXWy-V(oIvX%3)(LoKK^h8L1IGT0& z+|2t||LGNYHJPR0DO;3c_=jovc;A8uZhnBCzkQ2d>3W+42?@(9`iZuJA3ZkX>yW;- zc)YXtPopnxqR^KU#by6mI8l!6{hlbtlMlwmJG&)=8!|{4An8|yBm^EL z?LYMZL{9DB_IRq?`q(Sv+x=to<1hF3&P_c+N-%GF_kD12Zr9lPN|K=aEJ<2RBI>Vw zd*{ZoRF4Vn-RK{o_gX*+2NV^4<#_yo5y!vfB!I=Ab& zqCHHXI=G7I96Wk$m&b78l`Tj%>Cxj7T85rn8mXb^bgZn_(>OyhK9!i}Lo>=gHylDX zNolz*l&Nqou07d5S=a8Ok*q9H z*?(|rNB9n*3~!&N(CsB9qsMsBA_{X<>8+k|t}hO`JtiD{^T$!KLPP~WWrNHp-+@za819?v7d)n$6tW{F>UNk=Z`7O!Csp_8P_8VT_Dz3ib&1?2;rYx*y%`1AP zG((##gm?bRsxE@uHPiJcdQJ!h#7#2?>%l0ATE)BJe{4Q8V`JmjUT+6YNQ`MezAy26 zw*9YxFYeilpMPpRIT-TuVC-kOJFjtt;cn*e9qt#B6%-QIu1^L@u^=)7LT=i$At%XJ zB3BVbm#8jdH{_%;Br4&T!fwZSbyASob|;9m-PU2db7R~F^<4h#F3=`ZQ_vZKS|hY} z4YYAvoPz0^afKoj(-wu=ga}f2i-gQfi+6)hX5v=uej5c=P5+4xh%Q%tYwmu&$Zt$o z(Jn>7T_*jiu0*K4s ztY%|@lToGdnTomW3P&Oe8yv(pwr@J>w8xG}S}Sq2ff0pWkA4?!0CHNy+*wz}4;^>J z_1%E%H+>jEIE3Ntx7Xy!h~U?!VG)FEU5^3G;im>Ty!zaW`}nx&==~=AxOYNZ9)&9O zFCaG%*^&An9#B^5^h`&2MFJTE;u7ka&h&N0C!CVQy-S=Dj#!cv<1`!HVko;yHe4aB znvd8gfspS*5)LzoGR-gKdux1QwRD#@%jHIK0kN*a{ffP zqKgNAujsgBEv*DUsbtVA78J~Y1ic0Pa*`wkxt+e(IyC((gbM0NyemWPTA_@T1pB4y?A0hFVS>g8 zD0f*abCz&(pMju@vGnib$%gyXx5*9oOE8jkqqn(V$x&&ZPk5fRy|6+U*d5RU37Iqt zHtPlW3a>Wj;3_x0+Kd7=LX+(5U={;?5j13)77P>QB*8PqFI@s=1s1p%?ke9`O*yHzao1i<_y)j#&m~`u$TmaB+M`MX(U`g zE`}H`V%$C(DX3ooHkIhHl+ZQP(v(TdX~1fN@)EGj zNW}`47}ZxaHdLt?2{7~Fr_CXP=)codT*IC;Sa}12;x4j9sn$yz-qLu$FN-xpB zT#nX8jK|T^n^vmScw92+@8M5`qpuzNJ^V|({pDqI1ZtkKV*_r!5}S}&oMq_~tK zH}r5KUB3|shabY+o6^>?SeZ<`;lZ}=+Zd5LQ9Ur2@T6D+0$e?6LSQGEo!{D36vL6DIS!WrZksqOs zU7or%EI`p7AC8dLF<%+AALFDS!n40msz73Xej&4%RpMDvl)n@+n5pN^tCpyG2-n@n zomZ6%r;hyBaT0F@JMIu}X&(Dy2*;iAaY=sOENlsD@GK0TxRq5@HNYA!sIIM+tzFCv z?dkjHNtox4m_>O|;f@qQE%y%C*Z^$0j^xZm(07Uzw0U#+L9<60R^JFbqWC zq2vX@gfJi18Eq+krS;hujIxK0-B(DBlO=)a?AY*I9ebW zL5~Uxqj*rG=#fwoJnQ$RV5o38j3D3(g~=3I8ym4aY5_C9FtSQ`$gS zq-mt1_nY0R=CD9e|!-L zWJ)x}BPu?D9+pT+f;`(J`c&4nczyI_~4G7Qn628e4Z!+08 zDj^Y82qbz_ykLl{!Vu3>Jwq|b%gQOrrsn1gQgliqxKUbmtpg9siC|S~duLxSUJqwA z)T-aXopZwozlovy-<+7ddxJW4i|T2vIqPd~;k^>yT;UQI-!|W+&0KNGq85sA(FYcn zq%Dd|mUfLLqFMqi&Gz8ev;{0dX@+0a5YW~I3(Z!9jHidbbZS&wJQNq@!2k(_h^1W3hjUdVKRdTLhniPF^^`56`pSyo0~T|&g)1fF%BwlO z0-c~gu5&3>C$55lTu8&cS(LezzLjhgVPa*n^2Cd-KP9U_&lPjcWRMFK-iv4{RjatE zT2)rTF6X|8WK}xVAdM>R;(WCKV8NjO@K7Nke`IBcz+1$3Ic2#Zh=LVfQLw@*Vkv2J zbm+=CaLQ&^2-=fss>_A&}1ENgKQ6wX?HH?c&Eq3?;S0}F3T&uBC@ zgRSkQph1%f+;lgsUV(6)u62cTuskn@>Pv`@SjyX6ZrAO94E|=4`&RP$8_r?XCxH^> zOaJj!5>_jfC6kp))nFM*t+JSyjjEJ)m73Hd+TpW@2*(gN&^0s?H8wcRSpxg5Nib0M z-pJjVs0Xt5?>+kEp8tx)xq5VNnCyPs*8x{sB*NO>M2kgbI9J=tNc4uov?y`sYI2oL zYO4h0O=Mu<09QW^%c$GBA>k&azZvFwbz~Xc-sHtQyNf#An7AJRP*q{6Q3_cq#id0h z%&N+qrOj{QHeD_7Jpit#4whdG^bAQY?fmsC)N2!0uV21(bMp3$sg>25d)I*LjQMv_ z(08r(550cWa?4P>qzkIXRaOJFAS`=phFg)Qf4%@<_EQ~L#NAI93UIvLUakY&{D*Z} z?E2b zt&fC^^wi9hG&rJM9aJVPNG>jAlvlIh?^iXF!kK$S_?vgOQQ?~lovobSOMTrvD>sdI z*U#dI@c8HFg|7}|=fL{Hyn_5(R!%V_FDn9R>d02WsRnnmm5uC{riP}K;#HnS^MBhP zu0`T^i%Yu$-&3iE8-AGSsfUkgD|d%y@W1ui-~aVS^PcLz;b!B%bD6wF%zm`xKktsn cSst1UO;jNR&_srdOy2d$N>B!b?{jSYU#nEc;{X5v diff --git a/protocol-designer/src/images/modules/engage_height_static_gen1.png b/protocol-designer/src/images/modules/engage_height_static_gen1.png deleted file mode 100644 index 1bb216a5f06ebd98f17bae5907beaab89748ada0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7597 zcmXAu1yCH%(}s~i2pT-W-E+7*!JWer5}X5qyBq`v5ZpQ3g2Tb#65Q>85L`nbxI2gQ z-S7Wx)$UBy)K*Wu-P_&sY^1iP5*{`MHVO&~o{F+O5CsJdA30~kLPI{^)^9l=hgUAj zAKj57>i=#n=KeY4gbA$o!Q;If$lSx(4Fv@&!(DwuOI6=Rom1r<&_zQ{O#{G=i6RTe z{AC9w9Y8^$=2nrH)$_?d%JKC{*1sN-c1$kWOPXr%$j_&QFeGPHMbdi0V=87@k zNaAu|9v>$r7Pl-?zy{a={0nx73yKON(c>T~Qb;iP(eQ0bPB#Nkb^H=b%mYr-r=L9+|w%wu+ZC*!37;gDu%f#9) z*7>t`c$gtnBLxKoYink)ip1Jec8y|G__2zXjYf?|jxxSkGE~+al$HvEFZ z+YGV;v)B$i#F9rmw%NQ-Ry%5HPSv%W799@etDQMPS>tmC@QjQMH+2ONsMvx-j0{ag z9Q#S+_raBQ{l~bX0gK6ltM&DD8RY8Nlfr~yTyV{zLlD)3p}d9$-s`t-_x$Ij`$Z2t zTHRyoRGH^J?Kwe-C7PwvdmiWPNVAD`V}>ZAUUITZp%xq@4RSIa8+!idDZwLFNL|4l zG`?V6J@t{1k>Tmlf7yiv1xOYBi??x?vc)09L`1gYAkoL=BQF`4LG+w53#ifP9Wd20 zRvwNzK;ogzRwDQ9MUG}T?sIKtCrw0bEa_G7?kZ-eZsf3zm}Un{sZHh%(ifg$IX6{- z>FO+*#tutzAG4=23b{;Yn~J#pk~F5&X_kr$!m-6@Np$mNg4Jgaw=G`IgcQV6(a-<} z54Ls=OV$5X12N=cfXP}acDsIp9j_yvOOmMCd_&<<4Y(3EAX ztc>k;j|`%(P#T8RNjV`y*V7J9X9Dw5SOLbwK`1J!uOl-P#ODt6YWuTi%J^Qe4dWR7 zAEJnBnj(h}l(DqZ0)4IY$~MXfXz-goKOGx`$SM^ff{q9p%jOtYm5;M?D!h881c}b=dKa$DrRO5O&-zi?rYNVy9 zsXqg?m~eypA!QG_usn*()&jx3|pIepHi1cN6NvN z$Bze~9eni7Jy5gi=L|^BxlN|qsO;Bu0rr<%?ANM`U7k8Z`X1nh_;L~J?X&7(=_RP^ zy0rmmgBrqy%$Nq3>tYcg1;3awabP%l^|A1f-BO|RU?Vj|qDu$7>KD_N>VD66u8KN$ z4DyStlv<`nb%#3X_!0g$fTyJ;&=F5pZlm2DH&Ij_haMRqpqG2?a+;f3AKeg1&IW|k zL$x8fylnk8R-&zEwyrd$tNQ3oi5xz-f0D7AT?(*Cwbpp@sSyip-e1`+=CZaf&Uch~ zS@%!(%>3KrUzY)6;It$T@PPYKxPVaO-$M|inda>HGG4l_Cyx1OxvkqVP;VgefrkEBVV&~k2*fwvV4S{NEBa+ElZBbW8Kt|Gg6;Vd3tBU3r zuXI+t>0`afoVRELS(wscFWDu2W$RQ0NG%UM2hd9;-2<3ZhT#6KigkQ>8=mU>16PbF zNFi>WHM>IS~v7k@;J5T3J{`wmaa??Y4m7%ZX z2@7y@3rj-UBRXK;JImcXteOC6+GCkg^Cus^HlY201^R+X@A9&(gB&?#jhO_!tbc2C zqo2wQ@3a5DqeOey8r`*_%ky_8C3OG#ymXf^&$O_%BYx}ne>tCnxC6~UV;T}*+|;HM zc%PnHVKUYFH_zPoF_?7^wejEl!^dwCtsgpSHn(9tn_gb)O+0Y4sF85L2!>Qp@Rl!Kl)<>>!bXx}tPt-KrgidH9 z5$#Cu*~|xDE4)7i?2;oFl%ikEuYq;C#yMQ$8^u(|VtJ4^B^}@BYHm^L)A(A1g>5bt z!~(cVc111>PS=>6;M;}NNm7Ed{5B-~ckX+ikji;-F01*h1A!I`rak>8T#G#gRAe!w ze&i)xi+0A$<-a-J7)_fg3krurvEHV>8DY1PPh-^M&&I<$ z(rWM1a;=ouFMhEyMSPM{pw)bqS1fVcP`(^@nrr>4e!wXiSd#-V0ysTwQ3>(ZWL-+| zLSFQ1^fUE=F*|<6XV{Gw3N%7q6y*Y_r;UV(G+VBH9{8MhU*Opx?--}US zzrJAOx2!$W1O&cSmuGtcXlX8{D8|Jh6K?si(BMxh|pnlSs|O{Q*UDthNa z z_=|T50V@|fb@9$A-%`2a)wp{Vw^(RA1p)_qFoMA3Qd>nSScC^BAFGP&SGduhPvDOMu1w@Tv zmph^_UlWDS{z^=S=)2btnfYLwk3X|Z{)@o2T*YRP6Ezv-K_9YWT5eWP4{GpHa}C_%3<8`)w8Jn1X~mT z+J=68!;1NDqE0;+GIc+r(oWQOqL(CoI_7b~pNxY@T7nDO1L5!5?7ZPlsDLHSkFq=O z#Z4UrY)Ik*2Wlz8ywlVbEQ#YQ*Ejj)L+cK0O#Wckl*VXzop;M0e;AxPFs?4$Z*dYy zBRCh9_jrR0;GzV=(Qc-oXaAv<$rUloPsG(usLIpRl3_aKpCuuXH$j&ls%U#aA6M^j zvF)yS8B+ryS0sC8liX40L?Sw0;cBu}P-;EA^^AyL?Vb~3r>4~O#p~V`;1cS*$8$$- zG-#r+QWtxElqDLKRJS$^BK8Erhd85X9j{z_e8^(F(+DZ&tBfn=59;gd(N)E2z1|^< zbDC9kE3&Z(aB*v@*U_X~9v=X3b5~baBcQ^PS2@!IWCXij??kBP z6KnPPl|B}pP?d3SmLXr~%Vbl_5&SA@z$WMcKEOLSI{=dg+n{d3m;5CEr=435MfbWVxzc z*rgcJWSlTPyo0uf>ECWr(eXxQDXAU2624U1%y3qTHMiilU_=S?+9~b(EE-yBqs%wJ z;zi*L#`Jm_U$s^1DR2KVWkL&qvPom@h4+UmPcyRD=g;mYm1zO{y>2)8X)BsGBU4dayKTRWUG!y<|n} zSE%ui(<>F2=^kH1`Lshw5e$o*6~0)LhmSqjixGo~NczL)TY(hap}GE^oWVgYvPPdC zJl|OI7da2?Aj2kt#EE8Hu#ZQWClDiEb1T!Fitp? zUaqn%2bq4kv*6jf8*W%`-}}9^VZ=q`j2YAhEZzG)V=7DSrD|iBcihHcV!wjR?0vt& z>4|mQ8yXJD35tGnV$lzKMwLi?K+}|Nb}X( zDlcBd(>B59)B%VTijkn&dBn_5TrJy(ODd9nQW)vi36T=SPAu)sr;C)qL<&PfJ#ttD z^^FhfBk|kB-tr!c+6MN;HB4TFnNXIbv2`_|iNAE8{e(62NNvwY(eGhLuVY_OdXKmo zk<^q@cadJgFTdEue*Rhich!HnWB7-^T?KZjs0WRC#agz)&;`bETY{efVR6qfodjfn zCZ7qGDL%+|r{G~e^WkmA6#Pv#{&w7U+=OqScyJIbQn=couMNir$gK5E>u{hLUCS{T zMX|89tZWxziYUzd13yxDaVh)DEHRxfdg)}jvs3~)Vexy5m}D7?Pp9Nk0P%-Ex>!D`R$Q^Ap`jnu~w5dF{`9|+q? zc?Z1T{ww0}?=kNK1YH|AP*3UO^uFRft$cQw<+^q59~oEolJdH^G@*Q3(;hD}r|m3L)a#Us%Vvo-*`)^@u5rd{2y5r|k>dije^WGTmzVVUPPAmv z{>c8IU(09dVDX`|O#u&c2e!@DbqCR5)UJbfx4YdX1q)1%0u47OA|?j`WOs`Lg$FBt zGT>tK*ACtEKO-P|s8leozdZg)Y?F+qi*2a>X~kbDCfxusJo%F3@fz0NN#6p$PwH7}1l^}_F1ciOHI zGo)v+l#Ls|6j+{=oIC*l)oxYlk})RR#GNk=BvCW%|1NujNb(TqJO6XYH&wxjtn%Zu zB6=g7pHKos`X4Kb#cs5`k_=TrlJ)`g^_+h2^b6i@I2s@z+B9xbGK_Kd^C`9tg`9rO zSCu+u&4&xhy@vb?(~|bEeat%3qCwTxTyg20Mnd?VoLw= z_+ehNbi}j0p^11;3%P0sJFGUd!mc)zomcjP)aCzfoL{fph?X3CtMEDW@~r54EqDDo zK7$}|G*YZC7i8-lG_2j({v8p}yVA{1o|E$m{mYjx*7b|F0^8f$P(2Qt7ZRMDqiJ0k zDJdy`ad$Y80#;1vRLu5;;f*5Qu+OJsQaXKgv^=kCLIqxKa#Spwv=tEBLLw-GRKz3V z>;1JLl0?<}PM2u>BB@m^hX49?z5o1#4|$14(cp(Tb`N*6wk{d45Os%40LXs5D6`+3 zeFw=vt!qVw5VA4au3o!Xe6W}7hWk57#_MCOh$K1V_7s&K2cDTr-2qnHwERmU(r#!L${{|@r=hKr1pav5y(#v8UN75X1cZ3$B6oR$qKvL- z3T>#u04VpI!-sH9M*MF*BcWj^db9P-**v85J-dA>k?Td@JBEILSJ`<)4cDNmO2Vht zif0xZ>#$kVW@gsMy&h)k%I+&h-y=J+6V*}kqWHUVY}aCgwx{KWctKRKVwx7us1$c2 zJ-j5{VB`n{CL!~Rmduo`052~uS@eKbZB&n085CM)1j7$!WMGgho&M5j^nA{eJYv_l zZpFO+gYcQD;Ui1aMJdJ;MbHD0#Pxz7#|{}n#~c;sx5 zx*iu*1#=DB1&8cXb2nZ_ruAZ)m{&S6XZXFLQ)T?}e5&0ljG&@f;@DqnN6qTf|Tx<&9#wP|sLQ*O@)?gKH&d@@VbucWUHn&uBN0v|6EY}v6 z^jhO)b97byc~yjOT3rg_3TGX<(!Oqu`DY!3)nC1Ssdajy_PZ?o8nY0%r~^;;Ap5mB z3*#EMG?GRVkiRdU+}Wk6EPAj3hXc*`zS|4{+WvpA_8aPC@Wm-bTvMDiV0@#V@>64W z86zOLRdGZL)!fed(h?-qoa4RUMciC-v)8@uvT$_W+A36paj?I?#Kdj)-jrfIt=<2V zEs_Qr*mvU;=eUSa6@&wHbEV&a?32M?zYzT}B)tNXqKbZhG)Pl^O9<EGO9ojh-@vc1#_M8+I}hTLoA0ChVqduszQ(5<)Ra=r0HB@zj0 zB0Fy8!FAKx7)V&P+LFn;cUbKLioXnqdXIFO*)KMlY>2tPWqQOZdnR^B&)P?wLK#`{-Z_#6 zwgR#@>97Av>kyRTK~-4o2AsEqua7DCiDOcX57Umnq{- z8U?UhDzWDs7PT&xK9z?G2bjqaJ>RH`2_iAW)7>|Z-sQ1xpPv&+ETZr?N{NEVh1Sa} zoZpOuDp>VTF~&)PJ_~ev0t9PpaYl^Gpf;YU1e#17F{N`CJzi6mzWzBR?E58_3Q~y_ zibX$k&IV}im|T{9SI*fZ#T+!amU@Tg{csP_Ly{K1Sm|*H$C$zGaRpt*)MYaoZXHS! z^BpTv5s{h}96|e-y|chjfXWcH<=Y2|dQ4ZuivocMuV8zr7q-X)97C9Z&3#fqd2JHa zcwJtGao->Vp9N}MhA(?~aG}6LF{OH&_xgdU%2c9z&^MKIN)eaf3aJDz09H^y=7}Zw z4~59HDuKkMJ@qG6aEOQwtG;ji_}aF^wKVSUg?`9966DR}vr0w5b>Br?8+}8{egDsy zyy;FvGlPKhunX~-D6x&9f`{mg-TR;D|9)Dt?SNHTf+8; zroYx!`F+8+RXa%>vWOrvD6zpdj<7$IU?yd#O&2MUhTIMoylv8B=X`bdV<*&DanJXG z!8}c;M)(za9${60sm~hDx@{vJR_`r04!1Mep@!VY#)LZp+1H^Dg>vz%-zlYt$O2dw zvO@0^5xMphp#TDG{P0WOy_4R2)xF#FkGpp@bXdW~ItzMC4je?+)b@hK_l7uH)#NW^ zZ$z|y{_B{2d}P!9@ngDhggz!fXThy|)DWkO*y*|Q59WUNKwY;AjuP~ve)r~pgusYC zincw-R8)t;2|j42L^@Vvf^3E;vdWwb!=G;-deqTCTr)PVQY29CSemfz{0oK(x{>sO%>rTl(d%>cR{h! z<|8(t=Yw2+KxUDsafR?iq7Yc*EvJYz3x_&fvnvn}QEAc5Z>#rNI6EH`Jk&PlD!K6+ zdfP~)&r|&@6B0`s?Te_Ea3@4m57P6nX4URtp@FQU;_7X2NCin~Y=%18z3~u22nlZi z^rR19YHz?!242jl*)>~v6E@Z>Yd7b)!xX?avFq5FW1cybRVdg7ki=0q|1h-=)W<+> z`t*A!+sSOwk>8F+0^QBe&5QW(8znYh^;R0sb!(1;l>@3y>3-=h^FOeup~&j<8IQW@ zB_8=>b0mJc_IsQaLUR45+0E7I!9`B(A2K~}-@i1`ZxFxhzd(563Thh-h_}F;+(;8k zcIMs4wq8^AToXaLaIfp|=4cg*ksU>p25@PGGfuC5vsFmTQBly8uaYwh F{y+YP)z<(3 diff --git a/protocol-designer/src/images/modules/engage_height_static_gen2.png b/protocol-designer/src/images/modules/engage_height_static_gen2.png deleted file mode 100644 index 9e9e163371b791d0062e973e48f0b82876288de0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8173 zcmW-m1z1yG8^8xhihz`WgmjNi5eWrGx6%#L-6SjdXVnVMs}R`+w)T zcjrEL+k4)7&U@bY-AGks*;hCeH~;|P6-e%rIskx*hrDLNLPdU(*X`LMJ8UO89am(J z^8ZtXv2PxE!Qi9$)lJ&P!oXE1(kmk+m>x9cZPZ^Xr5CNBgj4HQgTrw?ATu5B-2O(sYIMy^NJ9S~%I0 zOG`;%A;ziKg=@{2*1Ea5mHoDf?k}AF);7}E(9lp{kB)&MC?r(z+vbN9t*l|ubjG+P zL>;OQ6&v+*bNk9cS{o>W2wJiu&Fj=lNtK7(8XN8f)}>tev{RadbQ=q1wMX4KU3Up- zW$#RV<3Bn&a$!)AmzR=~g4NZv%kS^+XO8c0ZW>4T^SU2v)*!sSy=97cfWYd?N)A3g zK3?9LljM|?l)k}18c{bE0fFBqmkV=qy9Wn@eSMT7E)2Z9F1tgChBU0q%(8w@4r=o@ z^)f{lV`|EZiX1?oI8ln8o}Q51;*`NiRaMm*Bs(`3XjNVYgl`C(2!fwZOtFG;eDm6z0 z)>;&FilM)4>UFF9Ms%yRi#aYyB__nSdTU;HKzqW0=jEQ2Q7Rqk+!V1Cu`;Ezv?`8K zQLMDIDBeCge*05Xst|QnBdAV}pcVe!;^Nm96&>YgdwcsJc?h8uJvc>+Emclu!j3`x znr*+gh8QUl6I-{6l>FLELxYHboSgL-eY))mr-CrC$gR>xNqN#uyiOi6V^hD~o@>HS zal~JHglBP)I6-M~S3aK$dw`a;rqt`&PqN%QoplRo-M*77vGL%pZ9kTrmchfpA$+`Y zsSHRSW?0@qYO8sYODi5y_9D@$AC0%D`7|x(g8Ouk z2tU7OKoj~#`F3{L|3!>tlxx{W_{>+dv)VAKMGi$mqfiSmQph#zZEfq87lu^F@#1`I zZW&oevtxY}bxH&qtJ(V0+LS+Er}zsqZ)LIhCA{NnA3SctG#}7B(xh|ItZSc7-r0fq zz)_~d>173?DSl$69F;__8H~7C78%93c>4QvK({IXKfF}qsnTl~)ADOWJGCpXd5yw( zB7ywujPj5LYDb@WaZW99i;97TBRBDpiSvH83Gdqy92z^MIUB+?oWs9MM`!0Iz7c6$ z@Ii_w)F$ywOI|m-TGpdc#Ix{iAFIiRWv7^0jkqA1{3@bc$^Ki&xBL{bRyBlfMtG@H2^Hdl^v!i(6CSF#(y-9wHd&#veVpd-zRu9n9<>~f{rt?IvpcbXj z#clej$od|_wYhTrO5vjh{$^uW>lgx6e08g^eN1jY&^%WCtBLPhy@0csm3j4j|FIqw zRuS3?cKo5G(Rf+q`sCbhTgBhqwp=~PPSVjQtZrw$&alCAHM((VdQ>}Ehsgqy=>D&I zLmt_#;%6%n3z`Ygq7uoKSo&z)#&R{B`~0xQXi$?D^RZAaI*t9oorf!M0|JrW%)B{# z8~N^BALyEXv88;ZKCM>mAHRWb>Bsh_y~nxv{)a(PAnroDtinC3`wX8~EL8MstD#vr z9?(vNTnYL3XBh55Av^1~WJ?91VTvQyK1+N*s8!FZRsL|4p7x4F0z1u#?h$(d$DE<% zDC=W8Nd+^I9j5i6(%l!>CSu&F1Ehu<17>du&dqitM`FgP%ucMlQAB)?hz6t&h=_U+ zsOoN&9K|J(M`%r!3|-4>l}kTnXkz7$UAY(jBS$s+&$ahkj8f$yC?VF( zV2NXWXiN9VMweLn{wFNqW3FdQ;PU$B?=7=66XT7;-_dYmQ{M#7j^f#)w#>2X7G4&X zp>O>2b8}4VFHh-c7?NPq#VQt@1Nm8}Lt#d}T6U0XV^T z+HnaG%6L0?o<6x`IME?b{dL@kYxkcT2iInc8p{K>ptAz$EGGB8dk`dxMRcUDE)sv| zzy@}YSQbRCAyN)bnoq+Z40YcsnY5q85w&t-GVkL~+(nF{`Em5GU&)>sIV2gG;7dVI z<;`wr<>GKE7~mUUSCiz+G&4W(fqfdCM=Y0TcvUzoVItGr=xpaE1Vw62jIUyJ>M5XM zvKu8OyK8Q&y+T-hw1?I13A`0eR&X%S@pO&e8fRj&-=X!O-;4a>4jl8@yiegvU^yT| zB!lbb>YUk!ji5f{3!~|eFTS%PYwNCxzn73ZDWu$e(PUH0L`3+^LOspV)H5BXkVkEm z1X~!<1hV?dZ6oZCi= zg-@1t=9nG)wjZO49m%ssW2r7IN7r6t^icu~!B3|;xK4)eDY_5YC zWD_VNXYxe(?%BcO35%+hueujMMPx2CIPZ{YKIsbwJ!)wREn~n5Zg2Wyl()|!DXY=F zKEzi{(hCR0c+JUsP^B`AouBmq$UI#MCF*%-DRJuDZ879tH~yc0~A3!ssa)TQG-BJ zO}a06gNx?-=DbjvgVMSiVdNNin$3@3h*;zXs)Ihz-=OVvC;H6C@By8-mT^XjjI!zv zo|C#D$Cl(VKX$YEF!0`Zl3}tAV~=5^R~%H-2tldge!i?Xh)A%rL7F-~zRzs8bKo-T zdwjUJAmIwU{eu-P(rbMX267HURBbH-869_PZ+Sh)imwAJt>6wa%+rf>^`W*+{2^aD ztl(7!KEjsUbsy}+UN~7}j8_48W^_sGlN#`tuJ^X;A0A&S^Yij1{**wBWISU_Z_7n> zt)g->C!^KzGBicj)>(Zl!d#%*cfJ2eoxRi~DNj_ZFC#Z`P2`v>Ee_;ZW8f;)e1#Yd9jk zBx-;(cRlDL@0%tE`{qs_z=PszYZiKPvlXMGfmv(25ra@%ExL$49dE`i)<8)DW%o~U z!1E2&jq>&Y&)4N*G6@?7&}(x>SGe5YEdXe1HJj1VItVM{mM>9O$e&1YZC_}%ks0f2nqiq^-Eo;S@h?)0zElzzcv zTm1~U!RX0U7JUQR*_1iT>8YuES~F4El*`M@tDtMgvFRf>Ft*OTj7XdvCQ&O427^L} zA?t4Qp4vou98++>JrxF>Sm{mE-{l>i_Cb+-j$Ew+<#jeQm2NZ({0bcW6nLvBJY{8h zSve7ge?YDOkYn^Qsxx3>hTV>{8|4#5ts9XE#rhDvcuWatbzt5?wW%MQhS6?Oj=qQ# zVZbfw*+WFg?-!V=U%M4t_3`x$2Vw9R=n3U!0fLW(x3|owsWZx6AJgvGzLX|%1jFCS^kPh&WG0uqhp8N`g=vC$V=QBydo+g3OdTNzu z(_tpUUXhPEL&iaBAUk%mYwm^K_EeRiq(%fdQ^qoQx9EfifzQW#k>Yd6Bi5%$rN2mr zw2n2%O6%R(aGpDF&i2K+*Ie zY%SasyTNFr80#$O1wpCB-ngO?=FpjfE^*h*%>{@SPF2|&(o;k!aB*q}yGP|{%?)X<{?VjUce7~0YS^t1*Mu2`A{bFoE@pw>+9w5Hb`&i)(x6wY$9~&y-YqF*L z&PEzA())&+h18W@rG}X!K?K~8%LOlo!P5n{cT>P!4d|6@J&O$3Mxrr*K=EZze$?F( z3(135sO^T?qgd`kj+>(@U~(cYch;!8z(&vbc|_5&ec+=ND6Z%`&&&y-npH_vNU0?+ z5d9t7;=GlIkzT^iJVz4U6gsGAvD4)KO?{h$ZsuQ7T|K>=4|Bj-c>8h^9%`;x`I+Kr zW+p|tRYc32Ej61)GheMp7K`2}zjyxWCj1C!VN0FPXMvnDUPRMw-o^nkKX0J<35>XW zAIMjAR`iPnl*oAEm4E4Fpf*;%`FATRWGG6q78j-`8qF6K!2<%ETq6S0dDvcesSbX* z$39O-C8SdvKdsokcD`jNc(FcGTEF&rQhW7VdbzLC0Z%XJwLu#T7#lFT=W>Mwqy*$^ zBc@eC%k2Ho9`1Ub06+4~s*g8?ONuXKs2gL^B4(HOKN@eP0DG?eb&XOUD+pw}&tB=( z1pEx4b&gSXk<`E&vvh3?euYP1hR>8dykzcSZK`KhK7pXtUpz~Rt7{xYhBS!bC4soJ zH_+OX75U&fv?IBtbO&1KrKzww6@2m(--hy^mRdRri~pSF3q9?=hjSUP%Z8R&TQh5} z^}lD3dOI|3Mt(0=U_+rdByXa;e?;yRAn57e^V~ou;fwj~O0kS)m@92Rnfg;-e`>>d ze2$Y0H!LYBiF)HqQs8~@O7yvnqbQ32KDpA_sVP2>)&Zc>-*G~80rFo8Z-`XLdgN?B zX;x}!sWcpS1QE~iOEw^&tH z1*3Bq0nxU>0Q+B`_A-H%w|2jBuV*=lRv^@W6CD2VmK-~Te-FAWb5on9^sqvkGGB5Z7IWSxs_ zKJ6WPLOxESt!}HnRfJoWZL9s7Ypk-K^*s7M@G2*0rU8cKq}0fnWzp>Cr4W>@iPG?_ z2i}(RmHY3X!eP#@HZ-?F(8MVHcP3VL9E4!+2iq4T>(bmBp;B|U6A07jx_Q?xVEGi! z5+_o1QmxCHU|6`~cO%@Yc!*{VUB<5d0FTGU?#mRSxg5L0fyi>GuxPcLLVyexS;PYQ9_i`n+5g|* zHOMlM$9_(yroX?RJXQvY1sv6oY(ZOFdt3e}f)fIP;1#8(rM0BIxytW0mIHxmJ&Vb3 zF`V6K_GXovt!ktA!r^;rT zH2pgvw!@}ZKfE^9Bi|tAV@Z?#J(qb04ZdKF zQQ6C(;Sq4XAPBqeR+SybKIS~-PDmZ=3uwDUbGHOikNgzZ_ot5ERrTx9jx0+5LSi9v zWK_3i(*es}8}#`D&OYCy$kc85HOD_=>`be(@(G%r%+kJd2x48UXiUh=#33do{#vCR z(*4u(kKsE;#(o_`I}Q#GViF=%4Gj%dRb%)F3eqy=A+X~A+9x-ojPd=42fwU?xMoblIG)O-rTgAEj z*?xrgS*x*wZ&Be?0QQ$l*7C6Qd$3Do0f zW~&bBRe(v-r5_ zo=ALEQsF%ty0dN)-66rZH*(XjNu-`kEZgPH3A#}feIvJt0F41dg^uI|z4t5R2;A1V zX`!eUZMie1*S4MtS?ze5AQ4Dc27?Eg@YivO z_}xu^`~5dTKlP!FQnM>oO(9-q?2R@13I>+K_7H`QcXScI8?$Ge9G`=r;J_>D$CMP} z~1Abi6p2ywpE z`|{<>N$L@#GqS9^8&f+o;OM~3ul@1tu%Chbj^0o{85FzQq2=d7^t=i+<%oY@>6}NTq|{`TL+DcOd!Q$<3ubcKXVr#2FarL>DAic!jIF;F{d4hprVNwd0NPO!y9V+oJsf&$}uO$THg#icOznik}eoa z>ryW6@52w0Q*OEY#=h@;nZyH3bfT%6O<^V4L48i3L{jak_3k!oz-WVp&5OgL)ao-) zc@6fZpBSQLqmCan6|=hrV3Za$RMk3qGO?Ss>0=U?()(y<0FGhJ&DmYB#LNVWY6 zLx@6e)omC|zQ=t)!>oXonT2X-dR=(xgRR*`Ecy!_BVVOP2@8A`UW)0YxvwCjlvTv? zjn}YZ;`^QtI4U2>YafJE+_Uot@`!!k1f1nb9cyr8Z<}J>m6es9>klpS@lz<}iTfj$ z?JEkXN?Z2RCnQ+KA7(?wjE08!|C87kn3%Tpy0J1lI_ux~bve@J=e3c{_CMZeucHHg zf4954yS%Jhx5QBrYQvuy5rH+$xUDWOD5zbfEyT}{M6*aRsa@eNnuF<$IU*@3xjnpf zAc!Q5i%6DtrY;CxF1KkAM=goE#*|FD=3=*}3y{IfImN6BM>`@UVKp`XR zP1J3iv=d(X%=^qOQKmcdB#FY{aZO1))qkRD#iUPPzu?&-syIFqJ1n|lu$AMWreuW= z&0&(vAJH!1`k7o0jL>Zrm|Y^QP-XM7B*l(*$&a$P@bgUgQe*npHAcQ~GT!GU`#!8Z z0-4v}9oY$Y1zNfuaaa3nc#2wV0n_RXU|Xm)Ek@_FLE#aA~REL2dv<+*oe+h<~x5QUDufz@na5p^`@t%X>|{I_wHiV8h%v>rre;ojU+vd5E^S-YzoLSNGr~2gIAd;7VB)4;;i3w*Q(*=-ep!2Kk>eje7D5|z4*#phY%gc$`VgV?__2T&L99D zPJR?hNDWr+Zj1N6j7J7FiBx;7f>XEOgW){Pv-Z@TS6}(0sig0bm}_1gx+hnqL={aG z(^FlnDwSbRC!)@7F~)8_7Z*KcY=?^6Gt+87YE)IXl&rcgCHMF2njY@Q*@NW{3^Txw zJ9c0;03`T(Z9{Qm5wrErpSw7EYK!OkBIJ9xDm8$8QX0CWclEoyX#7wEDdQcf7pQ3 zJ!U71cLc1WxbDSb*1mFPM899*o*uIcHo6Srq-_|px3bHVK4L?HyHYiU5En`S`lsG* zBEWzQ4h>UR-qdf+?A_!nz=UtwW2$){pJ?Pm?}uzQWMb(m6N@jNE%U44p7WIhAgq4? zH*yw*t=&b}N5bc_z&)VAF>c~>Osr#i7dl>W2-Yz7wiL?ASnq7L(6lpM+!J~>Sm&nlfaHAXj zKSY<{GbmdyR@djs9>vc#}N z1U96kbfw3@a`Sex_o(0C!1K<>gyD_o@Q~wJTptvJpZ+g!wG+tGO!F2W6zJdb?#Y98 zau0zZwZvnhr?!9hSoqcTWoz8UKfxz%{qk`>fml6Qlb>)`%=gtKM)2;agYhs^RK@Lz zn;=!vKb6A=qBENDu)DUz!5hfm_y%wHpy+UJCa@6VoAplJ3d6xmB93oX+q&CHA@34_a!6bd~zNATyDQ)AYjK>zYQL~fK zgU)1e6+^aDe&nxvb|DQF@l|yO_b20v-%dp%UHB2_C-l61hd~CAj=A<<7#f>E5-XSddTx463R^(5wE`!6^@=C; zDzr?;MT9MIy)*9q3*Rm|s@AGtoQc1k1EsM9E(J+zT!HHOh<2v#(8n>~1te-YGsJc} z@ETy6rB&5Yxxh|yC@IfPB0BYp*b{PkF2y}N6`{{1aYgdF_`gzqRb>nwOt^vyAdcOdV_rX?01MVQT_88O3#@&*lF%17H;c(kEuQ8PJH-g zSQ(AWLFUb;braoX^5s%rX~omg2fMhrWtTJ+E;ePv)0I`R)qgsL=WgliY4JBAeh09f zMB6nE6pjbqmXNu!`_qH7Ek`r1{~HUb{u7PJw?RGZd#VlOt9k(Fv+}2EDdXV( E0oZ2>)&Kwi diff --git a/protocol-designer/src/localization/en/application.json b/protocol-designer/src/localization/en/application.json index 79625a33d51..7943a006e6f 100644 --- a/protocol-designer/src/localization/en/application.json +++ b/protocol-designer/src/localization/en/application.json @@ -15,6 +15,8 @@ "update": "UPDATE", "updated": "UPDATED", "pipettes": "Pipettes", + "magnet_height_caption": "Must be between {{low}} to {{high}}.", + "magnet_recommended": "The recommended height is {{default}}", "networking": { "generic_verification_failure": "Something went wrong with your unique link. Fill out the form below to have a new one sent to your email. Please contact the Opentrons Support team if you require further help.", "unauthorized_verification_failure": "This unique link has expired and is no longer valid, to have a new link sent to your email, fill out the form below.", From 1819b8c90b8440d6d896f4315380651f1880989c Mon Sep 17 00:00:00 2001 From: Max Marrone Date: Tue, 9 Apr 2024 11:58:06 -0400 Subject: [PATCH 077/194] feat(api): Pause when `pick_up_tip()` errors in a Python protocol (#14753) --- .../protocol_api/core/engine/instrument.py | 7 +- .../protocol_engine/actions/__init__.py | 2 + .../protocol_engine/actions/actions.py | 21 + .../protocol_engine/clients/sync_client.py | 23 + .../protocol_engine/clients/transports.py | 115 ++++- .../execution/command_executor.py | 1 + .../protocol_engine/execution/queue_worker.py | 3 + .../protocol_engine/protocol_engine.py | 81 +++- .../protocol_engine/state/commands.py | 10 + .../opentrons/protocol_engine/state/state.py | 55 ++- .../opentrons/protocol_engine/state/tips.py | 32 +- .../protocol_runner/legacy_command_mapper.py | 1 + .../core/engine/test_instrument_core.py | 2 +- .../execution/test_command_executor.py | 1 + .../state/test_command_state.py | 361 ++++++++++++++- .../state/test_command_store_old.py | 425 +----------------- .../state/test_command_view_old.py | 2 + .../protocol_engine/state/test_state_store.py | 42 +- .../protocol_engine/test_protocol_engine.py | 102 +++++ .../test_legacy_command_mapper.py | 2 + 20 files changed, 810 insertions(+), 478 deletions(-) diff --git a/api/src/opentrons/protocol_api/core/engine/instrument.py b/api/src/opentrons/protocol_api/core/engine/instrument.py index 9c88a4f7ecb..485f45d0e94 100644 --- a/api/src/opentrons/protocol_api/core/engine/instrument.py +++ b/api/src/opentrons/protocol_api/core/engine/instrument.py @@ -408,13 +408,18 @@ def pick_up_tip( well_name=well_name, well_location=well_location, ) - self._engine_client.pick_up_tip( + + self._engine_client.pick_up_tip_wait_for_recovery( pipette_id=self._pipette_id, labware_id=labware_id, well_name=well_name, well_location=well_location, ) + # Set the "last location" unconditionally, even if the command failed + # and was recovered from and we don't know if the pipette is physically here. + # This isn't used for path planning, but rather for implicit destination + # selection like in `pipette.aspirate(location=None)`. self._protocol_core.set_last_location(location=location, mount=self.get_mount()) def drop_tip( diff --git a/api/src/opentrons/protocol_engine/actions/__init__.py b/api/src/opentrons/protocol_engine/actions/__init__.py index b1181e6a50e..ac3fc653976 100644 --- a/api/src/opentrons/protocol_engine/actions/__init__.py +++ b/api/src/opentrons/protocol_engine/actions/__init__.py @@ -11,6 +11,7 @@ PauseAction, PauseSource, StopAction, + ResumeFromRecoveryAction, FinishAction, HardwareStoppedAction, QueueCommandAction, @@ -38,6 +39,7 @@ "PlayAction", "PauseAction", "StopAction", + "ResumeFromRecoveryAction", "FinishAction", "HardwareStoppedAction", "QueueCommandAction", diff --git a/api/src/opentrons/protocol_engine/actions/actions.py b/api/src/opentrons/protocol_engine/actions/actions.py index d5c6bb49abc..ee36e76f7de 100644 --- a/api/src/opentrons/protocol_engine/actions/actions.py +++ b/api/src/opentrons/protocol_engine/actions/actions.py @@ -154,11 +154,32 @@ class FailCommandAction: """ command_id: str + """The command to fail.""" + error_id: str + """An ID to assign to the command's error. + + Must be unique to this occurrence of the error. + """ + failed_at: datetime + """When the command failed.""" + error: EnumeratedError + """The underlying exception that caused this command to fail.""" + notes: List[CommandNote] + """Overwrite the command's `.notes` with these.""" + type: ErrorRecoveryType + """How this error should be handled in the context of the overall run.""" + + # This is a quick hack so FailCommandAction handlers can get the params of the + # command that failed. We probably want this to be a new "failure details" + # object instead, similar to how succeeded commands can send a "private result" + # to Protocol Engine internals. + running_command: Command + """The command to fail, in its prior `running` state.""" @dataclass(frozen=True) diff --git a/api/src/opentrons/protocol_engine/clients/sync_client.py b/api/src/opentrons/protocol_engine/clients/sync_client.py index f9c9e2ee6c6..f95611c1b4c 100644 --- a/api/src/opentrons/protocol_engine/clients/sync_client.py +++ b/api/src/opentrons/protocol_engine/clients/sync_client.py @@ -296,6 +296,29 @@ def pick_up_tip( return cast(commands.PickUpTipResult, result) + def pick_up_tip_wait_for_recovery( + self, + pipette_id: str, + labware_id: str, + well_name: str, + well_location: WellLocation, + ) -> commands.PickUpTip: + """Execute a PickUpTip, wait for any error recovery, and return it. + + Note that the returned command will not necessarily have a `result`. + """ + request = commands.PickUpTipCreate( + params=commands.PickUpTipParams( + pipetteId=pipette_id, + labwareId=labware_id, + wellName=well_name, + wellLocation=well_location, + ) + ) + command = self._transport.execute_command_wait_for_recovery(request=request) + + return cast(commands.PickUpTip, command) + def drop_tip( self, pipette_id: str, diff --git a/api/src/opentrons/protocol_engine/clients/transports.py b/api/src/opentrons/protocol_engine/clients/transports.py index 270599ff469..6de08db97ed 100644 --- a/api/src/opentrons/protocol_engine/clients/transports.py +++ b/api/src/opentrons/protocol_engine/clients/transports.py @@ -1,15 +1,28 @@ """A helper for controlling a `ProtocolEngine` without async/await.""" from asyncio import AbstractEventLoop, run_coroutine_threadsafe -from typing import Any, overload +from typing import Any, Final, overload from typing_extensions import Literal from opentrons_shared_data.labware.dev_types import LabwareUri from opentrons_shared_data.labware.labware_definition import LabwareDefinition + from ..protocol_engine import ProtocolEngine from ..errors import ProtocolCommandFailedError +from ..error_recovery_policy import ErrorRecoveryType from ..state import StateView -from ..commands import CommandCreate, CommandResult +from ..commands import Command, CommandCreate, CommandResult, CommandStatus + + +class RunStoppedBeforeCommandError(RuntimeError): + """Raised if the ProtocolEngine was stopped before a command could start.""" + + def __init__(self, command: Command) -> None: + self._command = command + super().__init__( + f"The run was stopped" + f" before {command.commandType} command {command.id} could execute." + ) class ChildThreadTransport: @@ -30,8 +43,10 @@ def __init__(self, engine: ProtocolEngine, loop: AbstractEventLoop) -> None: want to synchronously access it. loop: The event loop that `engine` is running in (in the other thread). """ - self._engine = engine - self._loop = loop + # We might access these from different threads, + # so let's make them Final for (shallow) immutability. + self._engine: Final = engine + self._loop: Final = loop @property def state(self) -> StateView: @@ -39,7 +54,11 @@ def state(self) -> StateView: return self._engine.state_view def execute_command(self, request: CommandCreate) -> CommandResult: - """Execute a ProtocolEngine command, blocking until the command completes. + """Execute a ProtocolEngine command. + + This blocks until the command completes. If the command fails, this will always + raise the failure as an exception--even if ProtocolEngine deemed the failure + recoverable. Args: request: The ProtocolEngine command request @@ -48,8 +67,11 @@ def execute_command(self, request: CommandCreate) -> CommandResult: The command's result data. Raises: - ProtocolEngineError: if the command execution is not successful, - the specific error that cause the command to fail is raised. + ProtocolEngineError: If the command execution was not successful, + the specific error that caused the command to fail is raised. + + If the run was stopped before the command could complete, that's + also signaled as this exception. """ command = run_coroutine_threadsafe( self._engine.add_and_execute_command(request=request), @@ -64,21 +86,76 @@ def execute_command(self, request: CommandCreate) -> CommandResult: message=f"{error.errorType}: {error.detail}", ) - # FIXME(mm, 2023-04-10): This assert can easily trigger from this sequence: - # - # 1. The engine is paused. - # 2. The user's Python script calls this method to start a new command, - # which remains `queued` because of the pause. - # 3. The engine is stopped. - # - # The returned command will be `queued`, so it won't have a result. - # - # We need to figure out a proper way to report this condition to callers - # so they correctly interpret it as an intentional stop, not an internal error. - assert command.result is not None, f"Expected Command {command} to have result" + if command.result is None: + # This can happen with a certain pause timing: + # + # 1. The engine is paused. + # 2. The user's Python script calls this method to start a new command, + # which remains `queued` because of the pause. + # 3. The engine is stopped. The returned command will be `queued` + # and won't have a result. + raise RunStoppedBeforeCommandError(command) return command.result + def execute_command_wait_for_recovery(self, request: CommandCreate) -> Command: + """Execute a ProtocolEngine command, including error recovery. + + This blocks until the command completes. Additionally, if the command fails, + this will continue to block until its error recovery has been completed. + + Args: + request: The ProtocolEngine command request. + + Returns: + The command. If error recovery happened for it, the command will be + reported here as failed. + + Raises: + ProtocolEngineError: If the command failed, *and* the failure was not + recovered from. + + If the run was stopped before the command could complete, that's + also signalled as this exception. + """ + + async def run_in_pe_thread() -> Command: + command = await self._engine.add_and_execute_command_wait_for_recovery( + request=request + ) + + if command.error is not None: + error_was_recovered_from = ( + self._engine.state_view.commands.get_error_recovery_type(command.id) + == ErrorRecoveryType.WAIT_FOR_RECOVERY + ) + if not error_was_recovered_from: + error = command.error + # TODO: this needs to have an actual code + raise ProtocolCommandFailedError( + original_error=error, + message=f"{error.errorType}: {error.detail}", + ) + + elif command.status == CommandStatus.QUEUED: + # This can happen with a certain pause timing: + # + # 1. The engine is paused. + # 2. The user's Python script calls this method to start a new command, + # which remains `queued` because of the pause. + # 3. The engine is stopped. The returned command will be `queued`, + # and won't have a result. + raise RunStoppedBeforeCommandError(command) + + return command + + command = run_coroutine_threadsafe( + run_in_pe_thread(), + loop=self._loop, + ).result() + + return command + @overload def call_method( self, diff --git a/api/src/opentrons/protocol_engine/execution/command_executor.py b/api/src/opentrons/protocol_engine/execution/command_executor.py index d44d37f5641..9488d1719e9 100644 --- a/api/src/opentrons/protocol_engine/execution/command_executor.py +++ b/api/src/opentrons/protocol_engine/execution/command_executor.py @@ -167,6 +167,7 @@ async def execute(self, command_id: str) -> None: FailCommandAction( error=error, command_id=running_command.id, + running_command=running_command, error_id=self._model_utils.generate_id(), failed_at=self._model_utils.get_timestamp(), notes=note_tracker.get_notes(), diff --git a/api/src/opentrons/protocol_engine/execution/queue_worker.py b/api/src/opentrons/protocol_engine/execution/queue_worker.py index c1ba60eb143..179880c03e9 100644 --- a/api/src/opentrons/protocol_engine/execution/queue_worker.py +++ b/api/src/opentrons/protocol_engine/execution/queue_worker.py @@ -72,6 +72,9 @@ async def _run_commands(self) -> None: command_id = await self._state_store.wait_for( condition=self._state_store.commands.get_next_to_execute ) + # Assert for type hinting. This is valid because the wait_for() above + # only returns when the value is truthy. + assert command_id is not None except RunStoppedError: # There are no more commands that we should execute, either because the run has # completed on its own, or because a client requested it to stop. diff --git a/api/src/opentrons/protocol_engine/protocol_engine.py b/api/src/opentrons/protocol_engine/protocol_engine.py index 8e23c08013f..bd995f4339a 100644 --- a/api/src/opentrons/protocol_engine/protocol_engine.py +++ b/api/src/opentrons/protocol_engine/protocol_engine.py @@ -234,7 +234,10 @@ async def add_and_execute_command( the command in state. Returns: - The command. If the command completed, it will be succeeded or failed. + The command. + + If the command completed, it will be succeeded or failed. + If the engine was stopped before it reached the command, the command will be queued. """ @@ -242,6 +245,34 @@ async def add_and_execute_command( await self.wait_for_command(command.id) return self._state_store.commands.get(command.id) + async def add_and_execute_command_wait_for_recovery( + self, request: commands.CommandCreate + ) -> commands.Command: + """Like `add_and_execute_command()`, except wait for error recovery. + + Unlike `add_and_execute_command()`, if the command fails, this will not + immediately return the failed command. Instead, if the error is recoverable, + it will wait until error recovery has completed (e.g. when some other task + calls `self.resume_from_recovery()`). + + Returns: + The command. + + If the command completed, it will be succeeded or failed. If it failed + and then its failure was recovered from, it will still be failed. + + If the engine was stopped before it reached the command, + the command will be queued. + """ + queued_command = self.add_command(request) + await self.wait_for_command(command_id=queued_command.id) + completed_command = self._state_store.commands.get(queued_command.id) + await self._state_store.wait_for_not( + self.state_view.commands.get_recovery_in_progress_for_command, + queued_command.id, + ) + return completed_command + def estop( self, # TODO(mm, 2024-03-26): Maintenance runs are a robot-server concept that @@ -251,6 +282,15 @@ def estop( ) -> None: """Signal to the engine that an estop event occurred. + If an estop happens while the robot is moving, lower layers physically stop + motion and raise the event as an exception, which fails the Protocol Engine + command. No action from the `ProtocolEngine` caller is needed to handle that. + + However, if an estop happens in between commands, or in the middle of + a command like `comment` or `waitForDuration` that doesn't access the hardware, + `ProtocolEngine` needs to be told about it so it can treat it as a fatal run + error and stop executing more commands. This method is how to do that. + If there are any queued commands for the engine, they will be marked as failed due to the estop event. If there aren't any queued commands *and* this is a maintenance run (which has commands queued one-by-one), @@ -261,15 +301,27 @@ def estop( """ if self._state_store.commands.get_is_stopped(): return - - current_id = ( + running_or_next_queued_id = ( self._state_store.commands.get_running_command_id() or self._state_store.commands.get_queue_ids().head(None) + # TODO(mm, 2024-04-02): This logic looks wrong whenever the next queued + # command is a setup command, which is the normal case in maintenance + # runs. Setup commands won't show up in commands.get_queue_ids(). + ) + running_or_next_queued = ( + self._state_store.commands.get(running_or_next_queued_id) + if running_or_next_queued_id is not None + else None ) - if current_id is not None: + if running_or_next_queued_id is not None: + assert running_or_next_queued is not None + fail_action = FailCommandAction( - command_id=current_id, + command_id=running_or_next_queued_id, + # FIXME(mm, 2024-04-02): As of https://github.com/Opentrons/opentrons/pull/14726, + # this action is only legal if the command is running, not queued. + running_command=running_or_next_queued, error_id=self._model_utils.generate_id(), failed_at=self._model_utils.get_timestamp(), error=EStopActivatedError(message="Estop Activated"), @@ -278,12 +330,21 @@ def estop( ) self._action_dispatcher.dispatch(fail_action) - # In the case where the running command was a setup command - check if there - # are any pending *run* commands and, if so, clear them all - current_id = self._state_store.commands.get_queue_ids().head(None) - if current_id is not None: + # The FailCommandAction above will have cleared all the queued protocol + # OR setup commands, depending on whether we gave it a protocol or setup + # command. We want both to be cleared in either case. So, do that here. + running_or_next_queued_id = self._state_store.commands.get_queue_ids().head( + None + ) + if running_or_next_queued_id is not None: + running_or_next_queued = self._state_store.commands.get( + running_or_next_queued_id + ) fail_action = FailCommandAction( - command_id=current_id, + command_id=running_or_next_queued_id, + # FIXME(mm, 2024-04-02): As of https://github.com/Opentrons/opentrons/pull/14726, + # this action is only legal if the command is running, not queued. + running_command=running_or_next_queued, error_id=self._model_utils.generate_id(), failed_at=self._model_utils.get_timestamp(), error=EStopActivatedError(message="Estop Activated"), diff --git a/api/src/opentrons/protocol_engine/state/commands.py b/api/src/opentrons/protocol_engine/state/commands.py index 2c66e45826d..1ae0cb1ed68 100644 --- a/api/src/opentrons/protocol_engine/state/commands.py +++ b/api/src/opentrons/protocol_engine/state/commands.py @@ -178,6 +178,9 @@ class CommandState: stable. Eventually, we might want this info to be stored directly on each command. """ + recovery_target_command_id: Optional[str] + """If we're currently recovering from a command failure, which command it was.""" + finish_error: Optional[ErrorOccurrence] """The error that happened during the post-run finish steps (homing & dropping tips), if any.""" @@ -213,6 +216,7 @@ def __init__( finish_error=None, failed_command=None, command_error_recovery_types={}, + recovery_target_command_id=None, run_completed_at=None, run_started_at=None, latest_command_hash=None, @@ -300,6 +304,7 @@ def handle_action(self, action: Action) -> None: # noqa: C901 ): if action.type == ErrorRecoveryType.WAIT_FOR_RECOVERY: self._state.queue_status = QueueStatus.AWAITING_RECOVERY + self._state.recovery_target_command_id = action.command_id elif action.type == ErrorRecoveryType.FAIL_RUN: other_command_ids_to_fail = ( self._state.command_history.get_queue_ids() @@ -335,6 +340,7 @@ def handle_action(self, action: Action) -> None: # noqa: C901 elif isinstance(action, ResumeFromRecoveryAction): self._state.queue_status = QueueStatus.RUNNING + self._state.recovery_target_command_id = None elif isinstance(action, StopAction): if not self._state.run_result: @@ -708,6 +714,10 @@ def get_all_commands_final(self) -> bool: return no_command_running and no_command_to_execute + def get_recovery_in_progress_for_command(self, command_id: str) -> bool: + """Return whether the given command failed and its error recovery is in progress.""" + return self._state.recovery_target_command_id == command_id + def raise_fatal_command_error(self) -> None: """Raise the run's fatal command error, if there was one, as an exception. diff --git a/api/src/opentrons/protocol_engine/state/state.py b/api/src/opentrons/protocol_engine/state/state.py index a472b574e6f..6e08bf759c6 100644 --- a/api/src/opentrons/protocol_engine/state/state.py +++ b/api/src/opentrons/protocol_engine/state/state.py @@ -2,8 +2,8 @@ from __future__ import annotations from dataclasses import dataclass -from functools import partial -from typing import Any, Callable, Dict, List, Optional, Sequence, TypeVar +from typing import Callable, Dict, List, Optional, Sequence, TypeVar +from typing_extensions import ParamSpec from opentrons_shared_data.deck.dev_types import DeckDefinitionV4 @@ -30,7 +30,9 @@ from .state_summary import StateSummary from ..types import DeckConfigurationType -ReturnT = TypeVar("ReturnT") + +_ParamsT = ParamSpec("_ParamsT") +_ReturnT = TypeVar("_ReturnT") @dataclass(frozen=True) @@ -210,10 +212,10 @@ def handle_action(self, action: Action) -> None: async def wait_for( self, - condition: Callable[..., Optional[ReturnT]], - *args: Any, - **kwargs: Any, - ) -> ReturnT: + condition: Callable[_ParamsT, _ReturnT], + *args: _ParamsT.args, + **kwargs: _ParamsT.kwargs, + ) -> _ReturnT: """Wait for a condition to become true, checking whenever state changes. If the condition is already true, return immediately. @@ -258,14 +260,43 @@ async def wait_for( Raises: The exception raised by the `condition` function, if any. """ - predicate = partial(condition, *args, **kwargs) - is_done = predicate() - while not is_done: + def predicate() -> _ReturnT: + return condition(*args, **kwargs) + + return await self._wait_for(condition=predicate, truthiness_to_wait_for=True) + + async def wait_for_not( + self, + condition: Callable[_ParamsT, _ReturnT], + *args: _ParamsT.args, + **kwargs: _ParamsT.kwargs, + ) -> _ReturnT: + """Like `wait_for()`, except wait for the condition to become false. + + See the documentation in `wait_for()`, especially the warning about condition + design. + + The advantage of having this separate method over just passing a wrapper lambda + as the condition to `wait_for()` yourself is that wrapper lambdas are hard to + test in the mock-heavy Decoy + Protocol Engine style. + """ + + def predicate() -> _ReturnT: + return condition(*args, **kwargs) + + return await self._wait_for(condition=predicate, truthiness_to_wait_for=False) + + async def _wait_for( + self, condition: Callable[[], _ReturnT], truthiness_to_wait_for: bool + ) -> _ReturnT: + current_value = condition() + + while bool(current_value) != truthiness_to_wait_for: await self._change_notifier.wait() - is_done = predicate() + current_value = condition() - return is_done + return current_value def _get_next_state(self) -> State: """Get a new instance of the state value object.""" diff --git a/api/src/opentrons/protocol_engine/state/tips.py b/api/src/opentrons/protocol_engine/state/tips.py index a2539ff45e7..f5d68d61ee5 100644 --- a/api/src/opentrons/protocol_engine/state/tips.py +++ b/api/src/opentrons/protocol_engine/state/tips.py @@ -7,11 +7,13 @@ from ..actions import ( Action, SucceedCommandAction, + FailCommandAction, ResetTipsAction, ) from ..commands import ( Command, LoadLabwareResult, + PickUpTip, PickUpTipResult, DropTipResult, DropTipInPlaceResult, @@ -20,6 +22,7 @@ PipetteConfigUpdateResultMixin, PipetteNozzleLayoutResultMixin, ) +from ..error_recovery_policy import ErrorRecoveryType from opentrons.hardware_control.nozzle_manager import NozzleMap @@ -71,7 +74,7 @@ def handle_action(self, action: Action) -> None: self._state.channels_by_pipette_id[pipette_id] = config.channels self._state.active_channels_by_pipette_id[pipette_id] = config.channels self._state.nozzle_map_by_pipette_id[pipette_id] = config.nozzle_map - self._handle_command(action.command) + self._handle_succeeded_command(action.command) if isinstance(action.private_result, PipetteNozzleLayoutResultMixin): pipette_id = action.private_result.pipette_id @@ -86,6 +89,9 @@ def handle_action(self, action: Action) -> None: pipette_id ] = self._state.channels_by_pipette_id[pipette_id] + elif isinstance(action, FailCommandAction): + self._handle_failed_command(action) + elif isinstance(action, ResetTipsAction): labware_id = action.labware_id @@ -94,7 +100,7 @@ def handle_action(self, action: Action) -> None: well_name ] = TipRackWellState.CLEAN - def _handle_command(self, command: Command) -> None: + def _handle_succeeded_command(self, command: Command) -> None: if ( isinstance(command.result, LoadLabwareResult) and command.result.definition.parameters.isTiprack @@ -124,6 +130,28 @@ def _handle_command(self, command: Command) -> None: pipette_id = command.params.pipetteId self._state.length_by_pipette_id.pop(pipette_id, None) + def _handle_failed_command( + self, + action: FailCommandAction, + ) -> None: + # If a pickUpTip command fails recoverably, mark the tips as used. This way, + # when the protocol is resumed and the Python Protocol API calls + # `get_next_tip()`, we'll move on to other tips as expected. + # + # We don't attempt this for nonrecoverable errors because maybe the failure + # was due to a bad labware ID or well name. + if ( + isinstance(action.running_command, PickUpTip) + and action.type != ErrorRecoveryType.FAIL_RUN + ): + self._set_used_tips( + pipette_id=action.running_command.params.pipetteId, + labware_id=action.running_command.params.labwareId, + well_name=action.running_command.params.wellName, + ) + # Note: We're logically removing the tip from the tip rack, + # but we're not logically updating the pipette to have that tip on it. + def _set_used_tips( # noqa: C901 self, pipette_id: str, well_name: str, labware_id: str ) -> None: diff --git a/api/src/opentrons/protocol_runner/legacy_command_mapper.py b/api/src/opentrons/protocol_runner/legacy_command_mapper.py index ea212123cb3..e835a6af8e6 100644 --- a/api/src/opentrons/protocol_runner/legacy_command_mapper.py +++ b/api/src/opentrons/protocol_runner/legacy_command_mapper.py @@ -265,6 +265,7 @@ def map_command( # noqa: C901 results.append( pe_actions.FailCommandAction( command_id=running_command.id, + running_command=running_command, error_id=ModelUtils.generate_id(), failed_at=now, error=LegacyContextCommandError(command_error), diff --git a/api/tests/opentrons/protocol_api/core/engine/test_instrument_core.py b/api/tests/opentrons/protocol_api/core/engine/test_instrument_core.py index 3b296067a0d..6ac0e9aaaf0 100644 --- a/api/tests/opentrons/protocol_api/core/engine/test_instrument_core.py +++ b/api/tests/opentrons/protocol_api/core/engine/test_instrument_core.py @@ -276,7 +276,7 @@ def test_pick_up_tip( origin=WellOrigin.TOP, offset=WellOffset(x=3, y=2, z=1) ), ), - mock_engine_client.pick_up_tip( + mock_engine_client.pick_up_tip_wait_for_recovery( pipette_id="abc123", labware_id="labware-id", well_name="well-name", diff --git a/api/tests/opentrons/protocol_engine/execution/test_command_executor.py b/api/tests/opentrons/protocol_engine/execution/test_command_executor.py index 94b7ad25509..2cd753093f9 100644 --- a/api/tests/opentrons/protocol_engine/execution/test_command_executor.py +++ b/api/tests/opentrons/protocol_engine/execution/test_command_executor.py @@ -500,6 +500,7 @@ def _ImplementationCls(self) -> Type[_TestCommandImpl]: action_dispatcher.dispatch( FailCommandAction( command_id="command-id", + running_command=running_command, error_id="error-id", failed_at=datetime(year=2023, month=3, day=3), error=expected_error, diff --git a/api/tests/opentrons/protocol_engine/state/test_command_state.py b/api/tests/opentrons/protocol_engine/state/test_command_state.py index 001b1b7640c..8f1ea39fc00 100644 --- a/api/tests/opentrons/protocol_engine/state/test_command_state.py +++ b/api/tests/opentrons/protocol_engine/state/test_command_state.py @@ -6,10 +6,14 @@ from datetime import datetime -from opentrons_shared_data.errors import PythonException +import pytest -from opentrons.protocol_engine import actions, commands +from opentrons_shared_data.errors import ErrorCodes, PythonException + +from opentrons.ordered_set import OrderedSet +from opentrons.protocol_engine import actions, commands, errors from opentrons.protocol_engine.error_recovery_policy import ErrorRecoveryType +from opentrons.protocol_engine.notes.notes import CommandNote from opentrons.protocol_engine.state.commands import CommandStore, CommandView from opentrons.protocol_engine.state.config import Config from opentrons.protocol_engine.types import DeckType @@ -23,6 +27,269 @@ def _make_config() -> Config: ) +@pytest.mark.parametrize("error_recovery_type", ErrorRecoveryType) +def test_command_failure(error_recovery_type: ErrorRecoveryType) -> None: + """It should store an error and mark the command if it fails.""" + subject = CommandStore(is_door_open=False, config=_make_config()) + subject_view = CommandView(subject.state) + + command_id = "command-id" + command_key = "command-key" + created_at = datetime(year=2021, month=1, day=1) + started_at = datetime(year=2022, month=2, day=2) + failed_at = datetime(year=2023, month=3, day=3) + error_id = "error-id" + notes = [ + CommandNote( + noteKind="noteKind", + shortMessage="shortMessage", + longMessage="longMessage", + source="source", + ) + ] + + params = commands.CommentParams(message="No comment.") + + subject.handle_action( + actions.QueueCommandAction( + command_id=command_id, + created_at=created_at, + request=commands.CommentCreate(params=params, key=command_key), + request_hash=None, + ) + ) + subject.handle_action( + actions.RunCommandAction(command_id=command_id, started_at=started_at) + ) + subject.handle_action( + actions.FailCommandAction( + command_id=command_id, + running_command=subject_view.get(command_id), + error_id=error_id, + failed_at=failed_at, + error=errors.ProtocolEngineError(message="oh no"), + notes=notes, + type=error_recovery_type, + ) + ) + + expected_error_occurrence = errors.ErrorOccurrence( + id=error_id, + errorType="ProtocolEngineError", + createdAt=failed_at, + detail="oh no", + errorCode=ErrorCodes.GENERAL_ERROR.value.code, + ) + expected_failed_command = commands.Comment( + id=command_id, + key=command_key, + commandType="comment", + createdAt=created_at, + startedAt=started_at, + completedAt=failed_at, + status=commands.CommandStatus.FAILED, + params=params, + result=None, + error=expected_error_occurrence, + notes=notes, + ) + + assert subject_view.get("command-id") == expected_failed_command + + +def test_command_failure_clears_queues() -> None: + """It should clear the command queue on command failure.""" + subject = CommandStore(config=_make_config(), is_door_open=False) + subject_view = CommandView(subject.state) + + queue_1 = actions.QueueCommandAction( + request=commands.WaitForResumeCreate( + params=commands.WaitForResumeParams(), key="command-key-1" + ), + request_hash=None, + created_at=datetime(year=2021, month=1, day=1), + command_id="command-id-1", + ) + subject.handle_action(queue_1) + queue_2 = actions.QueueCommandAction( + request=commands.WaitForResumeCreate( + params=commands.WaitForResumeParams(), key="command-key-2" + ), + request_hash=None, + created_at=datetime(year=2021, month=1, day=1), + command_id="command-id-2", + ) + subject.handle_action(queue_2) + + run_1 = actions.RunCommandAction( + command_id="command-id-1", + started_at=datetime(year=2022, month=2, day=2), + ) + subject.handle_action(run_1) + fail_1 = actions.FailCommandAction( + command_id="command-id-1", + running_command=subject_view.get("command-id-1"), + error_id="error-id", + failed_at=datetime(year=2023, month=3, day=3), + error=errors.ProtocolEngineError(message="oh no"), + notes=[ + CommandNote( + noteKind="noteKind", + shortMessage="shortMessage", + longMessage="longMessage", + source="source", + ) + ], + type=ErrorRecoveryType.FAIL_RUN, + ) + subject.handle_action(fail_1) + + assert [(c.id, c.status) for c in subject_view.get_all()] == [ + ("command-id-1", commands.CommandStatus.FAILED), + ("command-id-2", commands.CommandStatus.FAILED), + ] + assert subject_view.get_running_command_id() is None + assert subject_view.get_queue_ids() == OrderedSet() + assert subject_view.get_next_to_execute() is None + + +def test_setup_command_failure_only_clears_setup_command_queue() -> None: + """It should clear only the setup command queue for a failed setup command. + + This test queues up a non-setup command followed by two setup commands, + then runs and fails the first setup command. + """ + subject = CommandStore(is_door_open=False, config=_make_config()) + subject_view = CommandView(subject.state) + + queue_1 = actions.QueueCommandAction( + request=commands.WaitForResumeCreate( + params=commands.WaitForResumeParams(), key="command-key-1" + ), + request_hash=None, + created_at=datetime(year=2021, month=1, day=1), + command_id="command-id-1", + ) + subject.handle_action(queue_1) + queue_2_setup = actions.QueueCommandAction( + request=commands.WaitForResumeCreate( + params=commands.WaitForResumeParams(), + intent=commands.CommandIntent.SETUP, + key="command-key-2", + ), + request_hash=None, + created_at=datetime(year=2021, month=1, day=1), + command_id="command-id-2", + ) + subject.handle_action(queue_2_setup) + queue_3_setup = actions.QueueCommandAction( + request=commands.WaitForResumeCreate( + params=commands.WaitForResumeParams(), + intent=commands.CommandIntent.SETUP, + key="command-key-3", + ), + request_hash=None, + created_at=datetime(year=2021, month=1, day=1), + command_id="command-id-3", + ) + subject.handle_action(queue_3_setup) + + run_2_setup = actions.RunCommandAction( + command_id="command-id-2", + started_at=datetime(year=2022, month=2, day=2), + ) + subject.handle_action(run_2_setup) + fail_2_setup = actions.FailCommandAction( + command_id="command-id-2", + running_command=subject_view.get("command-id-2"), + error_id="error-id", + failed_at=datetime(year=2023, month=3, day=3), + error=errors.ProtocolEngineError(message="oh no"), + notes=[ + CommandNote( + noteKind="noteKind", + shortMessage="shortMessage", + longMessage="longMessage", + source="source", + ) + ], + type=ErrorRecoveryType.FAIL_RUN, + ) + subject.handle_action(fail_2_setup) + + assert [(c.id, c.status) for c in subject_view.get_all()] == [ + ("command-id-1", commands.CommandStatus.QUEUED), + ("command-id-2", commands.CommandStatus.FAILED), + ("command-id-3", commands.CommandStatus.FAILED), + ] + assert subject_view.get_running_command_id() is None + + subject.handle_action( + actions.PlayAction(requested_at=datetime.now(), deck_configuration=None) + ) + assert subject_view.get_next_to_execute() == "command-id-1" + + +def test_nonfatal_command_failure() -> None: + """Test the command queue if a command fails recoverably. + + Commands that were after the failed command in the queue should be left in + the queue. + + The queue status should be "awaiting-recovery." + """ + subject = CommandStore(is_door_open=False, config=_make_config()) + subject_view = CommandView(subject.state) + + queue_1 = actions.QueueCommandAction( + request=commands.WaitForResumeCreate( + params=commands.WaitForResumeParams(), key="command-key-1" + ), + request_hash=None, + created_at=datetime(year=2021, month=1, day=1), + command_id="command-id-1", + ) + subject.handle_action(queue_1) + queue_2 = actions.QueueCommandAction( + request=commands.WaitForResumeCreate( + params=commands.WaitForResumeParams(), key="command-key-2" + ), + request_hash=None, + created_at=datetime(year=2021, month=1, day=1), + command_id="command-id-2", + ) + subject.handle_action(queue_2) + + run_1 = actions.RunCommandAction( + command_id="command-id-1", + started_at=datetime(year=2022, month=2, day=2), + ) + subject.handle_action(run_1) + fail_1 = actions.FailCommandAction( + command_id="command-id-1", + running_command=subject_view.get("command-id-1"), + error_id="error-id", + failed_at=datetime(year=2023, month=3, day=3), + error=errors.ProtocolEngineError(message="oh no"), + notes=[ + CommandNote( + noteKind="noteKind", + shortMessage="shortMessage", + longMessage="longMessage", + source="source", + ) + ], + type=ErrorRecoveryType.WAIT_FOR_RECOVERY, + ) + subject.handle_action(fail_1) + + assert [(c.id, c.status) for c in subject_view.get_all()] == [ + ("command-id-1", commands.CommandStatus.FAILED), + ("command-id-2", commands.CommandStatus.QUEUED), + ] + assert subject_view.get_running_command_id() is None + + def test_error_recovery_type_tracking() -> None: """It should keep track of each failed command's error recovery type.""" subject = CommandStore(config=_make_config(), is_door_open=False) @@ -50,9 +317,11 @@ def test_error_recovery_type_tracking() -> None: subject.handle_action( actions.RunCommandAction(command_id="c1", started_at=datetime.now()) ) + running_command_1 = CommandView(subject.state).get("c1") subject.handle_action( actions.FailCommandAction( command_id="c1", + running_command=running_command_1, error_id="c1-error", failed_at=datetime.now(), error=PythonException(RuntimeError("new sheriff in town")), @@ -63,9 +332,11 @@ def test_error_recovery_type_tracking() -> None: subject.handle_action( actions.RunCommandAction(command_id="c2", started_at=datetime.now()) ) + running_command_2 = CommandView(subject.state).get("c2") subject.handle_action( actions.FailCommandAction( command_id="c2", + running_command=running_command_2, error_id="c2-error", failed_at=datetime.now(), error=PythonException(RuntimeError("new sheriff in town")), @@ -77,3 +348,89 @@ def test_error_recovery_type_tracking() -> None: view = CommandView(subject.state) assert view.get_error_recovery_type("c1") == ErrorRecoveryType.WAIT_FOR_RECOVERY assert view.get_error_recovery_type("c2") == ErrorRecoveryType.FAIL_RUN + + +def test_get_recovery_in_progress_for_command() -> None: + """It should return whether error recovery is in progress for the given command.""" + subject = CommandStore(config=_make_config(), is_door_open=False) + subject_view = CommandView(subject.state) + + queue_1 = actions.QueueCommandAction( + "c1", + created_at=datetime.now(), + request=commands.CommentCreate(params=commands.CommentParams(message="")), + request_hash=None, + ) + subject.handle_action(queue_1) + run_1 = actions.RunCommandAction(command_id="c1", started_at=datetime.now()) + subject.handle_action(run_1) + fail_1 = actions.FailCommandAction( + command_id="c1", + error_id="c1-error", + failed_at=datetime.now(), + error=PythonException(RuntimeError()), + notes=[], + type=ErrorRecoveryType.WAIT_FOR_RECOVERY, + running_command=subject_view.get("c1"), + ) + subject.handle_action(fail_1) + + # c1 failed recoverably and we're currently recovering from it. + assert subject_view.get_recovery_in_progress_for_command("c1") + + resume_from_1_recovery = actions.ResumeFromRecoveryAction() + subject.handle_action(resume_from_1_recovery) + + # c1 failed recoverably, but we've already completed its recovery. + assert not subject_view.get_recovery_in_progress_for_command("c1") + + queue_2 = actions.QueueCommandAction( + "c2", + created_at=datetime.now(), + request=commands.CommentCreate(params=commands.CommentParams(message="")), + request_hash=None, + ) + subject.handle_action(queue_2) + run_2 = actions.RunCommandAction(command_id="c2", started_at=datetime.now()) + subject.handle_action(run_2) + fail_2 = actions.FailCommandAction( + command_id="c2", + error_id="c2-error", + failed_at=datetime.now(), + error=PythonException(RuntimeError()), + notes=[], + type=ErrorRecoveryType.WAIT_FOR_RECOVERY, + running_command=subject_view.get("c2"), + ) + subject.handle_action(fail_2) + + # c2 failed recoverably and we're currently recovering from it. + assert subject_view.get_recovery_in_progress_for_command("c2") + # ...and that means we're *not* currently recovering from c1, + # even though it failed recoverably before. + assert not subject_view.get_recovery_in_progress_for_command("c1") + + resume_from_2_recovery = actions.ResumeFromRecoveryAction() + subject.handle_action(resume_from_2_recovery) + queue_3 = actions.QueueCommandAction( + "c3", + created_at=datetime.now(), + request=commands.CommentCreate(params=commands.CommentParams(message="")), + request_hash=None, + ) + subject.handle_action(queue_3) + run_3 = actions.RunCommandAction(command_id="c3", started_at=datetime.now()) + subject.handle_action(run_3) + fail_3 = actions.FailCommandAction( + command_id="c3", + error_id="c3-error", + failed_at=datetime.now(), + error=PythonException(RuntimeError()), + notes=[], + type=ErrorRecoveryType.FAIL_RUN, + running_command=subject_view.get("c3"), + ) + subject.handle_action(fail_3) + + # c3 failed, but not recoverably. + assert not subject_view.get_recovery_in_progress_for_command("c2") diff --git a/api/tests/opentrons/protocol_engine/state/test_command_store_old.py b/api/tests/opentrons/protocol_engine/state/test_command_store_old.py index 7afde4a6e4b..a859ae7573b 100644 --- a/api/tests/opentrons/protocol_engine/state/test_command_store_old.py +++ b/api/tests/opentrons/protocol_engine/state/test_command_store_old.py @@ -14,12 +14,10 @@ from opentrons.ordered_set import OrderedSet from opentrons.protocol_engine.actions.actions import RunCommandAction -from opentrons.protocol_engine.notes.notes import CommandNote from opentrons.types import MountType, DeckSlotName from opentrons.hardware_control.types import DoorState from opentrons.protocol_engine import commands, errors -from opentrons.protocol_engine.error_recovery_policy import ErrorRecoveryType from opentrons.protocol_engine.types import DeckSlotLocation, DeckType, WellLocation from opentrons.protocol_engine.state import Config from opentrons.protocol_engine.state.commands import ( @@ -33,7 +31,6 @@ from opentrons.protocol_engine.actions import ( QueueCommandAction, SucceedCommandAction, - FailCommandAction, PlayAction, PauseAction, PauseSource, @@ -86,6 +83,7 @@ def test_initial_state( finish_error=None, failed_command=None, command_error_recovery_types={}, + recovery_target_command_id=None, latest_command_hash=None, stopped_by_estop=False, ) @@ -429,321 +427,6 @@ def test_running_command_id() -> None: assert subject.state.command_history.get_running_command() is None -def test_command_failure_clears_queues() -> None: - """It should clear the command queue on command failure.""" - queue_1 = QueueCommandAction( - request=commands.WaitForResumeCreate( - params=commands.WaitForResumeParams(), key="command-key-1" - ), - request_hash=None, - created_at=datetime(year=2021, month=1, day=1), - command_id="command-id-1", - ) - queue_2 = QueueCommandAction( - request=commands.WaitForResumeCreate( - params=commands.WaitForResumeParams(), key="command-key-2" - ), - request_hash=None, - created_at=datetime(year=2021, month=1, day=1), - command_id="command-id-2", - ) - run_1 = RunCommandAction( - command_id="command-id-1", - started_at=datetime(year=2022, month=2, day=2), - ) - fail_1 = FailCommandAction( - command_id="command-id-1", - error_id="error-id", - failed_at=datetime(year=2023, month=3, day=3), - error=errors.ProtocolEngineError(message="oh no"), - notes=[ - CommandNote( - noteKind="noteKind", - shortMessage="shortMessage", - longMessage="longMessage", - source="source", - ) - ], - type=ErrorRecoveryType.FAIL_RUN, - ) - - expected_failed_1 = commands.WaitForResume( - id="command-id-1", - key="command-key-1", - error=errors.ErrorOccurrence( - id="error-id", - createdAt=datetime(year=2023, month=3, day=3), - errorCode=ErrorCodes.GENERAL_ERROR.value.code, - errorType="ProtocolEngineError", - detail="oh no", - ), - notes=[ - CommandNote( - noteKind="noteKind", - shortMessage="shortMessage", - longMessage="longMessage", - source="source", - ) - ], - createdAt=datetime(year=2021, month=1, day=1), - startedAt=datetime(year=2022, month=2, day=2), - completedAt=datetime(year=2023, month=3, day=3), - params=commands.WaitForResumeParams(), - status=commands.CommandStatus.FAILED, - ) - expected_failed_2 = commands.WaitForResume( - id="command-id-2", - key="command-key-2", - error=None, - createdAt=datetime(year=2021, month=1, day=1), - completedAt=datetime(year=2023, month=3, day=3), - params=commands.WaitForResumeParams(), - status=commands.CommandStatus.FAILED, - ) - - subject = CommandStore(is_door_open=False, config=_make_config()) - - subject.handle_action(queue_1) - subject.handle_action(queue_2) - subject.handle_action(run_1) - subject.handle_action(fail_1) - - assert subject.state.command_history.get_running_command() is None - assert subject.state.command_history.get_queue_ids() == OrderedSet() - assert subject.state.command_history.get_all_ids() == [ - "command-id-1", - "command-id-2", - ] - assert subject.state.command_history.get("command-id-1") == CommandEntry( - index=0, command=expected_failed_1 - ) - assert subject.state.command_history.get("command-id-2") == CommandEntry( - index=1, command=expected_failed_2 - ) - - -def test_setup_command_failure_only_clears_setup_command_queue() -> None: - """It should clear only the setup command queue for a failed setup command. - - This test queues up a non-setup command followed by two setup commands, - then attempts to run and fail the first setup command and - """ - cmd_1_non_setup = commands.WaitForResume( - id="command-id-1", - key="command-key-1", - createdAt=datetime(year=2021, month=1, day=1), - params=commands.WaitForResumeParams(), - status=commands.CommandStatus.QUEUED, - ) - queue_action_1_non_setup = QueueCommandAction( - request=commands.WaitForResumeCreate( - params=cmd_1_non_setup.params, key="command-key-1" - ), - request_hash=None, - created_at=datetime(year=2021, month=1, day=1), - command_id="command-id-1", - ) - queue_action_2_setup = QueueCommandAction( - request=commands.WaitForResumeCreate( - params=commands.WaitForResumeParams(), - intent=commands.CommandIntent.SETUP, - key="command-key-2", - ), - request_hash=None, - created_at=datetime(year=2021, month=1, day=1), - command_id="command-id-2", - ) - queue_action_3_setup = QueueCommandAction( - request=commands.WaitForResumeCreate( - params=commands.WaitForResumeParams(), - intent=commands.CommandIntent.SETUP, - key="command-key-3", - ), - request_hash=None, - created_at=datetime(year=2021, month=1, day=1), - command_id="command-id-3", - ) - - run_action_cmd_2 = RunCommandAction( - command_id="command-id-2", - started_at=datetime(year=2022, month=2, day=2), - ) - failed_action_cmd_2 = FailCommandAction( - command_id="command-id-2", - error_id="error-id", - failed_at=datetime(year=2023, month=3, day=3), - error=errors.ProtocolEngineError(message="oh no"), - notes=[ - CommandNote( - noteKind="noteKind", - shortMessage="shortMessage", - longMessage="longMessage", - source="source", - ) - ], - type=ErrorRecoveryType.FAIL_RUN, - ) - expected_failed_cmd_2 = commands.WaitForResume( - id="command-id-2", - key="command-key-2", - error=errors.ErrorOccurrence( - id="error-id", - createdAt=datetime(year=2023, month=3, day=3), - errorType="ProtocolEngineError", - detail="oh no", - errorCode=ErrorCodes.GENERAL_ERROR.value.code, - ), - notes=[ - CommandNote( - noteKind="noteKind", - shortMessage="shortMessage", - longMessage="longMessage", - source="source", - ) - ], - createdAt=datetime(year=2021, month=1, day=1), - startedAt=datetime(year=2022, month=2, day=2), - completedAt=datetime(year=2023, month=3, day=3), - params=commands.WaitForResumeParams(), - status=commands.CommandStatus.FAILED, - intent=commands.CommandIntent.SETUP, - ) - expected_failed_cmd_3 = commands.WaitForResume( - id="command-id-3", - key="command-key-3", - error=None, - createdAt=datetime(year=2021, month=1, day=1), - completedAt=datetime(year=2023, month=3, day=3), - params=commands.WaitForResumeParams(), - status=commands.CommandStatus.FAILED, - intent=commands.CommandIntent.SETUP, - ) - - subject = CommandStore(is_door_open=False, config=_make_config()) - - subject.handle_action(queue_action_1_non_setup) - subject.handle_action(queue_action_2_setup) - subject.handle_action(queue_action_3_setup) - subject.handle_action(run_action_cmd_2) - subject.handle_action(failed_action_cmd_2) - - assert subject.state.command_history.get_running_command() is None - assert subject.state.command_history.get_setup_queue_ids() == OrderedSet() - assert subject.state.command_history.get_queue_ids() == OrderedSet(["command-id-1"]) - assert subject.state.command_history.get_all_ids() == [ - "command-id-1", - "command-id-2", - "command-id-3", - ] - assert subject.state.command_history.get("command-id-1") == CommandEntry( - index=0, command=cmd_1_non_setup - ) - assert subject.state.command_history.get("command-id-2") == CommandEntry( - index=1, command=expected_failed_cmd_2 - ) - assert subject.state.command_history.get("command-id-3") == CommandEntry( - index=2, command=expected_failed_cmd_3 - ) - - -def test_nonfatal_command_failure() -> None: - """Test the command queue if a command fails recoverably. - - Commands that were after the failed command in the queue should be left in - the queue. - """ - queue_1 = QueueCommandAction( - request=commands.WaitForResumeCreate( - params=commands.WaitForResumeParams(), key="command-key-1" - ), - request_hash=None, - created_at=datetime(year=2021, month=1, day=1), - command_id="command-id-1", - ) - queue_2 = QueueCommandAction( - request=commands.WaitForResumeCreate( - params=commands.WaitForResumeParams(), key="command-key-2" - ), - request_hash=None, - created_at=datetime(year=2021, month=1, day=1), - command_id="command-id-2", - ) - run_1 = RunCommandAction( - command_id="command-id-1", - started_at=datetime(year=2022, month=2, day=2), - ) - fail_1 = FailCommandAction( - command_id="command-id-1", - error_id="error-id", - failed_at=datetime(year=2023, month=3, day=3), - error=errors.ProtocolEngineError(message="oh no"), - notes=[ - CommandNote( - noteKind="noteKind", - shortMessage="shortMessage", - longMessage="longMessage", - source="source", - ) - ], - type=ErrorRecoveryType.WAIT_FOR_RECOVERY, - ) - - expected_failed_1 = commands.WaitForResume( - id="command-id-1", - key="command-key-1", - error=errors.ErrorOccurrence( - id="error-id", - createdAt=datetime(year=2023, month=3, day=3), - errorCode=ErrorCodes.GENERAL_ERROR.value.code, - errorType="ProtocolEngineError", - detail="oh no", - ), - notes=[ - CommandNote( - noteKind="noteKind", - shortMessage="shortMessage", - longMessage="longMessage", - source="source", - ) - ], - createdAt=datetime(year=2021, month=1, day=1), - startedAt=datetime(year=2022, month=2, day=2), - completedAt=datetime(year=2023, month=3, day=3), - params=commands.WaitForResumeParams(), - status=commands.CommandStatus.FAILED, - ) - expected_queued_2 = commands.WaitForResume( - id="command-id-2", - key="command-key-2", - error=None, - createdAt=datetime(year=2021, month=1, day=1), - startedAt=None, - completedAt=None, - params=commands.WaitForResumeParams(), - status=commands.CommandStatus.QUEUED, - ) - - subject = CommandStore(is_door_open=False, config=_make_config()) - - subject.handle_action(queue_1) - subject.handle_action(queue_2) - subject.handle_action(run_1) - subject.handle_action(fail_1) - - assert subject.state.command_history.get_running_command() is None - assert subject.state.command_history.get_queue_ids() == OrderedSet(["command-id-2"]) - assert subject.state.command_history.get_all_ids() == [ - "command-id-1", - "command-id-2", - ] - assert subject.state.command_history.get("command-id-1") == CommandEntry( - index=0, command=expected_failed_1 - ) - assert subject.state.command_history.get("command-id-2") == CommandEntry( - index=1, command=expected_queued_2 - ) - - def test_command_store_keeps_commands_in_queue_order() -> None: """It should keep commands in the order they were originally enqueued.""" command_create_1_non_setup = commands.CommentCreate( @@ -834,6 +517,7 @@ def test_command_store_handles_pause_action(pause_source: PauseSource) -> None: finish_error=None, failed_command=None, command_error_recovery_types={}, + recovery_target_command_id=None, latest_command_hash=None, stopped_by_estop=False, ) @@ -859,6 +543,7 @@ def test_command_store_handles_play_action(pause_source: PauseSource) -> None: finish_error=None, failed_command=None, command_error_recovery_types={}, + recovery_target_command_id=None, run_started_at=datetime(year=2021, month=1, day=1), latest_command_hash=None, stopped_by_estop=False, @@ -890,6 +575,7 @@ def test_command_store_handles_finish_action() -> None: finish_error=None, failed_command=None, command_error_recovery_types={}, + recovery_target_command_id=None, run_started_at=datetime(year=2021, month=1, day=1), latest_command_hash=None, stopped_by_estop=False, @@ -936,6 +622,7 @@ def test_command_store_handles_stop_action(from_estop: bool) -> None: finish_error=None, failed_command=None, command_error_recovery_types={}, + recovery_target_command_id=None, run_started_at=datetime(year=2021, month=1, day=1), latest_command_hash=None, stopped_by_estop=from_estop, @@ -966,6 +653,7 @@ def test_command_store_cannot_restart_after_should_stop() -> None: finish_error=None, failed_command=None, command_error_recovery_types={}, + recovery_target_command_id=None, run_started_at=None, latest_command_hash=None, stopped_by_estop=False, @@ -1098,6 +786,7 @@ def test_command_store_wraps_unknown_errors() -> None: run_started_at=None, failed_command=None, command_error_recovery_types={}, + recovery_target_command_id=None, latest_command_hash=None, stopped_by_estop=False, ) @@ -1159,6 +848,7 @@ def __init__(self, message: str) -> None: ), failed_command=None, command_error_recovery_types={}, + recovery_target_command_id=None, run_started_at=None, latest_command_hash=None, stopped_by_estop=False, @@ -1191,6 +881,7 @@ def test_command_store_ignores_stop_after_graceful_finish() -> None: finish_error=None, failed_command=None, command_error_recovery_types={}, + recovery_target_command_id=None, run_started_at=datetime(year=2021, month=1, day=1), latest_command_hash=None, stopped_by_estop=False, @@ -1223,6 +914,7 @@ def test_command_store_ignores_finish_after_non_graceful_stop() -> None: finish_error=None, failed_command=None, command_error_recovery_types={}, + recovery_target_command_id=None, run_started_at=datetime(year=2021, month=1, day=1), latest_command_hash=None, stopped_by_estop=False, @@ -1233,102 +925,6 @@ def test_command_store_ignores_finish_after_non_graceful_stop() -> None: assert subject.state.command_history.get_setup_queue_ids() == OrderedSet() -def test_command_store_handles_command_failed() -> None: - """It should store an error and mark the command if it fails.""" - error_recovery_type = ErrorRecoveryType.FAIL_RUN - - expected_error_occurrence = errors.ErrorOccurrence( - id="error-id", - errorType="ProtocolEngineError", - createdAt=datetime(year=2023, month=3, day=3), - detail="oh no", - errorCode=ErrorCodes.GENERAL_ERROR.value.code, - ) - - expected_failed_command = commands.Comment( - id="command-id", - commandType="comment", - key="command-key", - createdAt=datetime(year=2021, month=1, day=1), - startedAt=datetime(year=2022, month=2, day=2), - completedAt=expected_error_occurrence.createdAt, - status=commands.CommandStatus.FAILED, - params=commands.CommentParams(message="hello, world"), - result=None, - error=expected_error_occurrence, - notes=[ - CommandNote( - noteKind="noteKind", - shortMessage="shortMessage", - longMessage="longMessage", - source="source", - ) - ], - ) - - subject = CommandStore(is_door_open=False, config=_make_config()) - - subject.handle_action( - QueueCommandAction( - command_id=expected_failed_command.id, - created_at=expected_failed_command.createdAt, - request=commands.CommentCreate( - params=expected_failed_command.params, key=expected_failed_command.key - ), - request_hash=None, - ) - ) - subject.handle_action( - RunCommandAction( - command_id=expected_failed_command.id, - # Ignore arg-type errors because we know this isn't None. - started_at=expected_failed_command.startedAt, # type: ignore[arg-type] - ) - ) - subject.handle_action( - FailCommandAction( - command_id=expected_failed_command.id, - error_id=expected_error_occurrence.id, - failed_at=expected_error_occurrence.createdAt, - error=errors.ProtocolEngineError(message="oh no"), - notes=[ - CommandNote( - noteKind="noteKind", - shortMessage="shortMessage", - longMessage="longMessage", - source="source", - ) - ], - type=error_recovery_type, - ) - ) - - failed_command_entry = CommandEntry(index=0, command=expected_failed_command) - command_history = CommandHistory() - command_history._add("command-id", failed_command_entry) - command_history._set_terminal_command_id("command-id") - - assert subject.state == CommandState( - command_history=command_history, - queue_status=QueueStatus.SETUP, - run_result=None, - run_completed_at=None, - is_door_blocking=False, - run_error=None, - finish_error=None, - failed_command=failed_command_entry, - command_error_recovery_types={expected_failed_command.id: error_recovery_type}, - run_started_at=None, - latest_command_hash=None, - stopped_by_estop=False, - ) - assert subject.state.command_history.get_running_command() is None - assert subject.state.command_history.get_all_ids() == ["command-id"] - assert subject.state.command_history.get_queue_ids() == OrderedSet() - assert subject.state.command_history.get_setup_queue_ids() == OrderedSet() - assert subject.state.command_history.get("command-id") == failed_command_entry - - def test_handles_hardware_stopped() -> None: """It should mark the hardware as stopped on HardwareStoppedAction.""" subject = CommandStore(is_door_open=False, config=_make_config()) @@ -1347,6 +943,7 @@ def test_handles_hardware_stopped() -> None: finish_error=None, failed_command=None, command_error_recovery_types={}, + recovery_target_command_id=None, run_started_at=None, latest_command_hash=None, stopped_by_estop=False, diff --git a/api/tests/opentrons/protocol_engine/state/test_command_view_old.py b/api/tests/opentrons/protocol_engine/state/test_command_view_old.py index 64d7670f662..a9b5fc92cc3 100644 --- a/api/tests/opentrons/protocol_engine/state/test_command_view_old.py +++ b/api/tests/opentrons/protocol_engine/state/test_command_view_old.py @@ -58,6 +58,7 @@ def get_command_view( run_error: Optional[errors.ErrorOccurrence] = None, failed_command: Optional[CommandEntry] = None, command_error_recovery_types: Optional[Dict[str, ErrorRecoveryType]] = None, + recovery_target_command_id: Optional[str] = None, finish_error: Optional[errors.ErrorOccurrence] = None, commands: Sequence[cmd.Command] = (), latest_command_hash: Optional[str] = None, @@ -90,6 +91,7 @@ def get_command_view( finish_error=finish_error, failed_command=failed_command, command_error_recovery_types=command_error_recovery_types or {}, + recovery_target_command_id=recovery_target_command_id, run_started_at=run_started_at, latest_command_hash=latest_command_hash, stopped_by_estop=False, diff --git a/api/tests/opentrons/protocol_engine/state/test_state_store.py b/api/tests/opentrons/protocol_engine/state/test_state_store.py index dd32bbec591..170f05bb4b9 100644 --- a/api/tests/opentrons/protocol_engine/state/test_state_store.py +++ b/api/tests/opentrons/protocol_engine/state/test_state_store.py @@ -1,5 +1,5 @@ """Tests for the top-level StateStore/StateView.""" -from typing import Callable, Optional +from typing import Callable, Union from datetime import datetime import pytest @@ -80,47 +80,52 @@ def test_notify_on_state_change( decoy.verify(change_notifier.notify(), times=1) -async def test_wait_for_state( +async def test_wait_for( decoy: Decoy, change_notifier: ChangeNotifier, subject: StateStore, ) -> None: """It should return an awaitable that signals state changes.""" - check_condition: Callable[..., Optional[str]] = decoy.mock(name="check_condition") + check_condition: Callable[..., Union[str, int]] = decoy.mock(name="check_condition") decoy.when(check_condition("foo", bar="baz")).then_return( - None, - None, + 0, + 0, "hello world", ) - result = await subject.wait_for(check_condition, "foo", bar="baz") assert result == "hello world" + decoy.verify(await change_notifier.wait(), times=2) + decoy.reset() + + decoy.when(check_condition("foo", bar="baz")).then_return( + "hello world", + "hello world again", + 0, + ) + result = await subject.wait_for_not(check_condition, "foo", bar="baz") + assert result == 0 decoy.verify(await change_notifier.wait(), times=2) -async def test_wait_for_state_short_circuit( +async def test_wait_for_already_satisfied( decoy: Decoy, subject: StateStore, change_notifier: ChangeNotifier, ) -> None: - """It should short-circuit the change notifier if condition is satisfied.""" - check_condition: Callable[..., Optional[str]] = decoy.mock(name="check_condition") + """It should return immediately and skip the change notifier.""" + check_condition: Callable[..., Union[str, int]] = decoy.mock(name="check_condition") decoy.when(check_condition("foo", bar="baz")).then_return("hello world") - result = await subject.wait_for(check_condition, "foo", bar="baz") assert result == "hello world" - decoy.verify(await change_notifier.wait(), times=0) - -async def test_wait_for_already_true(decoy: Decoy, subject: StateStore) -> None: - """It should signal immediately if condition is already met.""" - check_condition = decoy.mock(name="check_condition") - decoy.when(check_condition()).then_return(True) - await subject.wait_for(check_condition) + decoy.when(check_condition("foo", bar="baz")).then_return(0) + result = await subject.wait_for_not(check_condition, "foo", bar="baz") + assert result == 0 + decoy.verify(await change_notifier.wait(), times=0) async def test_wait_for_raises(decoy: Decoy, subject: StateStore) -> None: @@ -131,3 +136,6 @@ async def test_wait_for_raises(decoy: Decoy, subject: StateStore) -> None: with pytest.raises(ValueError, match="oh no"): await subject.wait_for(check_condition) + + with pytest.raises(ValueError, match="oh no"): + await subject.wait_for_not(check_condition) diff --git a/api/tests/opentrons/protocol_engine/test_protocol_engine.py b/api/tests/opentrons/protocol_engine/test_protocol_engine.py index 2191b1c4954..dd96b8d968a 100644 --- a/api/tests/opentrons/protocol_engine/test_protocol_engine.py +++ b/api/tests/opentrons/protocol_engine/test_protocol_engine.py @@ -2,6 +2,7 @@ import inspect from datetime import datetime from typing import Any +from unittest.mock import sentinel import pytest from decoy import Decoy @@ -333,6 +334,99 @@ def _stub_completed(*_a: object, **_k: object) -> bool: assert result == completed +async def test_add_and_execute_command_wait_for_recovery( + decoy: Decoy, + state_store: StateStore, + action_dispatcher: ActionDispatcher, + model_utils: ModelUtils, + subject: ProtocolEngine, +) -> None: + """It should add and execute a command from a request.""" + created_at = datetime(year=2021, month=1, day=1) + original_request = commands.WaitForResumeCreate( + params=commands.WaitForResumeParams() + ) + standardized_request = commands.HomeCreate(params=commands.HomeParams()) + queued = commands.Home( + id="command-id", + key="command-key", + status=commands.CommandStatus.QUEUED, + createdAt=created_at, + params=commands.HomeParams(), + ) + completed = commands.Home( + id="command-id", + key="command-key", + status=commands.CommandStatus.SUCCEEDED, + createdAt=created_at, + params=commands.HomeParams(), + ) + + robot_type: RobotType = "OT-3 Standard" + decoy.when(state_store.config).then_return( + Config(robot_type=robot_type, deck_type=DeckType.OT3_STANDARD) + ) + + decoy.when( + slot_standardization.standardize_command(original_request, robot_type) + ).then_return(standardized_request) + + decoy.when(model_utils.generate_id()).then_return("command-id") + decoy.when(model_utils.get_timestamp()).then_return(created_at) + + def _stub_queued(*_a: object, **_k: object) -> None: + decoy.when(state_store.commands.get("command-id")).then_return(queued) + + def _stub_completed(*_a: object, **_k: object) -> bool: + decoy.when(state_store.commands.get("command-id")).then_return(completed) + return True + + decoy.when( + state_store.commands.validate_action_allowed( + QueueCommandAction( + command_id="command-id", + created_at=created_at, + request=standardized_request, + request_hash=None, + ) + ) + ).then_return( + QueueCommandAction( + command_id="command-id-validated", + created_at=created_at, + request=standardized_request, + request_hash=None, + ) + ) + + decoy.when( + action_dispatcher.dispatch( + QueueCommandAction( + command_id="command-id-validated", + created_at=created_at, + request=standardized_request, + request_hash=None, + ) + ) + ).then_do(_stub_queued) + + decoy.when( + await state_store.wait_for( + condition=state_store.commands.get_command_is_final, + command_id="command-id", + ), + ).then_do(_stub_completed) + + result = await subject.add_and_execute_command_wait_for_recovery(original_request) + assert result == completed + decoy.verify( + await state_store.wait_for_not( + state_store.commands.get_recovery_in_progress_for_command, + "command-id", + ) + ) + + def test_play( decoy: Decoy, state_store: StateStore, @@ -764,6 +858,8 @@ async def test_estop_during_command( """It should be able to stop the engine.""" timestamp = datetime(2021, 1, 1, 0, 0) command_id = "command_fake_id" + running_command = sentinel.running_command + queued_command = sentinel.queued_command error_id = "fake_error_id" fake_command_set = OrderedSet(["fake-id-1", "fake-id-1"]) @@ -771,10 +867,15 @@ async def test_estop_during_command( decoy.when(model_utils.generate_id()).then_return(error_id) decoy.when(state_store.commands.get_is_stopped()).then_return(False) decoy.when(state_store.commands.get_running_command_id()).then_return(command_id) + decoy.when(state_store.commands.get(command_id)).then_return(running_command) decoy.when(state_store.commands.get_queue_ids()).then_return(fake_command_set) + decoy.when(state_store.commands.get(fake_command_set.head())).then_return( + queued_command + ) expected_action = FailCommandAction( command_id=command_id, + running_command=running_command, error_id=error_id, failed_at=timestamp, error=EStopActivatedError(message="Estop Activated"), @@ -783,6 +884,7 @@ async def test_estop_during_command( ) expected_action_2 = FailCommandAction( command_id=fake_command_set.head(), + running_command=queued_command, error_id=error_id, failed_at=timestamp, error=EStopActivatedError(message="Estop Activated"), diff --git a/api/tests/opentrons/protocol_runner/test_legacy_command_mapper.py b/api/tests/opentrons/protocol_runner/test_legacy_command_mapper.py index 23b7ecac3bb..f0412878856 100644 --- a/api/tests/opentrons/protocol_runner/test_legacy_command_mapper.py +++ b/api/tests/opentrons/protocol_runner/test_legacy_command_mapper.py @@ -156,6 +156,7 @@ def test_map_after_with_error_command() -> None: assert result == [ pe_actions.FailCommandAction( command_id="command.COMMENT-0", + running_command=matchers.Anything(), error_id=matchers.IsA(str), failed_at=matchers.IsA(datetime), error=matchers.ErrorMatching( @@ -257,6 +258,7 @@ def test_command_stack() -> None: ), pe_actions.FailCommandAction( command_id="command.COMMENT-1", + running_command=matchers.Anything(), error_id=matchers.IsA(str), failed_at=matchers.IsA(datetime), error=matchers.ErrorMatching(LegacyContextCommandError, "oh no"), From 2cff9d24c542185c8d7de5efbfa1c137b64ac210 Mon Sep 17 00:00:00 2001 From: Ryan Howard Date: Tue, 9 Apr 2024 12:48:20 -0400 Subject: [PATCH 078/194] feat(hardware-testing): liquid sense testing script (#14807) # Overview This PR adds a new testing script that allows us to test all kinds of variations of the liquid-sense routine it adds some additional features in the hardware control layer to change up output options to during the probe so we can gate using the buffer-on-pipette feature to a firmware version flag, since that feature has to be compiled in separately # Test Plan # Changelog # Review requests # Risk assessment --------- Co-authored-by: caila-marashaj --- hardware-testing/Makefile | 8 + .../gravimetric/measurement/record.py | 13 +- .../labware/dial_indicator/1.json | 57 ++++ .../hardware_testing/liquid_sense/__init__.py | 1 + .../hardware_testing/liquid_sense/__main__.py | 317 ++++++++++++++++++ .../hardware_testing/liquid_sense/execute.py | 307 +++++++++++++++++ .../liquid_sense/post_process.py | 170 ++++++++++ .../hardware_testing/liquid_sense/report.py | 263 +++++++++++++++ .../opentrons_api/helpers_ot3.py | 4 +- .../protocols/liquid_sense_lpc/__init__.py | 1 + .../liquid_sense_ot3_p1000_96.py | 33 ++ .../liquid_sense_ot3_p1000_multi.py | 26 ++ .../liquid_sense_ot3_p1000_single.py | 33 ++ .../liquid_sense_ot3_p50_multi.py | 28 ++ .../liquid_sense_ot3_p50_single.py | 31 ++ .../firmware_bindings/messages/messages.py | 1 + .../hardware_control/tool_sensors.py | 3 +- 17 files changed, 1287 insertions(+), 9 deletions(-) create mode 100644 hardware-testing/hardware_testing/labware/dial_indicator/1.json create mode 100644 hardware-testing/hardware_testing/liquid_sense/__init__.py create mode 100644 hardware-testing/hardware_testing/liquid_sense/__main__.py create mode 100644 hardware-testing/hardware_testing/liquid_sense/execute.py create mode 100644 hardware-testing/hardware_testing/liquid_sense/post_process.py create mode 100644 hardware-testing/hardware_testing/liquid_sense/report.py create mode 100644 hardware-testing/hardware_testing/protocols/liquid_sense_lpc/__init__.py create mode 100644 hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p1000_96.py create mode 100644 hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p1000_multi.py create mode 100644 hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p1000_single.py create mode 100644 hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p50_multi.py create mode 100644 hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p50_single.py diff --git a/hardware-testing/Makefile b/hardware-testing/Makefile index 6c12dc305a0..a48b794977f 100755 --- a/hardware-testing/Makefile +++ b/hardware-testing/Makefile @@ -155,6 +155,14 @@ test-examples: test-scripts: $(python) -m hardware_testing.scripts.bowtie_ot3 --simulate +.PHONY: test-liquid-sense +test-liquid-sense: + $(python) -m hardware_testing.liquid_sense --simulate --pipette 1000 --channels 1 + $(python) -m hardware_testing.liquid_sense --simulate --pipette 50 --channels 1 + $(python) -m hardware_testing.liquid_sense --simulate --pipette 1000 --channels 8 + $(python) -m hardware_testing.liquid_sense --simulate --pipette 50 --channels 8 + $(python) -m hardware_testing.liquid_sense --simulate --pipette 1000 --channels 96 + .PHONY: test-integration test-integration: test-production-qc test-examples test-scripts test-gravimetric diff --git a/hardware-testing/hardware_testing/gravimetric/measurement/record.py b/hardware-testing/hardware_testing/gravimetric/measurement/record.py index d1e4ab7e4d4..86ef8b84903 100644 --- a/hardware-testing/hardware_testing/gravimetric/measurement/record.py +++ b/hardware-testing/hardware_testing/gravimetric/measurement/record.py @@ -280,7 +280,11 @@ class GravimetricRecorder: """Gravimetric Recorder.""" def __init__( - self, cfg: GravimetricRecorderConfig, scale: Scale, simulate: bool = False + self, + cfg: GravimetricRecorderConfig, + scale: Scale, + simulate: bool = False, + start_graph: bool = True, ) -> None: """Gravimetric Recorder.""" self._cfg = cfg @@ -294,7 +298,7 @@ def __init__( self._scale_serial: str = "" self._scale_max_capacity: float = 0.0 super().__init__() - self.activate() + self.activate(start_graph) def _start_graph_server_process(self) -> None: if self.is_simulator: @@ -350,9 +354,10 @@ def add_simulation_mass(self, mass: float) -> None: """Add simulation mass.""" self._scale.add_simulation_mass(mass) - def activate(self) -> None: + def activate(self, graph: bool = True) -> None: """Activate.""" - self._start_graph_server_process() + if graph: + self._start_graph_server_process() # Some Radwag settings cannot be controlled remotely. # Listed below are the things the must be done using the touchscreen: # 1) Set profile to USER diff --git a/hardware-testing/hardware_testing/labware/dial_indicator/1.json b/hardware-testing/hardware_testing/labware/dial_indicator/1.json new file mode 100644 index 00000000000..6c3ac9c3f24 --- /dev/null +++ b/hardware-testing/hardware_testing/labware/dial_indicator/1.json @@ -0,0 +1,57 @@ +{ + "schemaVersion": 2, + "version": 1, + "namespace": "custom_beta", + "ordering": [["A1"]], + "metadata": { + "displayName": "Mitutoyo Digimatic Indicator", + "displayCategory": "tubeRack", + "displayVolumeUnits": "µL", + "tags": [] + }, + "dimensions": { + "xDimension": 128, + "yDimension": 86, + "zDimension": 136 + }, + "parameters": { + "format": "irregular", + "quirks": [], + "isTiprack": false, + "isMagneticModuleCompatible": false, + "loadName": "dial_indicator" + }, + "wells": { + "A1": { + "depth": 14, + "totalLiquidVolume": 10, + "shape": "circular", + "diameter": 4, + "x": 60.8, + "y": 41.5, + "z": 135 + } + }, + "brand": { + "brand": "Mitutoyo", + "brandId": ["ID-S"] + }, + "groups": [ + { + "brand": { + "brand": "Mitutoyo", + "brandId": ["ID-S"] + }, + "metadata": { + "wellBottomShape": "flat", + "displayCategory": "tubeRack" + }, + "wells": ["A1"] + } + ], + "cornerOffsetFromSlot": { + "x": 0, + "y": 0, + "z": 0 + } +} diff --git a/hardware-testing/hardware_testing/liquid_sense/__init__.py b/hardware-testing/hardware_testing/liquid_sense/__init__.py new file mode 100644 index 00000000000..e6b26332d7b --- /dev/null +++ b/hardware-testing/hardware_testing/liquid_sense/__init__.py @@ -0,0 +1 @@ +"""Liquid Sense.""" diff --git a/hardware-testing/hardware_testing/liquid_sense/__main__.py b/hardware-testing/hardware_testing/liquid_sense/__main__.py new file mode 100644 index 00000000000..10db70e67c8 --- /dev/null +++ b/hardware-testing/hardware_testing/liquid_sense/__main__.py @@ -0,0 +1,317 @@ +"""Liquid sense testing.""" +import argparse +from dataclasses import dataclass +from json import load as json_load +from pathlib import Path +import subprocess +from time import sleep +import os +from typing import List, Any, Optional +import traceback + +from hardware_testing.opentrons_api import helpers_ot3 +from hardware_testing.gravimetric import helpers, workarounds +from hardware_testing.data.csv_report import CSVReport +from hardware_testing.gravimetric.measurement.record import GravimetricRecorder +from hardware_testing.gravimetric.measurement.scale import Scale +from hardware_testing.drivers import ( + asair_sensor, + mitutoyo_digimatic_indicator, + list_ports_and_select, +) +from hardware_testing.data import ( + ui, + create_run_id_and_start_time, + get_git_description, + get_testing_data_directory, +) + +from opentrons.protocol_api import InstrumentContext, ProtocolContext +from opentrons.protocol_engine.types import LabwareOffset + +from hardware_testing.liquid_sense import execute +from .report import build_ls_report, store_config, store_serial_numbers +from .post_process import process_csv_directory + +from hardware_testing.protocols.liquid_sense_lpc import ( + liquid_sense_ot3_p50_single, + liquid_sense_ot3_p50_multi, + liquid_sense_ot3_p1000_single, + liquid_sense_ot3_p1000_multi, + liquid_sense_ot3_p1000_96, +) + +API_LEVEL = "2.18" + +LABWARE_OFFSETS: List[LabwareOffset] = [] + + +LIQUID_SENSE_CFG = { + 50: { + 1: liquid_sense_ot3_p50_single, + 8: liquid_sense_ot3_p50_multi, + }, + 1000: { + 1: liquid_sense_ot3_p1000_single, + 8: liquid_sense_ot3_p1000_multi, + 96: liquid_sense_ot3_p1000_96, + }, +} + +PIPETTE_MODEL_NAME = { + 50: { + 1: "p50_single_flex", + 8: "p50_multi_flex", + }, + 1000: { + 1: "p1000_single_flex", + 8: "p1000_multi_flex", + 96: "p1000_96_flex", + }, +} + + +@dataclass +class RunArgs: + """Common resources across multiple runs.""" + + tip_volumes: List[int] + run_id: str + pipette: InstrumentContext + pipette_tag: str + git_description: str + robot_serial: str + recorder: GravimetricRecorder + pipette_volume: int + pipette_channels: int + name: str + environment_sensor: asair_sensor.AsairSensorBase + trials: int + z_speed: float + return_tip: bool + ctx: ProtocolContext + protocol_cfg: Any + test_report: CSVReport + start_height_offset: float + aspirate: bool + dial_indicator: Optional[mitutoyo_digimatic_indicator.Mitutoyo_Digimatic_Indicator] + plunger_speed: bool + trials_before_jog: int + + @classmethod + def _get_protocol_context(cls, args: argparse.Namespace) -> ProtocolContext: + if not args.simulate and not args.skip_labware_offsets: + # getting labware offsets must be done before creating the protocol context + # because it requires the robot-server to be running + ui.print_title("SETUP") + ui.print_info( + "Starting opentrons-robot-server, so we can http GET labware offsets" + ) + LABWARE_OFFSETS.extend(workarounds.http_get_all_labware_offsets()) + ui.print_info(f"found {len(LABWARE_OFFSETS)} offsets:") + for offset in LABWARE_OFFSETS: + ui.print_info(f"\t{offset.createdAt}:") + ui.print_info(f"\t\t{offset.definitionUri}") + ui.print_info(f"\t\t{offset.vector}") + # gather the custom labware (for simulation) + custom_defs = {} + if args.simulate: + labware_dir = Path(__file__).parent.parent / "labware" + custom_def_uris = [ + "radwag_pipette_calibration_vial", + "dial_indicator", + ] + for def_uri in custom_def_uris: + with open(labware_dir / def_uri / "1.json", "r") as f: + custom_def = json_load(f) + custom_defs[def_uri] = custom_def + _ctx = helpers.get_api_context( + API_LEVEL, # type: ignore[attr-defined] + is_simulating=args.simulate, + pipette_left=PIPETTE_MODEL_NAME[args.pipette][args.channels], + extra_labware=custom_defs, + ) + for offset in LABWARE_OFFSETS: + engine = _ctx._core._engine_client._transport._engine # type: ignore[attr-defined] + engine.state_view._labware_store._add_labware_offset(offset) + return _ctx + + @classmethod + def build_run_args(cls, args: argparse.Namespace) -> "RunArgs": + """Build.""" + _ctx = RunArgs._get_protocol_context(args) + robot_serial = helpers._get_robot_serial(_ctx.is_simulating()) + run_id, start_time = create_run_id_and_start_time() + environment_sensor = asair_sensor.BuildAsairSensor( + _ctx.is_simulating() or args.ignore_env + ) + git_description = get_git_description() + protocol_cfg = LIQUID_SENSE_CFG[args.pipette][args.channels] + name = protocol_cfg.metadata["protocolName"] # type: ignore[attr-defined] + ui.print_header("LOAD PIPETTE") + pipette = _ctx.load_instrument( + f"flex_{args.channels}channel_{args.pipette}", "left" + ) + loaded_labwares = _ctx.loaded_labwares + if 12 in loaded_labwares.keys(): + trash = loaded_labwares[12] + else: + trash = _ctx.load_labware("opentrons_1_trash_3200ml_fixed", "A3") + pipette.trash_container = trash + pipette_tag = helpers._get_tag_from_pipette(pipette, False, False) + + if args.trials == 0: + trials = 10 + else: + trials = args.trials + + if args.tip == 0: + if args.pipette == 1000: + tip_volumes: List[int] = [50, 200, 1000] + else: + tip_volumes = [50] + else: + tip_volumes = [args.tip] + + scale = Scale.build(simulate=_ctx.is_simulating() or args.ignore_scale) + recorder: GravimetricRecorder = execute._load_scale( + name, + scale, + run_id, + pipette_tag, + start_time, + _ctx.is_simulating() or args.ignore_scale, + ) + dial: Optional[mitutoyo_digimatic_indicator.Mitutoyo_Digimatic_Indicator] = None + if not _ctx.is_simulating() and not args.ignore_dial: + dial_port = list_ports_and_select("Dial Indicator") + dial = mitutoyo_digimatic_indicator.Mitutoyo_Digimatic_Indicator( + port=dial_port + ) + dial.connect() + ui.print_info(f"pipette_tag {pipette_tag}") + report = build_ls_report(name, run_id, trials, tip_volumes) + report.set_tag(name) + # go ahead and store the meta data now + store_serial_numbers( + report, + robot_serial, + pipette_tag, + scale.read_serial_number(), + environment_sensor.get_serial(), + git_description, + ) + + store_config( + report, + name, + args.pipette, + tip_volumes, + trials, + args.plunger_direction, + args.liquid, + protocol_cfg.LABWARE_ON_SCALE, # type: ignore[attr-defined] + args.z_speed, + args.start_height_offset, + ) + return RunArgs( + tip_volumes=tip_volumes, + run_id=run_id, + pipette=pipette, + pipette_tag=pipette_tag, + git_description=git_description, + robot_serial=robot_serial, + recorder=recorder, + pipette_volume=args.pipette, + pipette_channels=args.channels, + name=name, + environment_sensor=environment_sensor, + trials=trials, + z_speed=args.z_speed, + return_tip=args.return_tip, + ctx=_ctx, + protocol_cfg=protocol_cfg, + test_report=report, + start_height_offset=args.start_height_offset, + aspirate=args.plunger_direction == "aspirate", + dial_indicator=dial, + plunger_speed=args.plunger_speed, + trials_before_jog=args.trials_before_jog, + ) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser("Pipette Testing") + parser.add_argument("--simulate", action="store_true") + parser.add_argument("--pipette", type=int, choices=[50, 1000], required=True) + parser.add_argument("--channels", type=int, choices=[1, 8, 96], default=1) + parser.add_argument("--tip", type=int, choices=[0, 50, 200, 1000], default=0) + parser.add_argument("--trials", type=int, default=0) + parser.add_argument("--return-tip", action="store_true") + parser.add_argument("--skip-labware-offsets", action="store_true") + parser.add_argument( + "--liquid", type=str, choices=["water", "glycerol", "alchohol"], default="water" + ) + parser.add_argument("--z-speed", type=float, default=5) + parser.add_argument( + "--plunger-direction", + type=str, + choices=["aspirate", "dispense"], + default="aspirate", + ) + parser.add_argument("--labware-type", type=str, default="nest_1_reservoir_195ml") + parser.add_argument("--plunger-speed", type=float, default=-1.0) + parser.add_argument("--isolate-plungers", action="store_true") + parser.add_argument("--start-height-offset", type=float, default=0) + parser.add_argument("--ignore-scale", action="store_true") + parser.add_argument("--ignore-env", action="store_true") + parser.add_argument("--ignore-dial", action="store_true") + parser.add_argument("--trials-before-jog", type=int, default=10) + + args = parser.parse_args() + run_args = RunArgs.build_run_args(args) + try: + if not run_args.ctx.is_simulating(): + data_dir = get_testing_data_directory() + data_file = f"/{data_dir}/{run_args.name}/{run_args.run_id}/serial.log" + ui.print_info(f"logging can data to {data_file}") + serial_logger = subprocess.Popen( + [f"python3 -m opentrons_hardware.scripts.can_mon > {data_file}"], + shell=True, + ) + sleep(1) + hw = run_args.ctx._core.get_hardware() + if not run_args.ctx.is_simulating(): + ui.get_user_ready("CLOSE the door, and MOVE AWAY from machine") + ui.print_info("homing...") + run_args.ctx.home() + for tip in run_args.tip_volumes: + if args.channels == 96 and not run_args.ctx.is_simulating(): + ui.alert_user_ready(f"prepare the {tip}ul tipracks", hw) + execute.run(tip, run_args) + except Exception as e: + ui.print_info(f"got error {e}") + ui.print_info(traceback.format_exc()) + finally: + if run_args.recorder is not None: + ui.print_info("ending recording") + run_args.recorder.stop() + run_args.recorder.deactivate() + if not run_args.ctx.is_simulating(): + ui.print_info("killing serial log") + serial_logger.terminate() + if run_args.dial_indicator is not None: + run_args.dial_indicator.disconnect() + run_args.test_report.save_to_disk() + run_args.test_report.print_results() + ui.print_info("done\n\n") + if not run_args.ctx.is_simulating(): + process_csv_directory( + f"{data_dir}/{run_args.name}/{run_args.run_id}", + run_args.tip_volumes, + run_args.trials, + ) + run_args.ctx.cleanup() + if not args.simulate: + helpers_ot3.restart_server_ot3() + os._exit(os.EX_OK) diff --git a/hardware-testing/hardware_testing/liquid_sense/execute.py b/hardware-testing/hardware_testing/liquid_sense/execute.py new file mode 100644 index 00000000000..1fc95d62d44 --- /dev/null +++ b/hardware-testing/hardware_testing/liquid_sense/execute.py @@ -0,0 +1,307 @@ +"""Logic for running a single liquid probe test.""" +from typing import Dict, Any, List, Tuple, Optional +from .report import store_tip_results, store_trial, store_baseline_trial +from opentrons.config.types import LiquidProbeSettings, OutputOptions +from .__main__ import RunArgs +from hardware_testing.gravimetric.workarounds import get_sync_hw_api +from hardware_testing.gravimetric.helpers import ( + _jog_to_find_liquid_height, +) +from hardware_testing.gravimetric.config import LIQUID_PROBE_SETTINGS +from hardware_testing.gravimetric.tips import get_unused_tips +from hardware_testing.data import ui, get_testing_data_directory +from opentrons.hardware_control.types import ( + InstrumentProbeType, + OT3Mount, + Axis, + top_types, +) + +from hardware_testing.gravimetric.measurement.scale import Scale +from hardware_testing.gravimetric.measurement.record import ( + GravimetricRecorder, + GravimetricRecorderConfig, +) +from opentrons.protocol_api._types import OffDeckType + +from opentrons.protocol_api import ProtocolContext, Well, Labware + + +def _load_tipracks( + ctx: ProtocolContext, pipette_channels: int, protocol_cfg: Any, tip: int +) -> List[Labware]: + # TODO add logic here for partial tip using 96 + use_adapters: bool = pipette_channels == 96 + tiprack_load_settings: List[Tuple[int, str]] = [ + ( + slot, + f"opentrons_flex_96_tiprack_{tip}ul", + ) + for slot in protocol_cfg.SLOTS_TIPRACK[tip] # type: ignore[attr-defined] + ] + for ls in tiprack_load_settings: + ui.print_info(f'Loading tiprack "{ls[1]}" in slot #{ls[0]}') + + adapter: Optional[str] = ( + "opentrons_flex_96_tiprack_adapter" if use_adapters else None + ) + # If running multiple tests in one run, the labware may already be loaded + loaded_labwares = ctx.loaded_labwares + ui.print_info(f"Loaded labwares {loaded_labwares}") + pre_loaded_tips: List[Labware] = [] + for ls in tiprack_load_settings: + if ls[0] in loaded_labwares.keys(): + if loaded_labwares[ls[0]].name == ls[1]: + pre_loaded_tips.append(loaded_labwares[ls[0]]) + else: + # If something is in the slot that's not what we want, remove it + # we use this only for the 96 channel + ui.print_info( + f"Removing {loaded_labwares[ls[0]].name} from slot {ls[0]}" + ) + ctx._core.move_labware( + loaded_labwares[ls[0]]._core, + new_location=OffDeckType.OFF_DECK, + use_gripper=False, + pause_for_manual_move=False, + pick_up_offset=None, + drop_offset=None, + ) + if len(pre_loaded_tips) == len(tiprack_load_settings): + return pre_loaded_tips + + tipracks: List[Labware] = [] + for ls in tiprack_load_settings: + if ctx.deck[ls[0]] is not None: + tipracks.append( + ctx.deck[ls[0]].load_labware(ls[1]) # type: ignore[union-attr] + ) + else: + tipracks.append(ctx.load_labware(ls[1], location=ls[0], adapter=adapter)) + return tipracks + + +def _load_dial_indicator(run_args: RunArgs) -> Labware: + slot_dial = run_args.protocol_cfg.SLOT_DIAL # type: ignore[union-attr] + dial_labware_name = "dial_indicator" + loaded_labwares = run_args.ctx.loaded_labwares + if ( + slot_dial in loaded_labwares.keys() + and loaded_labwares[slot_dial].name == dial_labware_name + ): + return loaded_labwares[slot_dial] + + dial_labware = run_args.ctx.load_labware( + dial_labware_name, location=slot_dial, namespace="custom_beta" + ) + return dial_labware + + +def _load_test_well(run_args: RunArgs) -> Labware: + slot_scale = run_args.protocol_cfg.SLOT_SCALE # type: ignore[union-attr] + labware_on_scale = run_args.protocol_cfg.LABWARE_ON_SCALE # type: ignore[union-attr] + ui.print_info(f'Loading labware on scale: "{labware_on_scale}"') + if labware_on_scale == "radwag_pipette_calibration_vial": + namespace = "custom_beta" + else: + namespace = "opentrons" + # If running multiple tests in one run, the labware may already be loaded + loaded_labwares = run_args.ctx.loaded_labwares + if ( + slot_scale in loaded_labwares.keys() + and loaded_labwares[slot_scale].name == labware_on_scale + ): + return loaded_labwares[slot_scale] + + labware_on_scale = run_args.ctx.load_labware( + labware_on_scale, location=slot_scale, namespace=namespace + ) + return labware_on_scale + + +def _load_scale( + name: str, + scale: Scale, + run_id: str, + pipette_tag: str, + start_time: float, + simulating: bool, +) -> GravimetricRecorder: + ui.print_header("LOAD SCALE") + ui.print_info( + "Some Radwag settings cannot be controlled remotely.\n" + "Listed below are the things the must be done using the touchscreen:\n" + " 1) Set profile to USER\n" + " 2) Set screensaver to NONE\n" + ) + recorder = GravimetricRecorder( + GravimetricRecorderConfig( + test_name=name, + run_id=run_id, + tag=pipette_tag, + start_time=start_time, + duration=0, + frequency=1000 if simulating else 60, + stable=False, + ), + scale, + simulate=simulating, + start_graph=False, + ) + ui.print_info(f'found scale "{recorder.serial_number}"') + if simulating: + recorder.set_simulation_mass(0) + recorder.record(in_thread=True) + ui.print_info(f'scale is recording to "{recorder.file_name}"') + return recorder + + +def run(tip: int, run_args: RunArgs) -> None: + """Run a liquid probe test.""" + test_labware: Labware = _load_test_well(run_args) + dial_indicator: Labware = _load_dial_indicator(run_args) + dial_well: Well = dial_indicator["A1"] + hw_api = get_sync_hw_api(run_args.ctx) + test_well: Well = test_labware["A1"] + _load_tipracks(run_args.ctx, run_args.pipette_channels, run_args.protocol_cfg, tip) + tips: List[Well] = get_unused_tips( + ctx=run_args.ctx, tip_volume=tip, pipette_mount="" + ) + assert len(tips) >= run_args.trials + results: List[float] = [] + adjusted_results: List[float] = [] + lpc_offset = 0.0 + if run_args.dial_indicator is not None: + run_args.pipette.move_to(dial_well.top()) + lpc_offset = run_args.dial_indicator.read_stable() + run_args.pipette._retract() + + def _get_baseline() -> float: + run_args.pipette.pick_up_tip(tips.pop(0)) + liquid_height = _jog_to_find_liquid_height( + run_args.ctx, run_args.pipette, test_well + ) + target_height = test_well.bottom(liquid_height).point.z + + run_args.pipette._retract() + # tip_offset = 0.0 + if run_args.dial_indicator is not None: + run_args.pipette.move_to(dial_well.top()) + tip_offset = run_args.dial_indicator.read_stable() + run_args.pipette._retract() + if run_args.return_tip: + run_args.pipette.return_tip() + else: + run_args.pipette.drop_tip() + + env_data = run_args.environment_sensor.get_reading() + + store_baseline_trial( + run_args.test_report, + tip, + target_height, + env_data.relative_humidity, + env_data.temperature, + test_well.top().point.z - target_height, + tip_offset - lpc_offset, + ) + return target_height + + trials_before_jog = run_args.trials_before_jog + tip_offset = 0.0 + for trial in range(run_args.trials): + if trial % trials_before_jog == 0: + tip_offset = _get_baseline() + + ui.print_info(f"Picking up {tip}ul tip") + run_args.pipette.pick_up_tip(tips.pop(0)) + run_args.pipette.move_to(test_well.top()) + + start_pos = hw_api.current_position_ot3(OT3Mount.LEFT) + height = _run_trial(run_args, tip, test_well, trial) + end_pos = hw_api.current_position_ot3(OT3Mount.LEFT) + run_args.pipette.blow_out() + tip_length_offset = 0.0 + if run_args.dial_indicator is not None: + + run_args.pipette._retract() + run_args.pipette.move_to(dial_well.top()) + tip_length_offset = tip_offset - run_args.dial_indicator.read_stable() + run_args.pipette._retract() + ui.print_info(f"Tip Offset {tip_length_offset}") + + ui.print_info("Droping tip") + if run_args.return_tip: + run_args.pipette.return_tip() + else: + run_args.pipette.drop_tip() + results.append(height) + adjusted_results.append(height + tip_length_offset) + env_data = run_args.environment_sensor.get_reading() + hw_pipette = hw_api.hardware_pipettes[top_types.Mount.LEFT] + plunger_start = ( + hw_pipette.plunger_positions.bottom + if run_args.aspirate + else hw_pipette.plunger_positions.top + ) + store_trial( + run_args.test_report, + trial, + tip, + height, + end_pos[Axis.P_L], + env_data.relative_humidity, + env_data.temperature, + start_pos[Axis.Z_L] - end_pos[Axis.Z_L], + plunger_start - end_pos[Axis.P_L], + tip_length_offset, + ) + ui.print_info( + f"\n\n Z axis start pos {start_pos[Axis.Z_L]} end pos {end_pos[Axis.Z_L]}" + ) + ui.print_info( + f"plunger start pos {plunger_start} end pos {end_pos[Axis.P_L]}\n\n" + ) + + ui.print_info(f"RESULTS: \n{results}") + ui.print_info(f"Adjusted RESULTS: \n{adjusted_results}") + store_tip_results(run_args.test_report, tip, results, adjusted_results) + + +def _run_trial(run_args: RunArgs, tip: int, well: Well, trial: int) -> float: + hw_api = get_sync_hw_api(run_args.ctx) + lqid_cfg: Dict[str, int] = LIQUID_PROBE_SETTINGS[run_args.pipette_volume][ + run_args.pipette_channels + ][tip] + data_dir = get_testing_data_directory() + data_filename = f"pressure_sensor_data-trial{trial}-tip{tip}.csv" + data_file = f"{data_dir}/{run_args.name}/{run_args.run_id}/{data_filename}" + ui.print_info(f"logging pressure data to {data_file}") + + plunger_speed = ( + lqid_cfg["plunger_speed"] + if run_args.plunger_speed == -1 + else run_args.plunger_speed + ) + lps = LiquidProbeSettings( + starting_mount_height=well.top().point.z + run_args.start_height_offset, + max_z_distance=min(well.depth, lqid_cfg["max_z_distance"]), + min_z_distance=lqid_cfg["min_z_distance"], + mount_speed=run_args.z_speed, + plunger_speed=plunger_speed, + sensor_threshold_pascals=lqid_cfg["sensor_threshold_pascals"], + expected_liquid_height=110, + output_option=OutputOptions.sync_buffer_to_csv, + aspirate_while_sensing=run_args.aspirate, + auto_zero_sensor=True, + num_baseline_reads=10, + data_file=data_file, + ) + + hw_mount = OT3Mount.LEFT if run_args.pipette.mount == "left" else OT3Mount.RIGHT + run_args.recorder.set_sample_tag(f"trial-{trial}-{tip}ul") + # TODO add in stuff for secondary probe + height = hw_api.liquid_probe(hw_mount, lps, InstrumentProbeType.PRIMARY) + ui.print_info(f"Trial {trial} complete") + run_args.recorder.clear_sample_tag() + return height diff --git a/hardware-testing/hardware_testing/liquid_sense/post_process.py b/hardware-testing/hardware_testing/liquid_sense/post_process.py new file mode 100644 index 00000000000..20e46ed746a --- /dev/null +++ b/hardware-testing/hardware_testing/liquid_sense/post_process.py @@ -0,0 +1,170 @@ +"""Post process script csvs.""" +import csv +import os +from typing import List, Dict, Tuple +from math import isclose + +COL_TRIAL_CONVERSION = { + 1: "E", + 2: "H", + 3: "K", + 4: "N", + 5: "Q", + 6: "T", + 7: "W", + 8: "Z", + 9: "AC", + 10: "AF", + 11: "AI", + 12: "AL", + 13: "AO", +} + + +def process_csv_directory( # noqa: C901 + data_directory: str, tips: List[int], trials: int, make_graph: bool = False +) -> None: + """Post process script csvs.""" + csv_files: List[str] = os.listdir(data_directory) + summary: str = [f for f in csv_files if "CSVReport" in f][0] + final_report_file: str = f"{data_directory}/final_report.csv" + # initialize our data structs + pressure_csvs = [f for f in csv_files if "pressure_sensor_data" in f] + pressure_results_files: Dict[int, List[str]] = {} + pressure_results: Dict[int, Dict[int, List[float]]] = {} + results_settings: Dict[int, Dict[int, Tuple[float, float, float]]] = {} + tip_offsets: Dict[int, List[float]] = {} + p_offsets: Dict[int, List[float]] = {} + meniscus_travel: float = 0 + for tip in tips: + pressure_results_files[tip] = [f for f in pressure_csvs if f"tip{tip}" in f] + pressure_results[tip] = {} + results_settings[tip] = {} + tip_offsets[tip] = [] + p_offsets[tip] = [i * 0 for i in range(trials)] + for trial in range(trials): + pressure_results[tip][trial] = [] + results_settings[tip][trial] = (0.0, 0.0, 0.0) + max_results_len = 0 + + # read in all of the pressure csvs into one big struct so we can process them + for tip in tips: + for trial in range(trials): + with open( + f"{data_directory}/{pressure_results_files[tip][trial]}", newline="" + ) as trial_csv: + trial_reader = csv.reader(trial_csv) + i = 0 + for row in trial_reader: + if i == 1: + results_settings[tip][trial] = ( + float(row[2]), + float(row[3]), + float(row[4]), + ) + if i > 1: + pressure_results[tip][trial].append(float(row[1])) + i += 1 + max_results_len = max([i - 2, max_results_len]) + # start writing the final report csv + with open(f"{data_directory}/{summary}", newline="") as summary_csv: + summary_reader = csv.reader(summary_csv) + with open(final_report_file, "w", newline="") as final_report: + # copy over the results summary + final_report_writer = csv.writer(final_report) + s = 0 + for row in summary_reader: + final_report_writer.writerow(row) + s += 1 + if s == 45: + meniscus_travel = float(row[6]) + if s >= 46 and s < 46 + (trials * len(tips)): + # while processing this grab the tip offsets from the summary + tip_offsets[tips[int((s - 46) / trials)]].append(float(row[8])) + # summary_reader.line_num is the last line in the summary that has text + pressures_start_line = summary_reader.line_num + 3 + # calculate where the start and end of each block of data we want to graph + final_report_writer.writerow( + [ + "50ul", + f"A{pressures_start_line-1}", + f"{COL_TRIAL_CONVERSION[trials]}{pressures_start_line + max_results_len -1}", + "200ul", + f"A{pressures_start_line+max_results_len-1}", + f"{COL_TRIAL_CONVERSION[trials]}{pressures_start_line +(2*max_results_len)-1}", + "10000ul", + f"A{pressures_start_line+(2*max_results_len-1)}", + f"{COL_TRIAL_CONVERSION[trials]}{pressures_start_line + (3*max_results_len)-1}", + ] + ) + + # build a header row + pressure_header_row = ["time", ""] + for i in range(trials): + pressure_header_row.extend( + [f"pressure T{i+1}", f"z_travel T{i+1}", f"p_travel T{i+1}"] + ) + + # we want to line up the z height's of each trial at time==0 + # to do this we drop the results at the beginning of each of the trials + # except for one with the longest tip (lower tip offset are longer tips) + min_tip_offset = 0.0 + if make_graph: + for tip in tips: + min_tip_offset = min(tip_offsets[tip]) + for trial in range(trials): + for i in range(max_results_len): + if tip_offsets[tip][trial] > min_tip_offset: + # drop this pressure result + pressure_results[tip][trial].pop(0) + # we don't want to change the length of this array so just + # stretch out the last value + pressure_results[tip][trial].append( + pressure_results[tip][trial][-1] + ) + # decrement the offset while this is true + # so we can account for it later + tip_offsets[tip][trial] -= ( + 0.001 * results_settings[tip][0][0] + ) + # keep track of how this effects the plunger start position + p_offsets[tip][trial] = ( + (i + 1) * 0.001 * results_settings[tip][0][1] * -1 + ) + else: + # we've lined up this trial so move to the next + break + # write the processed test data + for tip in tips: + time = 0.0 + final_report_writer.writerow(pressure_header_row) + meniscus_time = (meniscus_travel + min_tip_offset) / results_settings[ + tip + ][0][0] + for i in range(max_results_len): + pressure_row: List[str] = [f"{time}"] + if isclose( + time, + meniscus_time, + rel_tol=0.001, + ): + pressure_row.append("Meniscus") + else: + pressure_row.append("") + for trial in range(trials): + if i < len(pressure_results[tip][trial]): + pressure_row.append(f"{pressure_results[tip][trial][i]}") + else: + pressure_row.append("") + pressure_row.append( + f"{results_settings[tip][trial][0] * time - tip_offsets[tip][trial]}" + ) + pressure_row.append( + f"{abs(results_settings[tip][trial][1]) * time + p_offsets[tip][trial]}" + ) + final_report_writer.writerow(pressure_row) + time += 0.001 + + +if __name__ == "__main__": + process_csv_directory("/home/ryan/testdata", [50], 10) diff --git a/hardware-testing/hardware_testing/liquid_sense/report.py b/hardware-testing/hardware_testing/liquid_sense/report.py new file mode 100644 index 00000000000..bca898e79c7 --- /dev/null +++ b/hardware-testing/hardware_testing/liquid_sense/report.py @@ -0,0 +1,263 @@ +"""Format the csv report for a liquid-sense run.""" + +import statistics +from hardware_testing.data.csv_report import ( + CSVReport, + CSVSection, + CSVLine, + CSVLineRepeating, +) +from typing import List, Union + +""" +CSV Test Report: + - Serial numbers: + - Robot + - Pipette + - Scale + - Environment sensor + - Config: + - protocol name + - pipette_volume + - pipette_mount + - tip_volume + - trials + - plunger direction + - liquid + - labware type + - speed + - start height offset + - Trials + trial-x-{tipsize}ul + - Results + {tipsize}ul-average + {tipsize}ul-cv + {tipsize}ul-d +""" + + +def build_serial_number_section() -> CSVSection: + """Build section.""" + return CSVSection( + title="SERIAL-NUMBERS", + lines=[ + CSVLine("robot", [str]), + CSVLine("git_description", [str]), + CSVLine("pipette", [str]), + CSVLine("scale", [str]), + CSVLine("environment", [str]), + ], + ) + + +def build_config_section() -> CSVSection: + """Build section.""" + return CSVSection( + title="CONFIG", + lines=[ + CSVLine("protocol_name", [str]), + CSVLine("pipette_volume", [str]), + CSVLine("tip_volume", [bool, bool, bool]), + CSVLine("trials", [str]), + CSVLine("plunger_direction", [str]), + CSVLine("liquid", [str]), + CSVLine("labware_type", [str]), + CSVLine("speed", [str]), + CSVLine("start_height_offset", [str]), + ], + ) + + +def build_trials_section(trials: int, tips: List[int]) -> CSVSection: + """Build section.""" + lines: List[Union[CSVLine, CSVLineRepeating]] = [ + CSVLine("trial_number", [str, str, str, str, str, str, str, str]) + ] + lines.extend( + [ + CSVLine( + f"trial-baseline-{tip}ul", + [float, float, float, float, float, float, float, float], + ) + for tip in tips + ] + ) + lines.extend( + [ + CSVLine( + f"trial-{t + 1}-{tip}ul", + [float, float, float, float, float, float, float, float], + ) + for tip in tips + for t in range(trials) + ] + ) + + return CSVSection( + title="TRIALS", + lines=lines, + ) + + +def build_results_section(tips: List[int]) -> CSVSection: + """Build section.""" + lines: List[CSVLine] = [] + for tip in tips: + lines.append(CSVLine(f"{tip}ul-average", [float])) + lines.append(CSVLine(f"{tip}ul-minumum", [float])) + lines.append(CSVLine(f"{tip}ul-maximum", [float])) + lines.append(CSVLine(f"{tip}ul-stdev", [float])) + lines.append(CSVLine(f"{tip}ul-adjusted-average", [float])) + lines.append(CSVLine(f"{tip}ul-adjusted-minumum", [float])) + lines.append(CSVLine(f"{tip}ul-adjusted-maximum", [float])) + lines.append(CSVLine(f"{tip}ul-adjusted-stdev", [float])) + return CSVSection(title="RESULTS", lines=lines) # type: ignore[arg-type] + + +def store_serial_numbers( + report: CSVReport, + robot: str, + pipette: str, + scale: str, + environment: str, + git_description: str, +) -> None: + """Report serial numbers.""" + report("SERIAL-NUMBERS", "robot", [robot]) + report("SERIAL-NUMBERS", "git_description", [git_description]) + report("SERIAL-NUMBERS", "pipette", [pipette]) + report("SERIAL-NUMBERS", "scale", [scale]) + report("SERIAL-NUMBERS", "environment", [environment]) + + +def store_config( + report: CSVReport, + protocol_name: str, + pipette_volume: str, + tip_volumes: List[int], + trials: int, + plunger_direction: str, + liquid: str, + labware_type: str, + speed: str, + start_height_offset: str, +) -> None: + """Report config.""" + report("CONFIG", "protocol_name", [protocol_name]) + report("CONFIG", "pipette_volume", [pipette_volume]) + report( + "CONFIG", + "tip_volume", + [50 in tip_volumes, 200 in tip_volumes, 1000 in tip_volumes], + ) + report("CONFIG", "trials", [trials]) + report("CONFIG", "plunger_direction", [plunger_direction]) + report("CONFIG", "liquid", [liquid]) + report("CONFIG", "labware_type", [labware_type]) + report("CONFIG", "speed", [speed]) + report("CONFIG", "start_height_offset", [start_height_offset]) + + +def store_baseline_trial( + report: CSVReport, + tip: float, + height: float, + humidity: float, + temp: float, + z_travel: float, + measured_error: float, +) -> None: + """Report Trial.""" + report( + "TRIALS", + f"trial-baseline-{tip}ul", + [ + height, + 0, + humidity, + temp, + z_travel, + 0, + 0, + measured_error, + ], + ) + + +def store_trial( + report: CSVReport, + trial: int, + tip: float, + height: float, + plunger_pos: float, + humidity: float, + temp: float, + z_travel: float, + plunger_travel: float, + tip_length_offset: float, +) -> None: + """Report Trial.""" + report( + "TRIALS", + f"trial-{trial + 1}-{tip}ul", + [ + height, + plunger_pos, + humidity, + temp, + z_travel, + plunger_travel, + tip_length_offset, + height + tip_length_offset, + ], + ) + + +def store_tip_results( + report: CSVReport, tip: float, results: List[float], adjusted_results: List[float] +) -> None: + """Store final results.""" + report("RESULTS", f"{tip}ul-average", [sum(results) / len(results)]) + report("RESULTS", f"{tip}ul-minumum", [min(results)]) + report("RESULTS", f"{tip}ul-maximum", [max(results)]) + report("RESULTS", f"{tip}ul-stdev", [statistics.stdev(results)]) + report( + "RESULTS", + f"{tip}ul-adjusted-average", + [sum(adjusted_results) / len(adjusted_results)], + ) + report("RESULTS", f"{tip}ul-adjusted-minumum", [min(adjusted_results)]) + report("RESULTS", f"{tip}ul-adjusted-maximum", [max(adjusted_results)]) + report("RESULTS", f"{tip}ul-adjusted-stdev", [statistics.stdev(adjusted_results)]) + + +def build_ls_report( + test_name: str, run_id: str, trials: int, tips: List[int] +) -> CSVReport: + """Generate a CSV Report.""" + report = CSVReport( + test_name=test_name, + sections=[ + build_serial_number_section(), + build_config_section(), + build_trials_section(trials, tips), + build_results_section(tips), + ], + run_id=run_id, + start_time=0.0, + ) + report( + "TRIALS", + "trial_number", + [ + "height", + "plunger_pos", + "humidity", + "temp", + "z_travel", + "plunger_travel", + "tip_length_offset", + "adjusted_height", + ], + ) + return report diff --git a/hardware-testing/hardware_testing/opentrons_api/helpers_ot3.py b/hardware-testing/hardware_testing/opentrons_api/helpers_ot3.py index f277ff93f76..d1ff8f91d53 100644 --- a/hardware-testing/hardware_testing/opentrons_api/helpers_ot3.py +++ b/hardware-testing/hardware_testing/opentrons_api/helpers_ot3.py @@ -84,9 +84,7 @@ def stop_server_ot3() -> None: def restart_server_ot3() -> None: """Start opentrons-robot-server on the OT3.""" print('Starting "opentrons-robot-server"...') - Popen( - ["systemctl", "restart", "opentrons-robot-server", "&"], - ) + Popen(["systemctl restart opentrons-robot-server &"], shell=True) def start_server_ot3() -> None: diff --git a/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/__init__.py b/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/__init__.py new file mode 100644 index 00000000000..6ec34e45de0 --- /dev/null +++ b/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/__init__.py @@ -0,0 +1 @@ +"""Liquid Sense LPC.""" diff --git a/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p1000_96.py b/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p1000_96.py new file mode 100644 index 00000000000..02644b314a4 --- /dev/null +++ b/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p1000_96.py @@ -0,0 +1,33 @@ +"""Liquid sense OT3 P1000.""" +from opentrons.protocol_api import ProtocolContext + +metadata = {"protocolName": "liquid-sense-ot3-p1000-96"} +requirements = {"robotType": "Flex", "apiLevel": "2.15"} + +SLOT_SCALE = 4 +SLOT_DIAL = 5 +SLOTS_TIPRACK = { + # TODO: add slot 12 when tipracks are disposable + 50: [2, 3, 6, 7, 8, 9, 10, 11], + 200: [2, 3, 6, 7, 8, 9, 10, 11], # NOTE: ignored during calibration + 1000: [2, 3, 6, 7, 8, 9, 10, 11], # NOTE: ignored during calibration +} + +LABWARE_ON_SCALE = "nest_1_reservoir_195ml" + + +def run(ctx: ProtocolContext) -> None: + """Run.""" + tipracks = [ + ctx.load_labware(f"opentrons_flex_96_tiprack_{size}uL_adp", slot) + for size, slots in SLOTS_TIPRACK.items() + for slot in slots + if size == 50 # only calibrate 50ul tip-racks + ] + scale_labware = ctx.load_labware(LABWARE_ON_SCALE, SLOT_SCALE) + pipette = ctx.load_instrument("p1000_96", "left") + for rack in tipracks: + pipette.pick_up_tip(rack["A1"]) + pipette.aspirate(10, scale_labware["A1"].top()) + pipette.dispense(10, scale_labware["A1"].top()) + pipette.drop_tip(home_after=False) diff --git a/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p1000_multi.py b/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p1000_multi.py new file mode 100644 index 00000000000..d2b806d1229 --- /dev/null +++ b/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p1000_multi.py @@ -0,0 +1,26 @@ +"""LiquidSense OT3 P1000.""" +from opentrons.protocol_api import ProtocolContext + +metadata = {"protocolName": "liquid-sense-ot3-p1000-multi"} +requirements = {"robotType": "Flex", "apiLevel": "2.15"} + +SLOT_SCALE = 4 +SLOT_DIAL = 5 +SLOTS_TIPRACK = {50: [2], 200: [3], 1000: [6]} +LABWARE_ON_SCALE = "nest_1_reservoir_195ml" + + +def run(ctx: ProtocolContext) -> None: + """Run.""" + tipracks = [ + ctx.load_labware(f"opentrons_flex_96_tiprack_{size}uL", slot) + for size, slots in SLOTS_TIPRACK.items() + for slot in slots + ] + vial = ctx.load_labware(LABWARE_ON_SCALE, SLOT_SCALE) + pipette = ctx.load_instrument("flex_8channel_1000", "left") + for rack in tipracks: + pipette.pick_up_tip(rack["A1"]) + pipette.aspirate(10, vial["A1"].top()) + pipette.dispense(10, vial["A1"].top()) + pipette.drop_tip(home_after=False) diff --git a/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p1000_single.py b/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p1000_single.py new file mode 100644 index 00000000000..4e8fcc177f4 --- /dev/null +++ b/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p1000_single.py @@ -0,0 +1,33 @@ +"""Liquid Sense OT3 P1000.""" +from opentrons.protocol_api import ProtocolContext + +metadata = {"protocolName": "liquid-sense-ot3-p1000-single"} +requirements = {"robotType": "Flex", "apiLevel": "2.15"} + +SLOT_SCALE = 4 +SLOT_DIAL = 5 +SLOTS_TIPRACK = { + 50: [3], + 200: [6], + 1000: [9], +} +LABWARE_ON_SCALE = "nest_1_reservoir_195ml" + + +def run(ctx: ProtocolContext) -> None: + """Run.""" + tipracks = [ + ctx.load_labware(f"opentrons_flex_96_tiprack_{size}uL", slot) + for size, slots in SLOTS_TIPRACK.items() + for slot in slots + ] + vial = ctx.load_labware(LABWARE_ON_SCALE, SLOT_SCALE) + dial = ctx.load_labware("dial_indicator", SLOT_DIAL) + pipette = ctx.load_instrument("flex_1channel_1000", "left") + for rack in tipracks: + pipette.pick_up_tip(rack["A1"]) + pipette.aspirate(10, vial["A1"].top()) + pipette.dispense(10, vial["A1"].top()) + pipette.aspirate(1, dial["A1"].top()) + pipette.dispense(1, dial["A1"].top()) + pipette.drop_tip(home_after=False) diff --git a/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p50_multi.py b/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p50_multi.py new file mode 100644 index 00000000000..34f83cd4cf7 --- /dev/null +++ b/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p50_multi.py @@ -0,0 +1,28 @@ +"""Liquid Sense OT3.""" +from opentrons.protocol_api import ProtocolContext + +metadata = {"protocolName": "liquid_sense-ot3-p50-multi-50ul-tip"} +requirements = {"robotType": "Flex", "apiLevel": "2.15"} + +SLOT_SCALE = 4 +SLOT_DIAL = 5 +SLOTS_TIPRACK = { + 50: [3], +} +LABWARE_ON_SCALE = "nest_1_reservoir_195ml" + + +def run(ctx: ProtocolContext) -> None: + """Run.""" + tipracks = [ + ctx.load_labware(f"opentrons_flex_96_tiprack_{size}uL", slot) + for size, slots in SLOTS_TIPRACK.items() + for slot in slots + ] + vial = ctx.load_labware(LABWARE_ON_SCALE, SLOT_SCALE) + pipette = ctx.load_instrument("flex_8channel_50", "left") + for rack in tipracks: + pipette.pick_up_tip(rack["A1"]) + pipette.aspirate(pipette.min_volume, vial["A1"].top()) + pipette.dispense(pipette.min_volume, vial["A1"].top()) + pipette.drop_tip(home_after=False) diff --git a/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p50_single.py b/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p50_single.py new file mode 100644 index 00000000000..8e9d65a72e2 --- /dev/null +++ b/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p50_single.py @@ -0,0 +1,31 @@ +"""Liquid Sense OT3.""" +from opentrons.protocol_api import ProtocolContext + +metadata = {"protocolName": "liquid-sense-ot3-p50-single"} +requirements = {"robotType": "Flex", "apiLevel": "2.15"} + +SLOT_SCALE = 4 +SLOT_DIAL = 5 +SLOTS_TIPRACK = { + 50: [3], +} +LABWARE_ON_SCALE = "radwag_pipette_calibration_vial" + + +def run(ctx: ProtocolContext) -> None: + """Run.""" + tipracks = [ + ctx.load_labware(f"opentrons_flex_96_tiprack_{size}uL", slot) + for size, slots in SLOTS_TIPRACK.items() + for slot in slots + ] + vial = ctx.load_labware(LABWARE_ON_SCALE, SLOT_SCALE) + dial = ctx.load_labware("dial_indicator", SLOT_DIAL) + pipette = ctx.load_instrument("flex_1channel_50", "left") + for rack in tipracks: + pipette.pick_up_tip(rack["A1"]) + pipette.aspirate(10, vial["A1"].top()) + pipette.dispense(10, vial["A1"].top()) + pipette.aspirate(1, dial["A1"].top()) + pipette.dispense(1, dial["A1"].top()) + pipette.drop_tip(home_after=False) diff --git a/hardware/opentrons_hardware/firmware_bindings/messages/messages.py b/hardware/opentrons_hardware/firmware_bindings/messages/messages.py index 6611edecfe4..9906aa8dc07 100644 --- a/hardware/opentrons_hardware/firmware_bindings/messages/messages.py +++ b/hardware/opentrons_hardware/firmware_bindings/messages/messages.py @@ -111,6 +111,7 @@ defs.GetHepaUVStateResponse, defs.SendAccumulatedPressureDataRequest, defs.AddSensorLinearMoveRequest, + defs.SendAccumulatedPressureDataRequest, ] diff --git a/hardware/opentrons_hardware/hardware_control/tool_sensors.py b/hardware/opentrons_hardware/hardware_control/tool_sensors.py index 94301464f22..67e85a1554b 100644 --- a/hardware/opentrons_hardware/hardware_control/tool_sensors.py +++ b/hardware/opentrons_hardware/hardware_control/tool_sensors.py @@ -201,7 +201,6 @@ async def liquid_probe( csv_output: bool = False, sync_buffer_output: bool = False, can_bus_only_output: bool = False, - # output_option: OutputOptions, data_file: Optional[str] = None, auto_zero_sensor: bool = True, num_baseline_reads: int = 10, @@ -232,7 +231,7 @@ async def liquid_probe( ) sensor_runner = MoveGroupRunner(move_groups=[[sensor_group]]) - log_file: str = "/var/pressure_sensor_data.csv" if not data_file else data_file + log_file: str = "/data/pressure_sensor_data.csv" if not data_file else data_file if csv_output: return await run_stream_output_to_csv( messenger, From 0c799fec1ab8df32918633ccf015c396ca18ab8d Mon Sep 17 00:00:00 2001 From: Rhyann Clarke <146747548+rclarke0@users.noreply.github.com> Date: Tue, 9 Apr 2024 13:09:41 -0400 Subject: [PATCH 079/194] Add errored runs to abr tracking sheet (#14845) # Overview Improved ABR Error Data Collection # Test Plan Tested code on multiple robots. # Changelog Added function to download robot logs Added lines of code to move error documents (run log, calibration log, robot logs) into folder named after ticket. Adds robot run to ABR sheet and links JIRA ticket Added extra lines to abr_scale to read scale more often Edited ABR calibration script to ensure duplicate calibrations are not added. # Review requests Is 5000 lines of recording enough to capture robot error if script is run immediately? Is there any manipulation to robot logs that can be down to make error analysis more efficient. # Risk assessment --- .../automation/google_drive_tool.py | 1 - .../automation/google_sheets_tool.py | 7 ++ .../abr_testing/automation/jira_tool.py | 11 +-- .../data_collection/abr_calibration_logs.py | 32 ++++++--- .../data_collection/abr_google_drive.py | 26 +++++-- .../abr_testing/data_collection/abr_lpc.py | 1 + .../data_collection/abr_robot_error.py | 67 ++++++++++++++++--- .../data_collection/read_robot_logs.py | 63 +++++++++++++++-- abr-testing/abr_testing/tools/abr_scale.py | 7 ++ 9 files changed, 179 insertions(+), 36 deletions(-) create mode 100644 abr-testing/abr_testing/data_collection/abr_lpc.py diff --git a/abr-testing/abr_testing/automation/google_drive_tool.py b/abr-testing/abr_testing/automation/google_drive_tool.py index 8b56d0390fe..3b65456d0ff 100644 --- a/abr-testing/abr_testing/automation/google_drive_tool.py +++ b/abr-testing/abr_testing/automation/google_drive_tool.py @@ -25,7 +25,6 @@ def __init__(self, credentials: Any, folder_name: str, email: str) -> None: self.drive_service = build("drive", "v3", credentials=self.credentials) self.parent_folder = folder_name self.email = email - self.folder = self.open_folder() def list_folder(self, delete: Any = False) -> Set[str]: """List folders and files in Google Drive.""" diff --git a/abr-testing/abr_testing/automation/google_sheets_tool.py b/abr-testing/abr_testing/automation/google_sheets_tool.py index e486a28fed2..af38a39dcc0 100644 --- a/abr-testing/abr_testing/automation/google_sheets_tool.py +++ b/abr-testing/abr_testing/automation/google_sheets_tool.py @@ -2,6 +2,7 @@ import gspread # type: ignore[import] import socket import httplib2 +from datetime import datetime from oauth2client.service_account import ServiceAccountCredentials # type: ignore[import] from typing import Dict, List, Any, Set, Tuple @@ -57,6 +58,12 @@ def write_to_row(self, data: List) -> None: """Write data into a row in a List[] format.""" try: self.row_index += 1 + data = [ + item.strftime("%Y/%m/%d %H:%M:%S") + if isinstance(item, datetime) + else item + for item in data + ] self.worksheet.insert_row(data, index=self.row_index) except socket.gaierror: pass diff --git a/abr-testing/abr_testing/automation/jira_tool.py b/abr-testing/abr_testing/automation/jira_tool.py index aff3a6798c3..5c0a2556dfb 100644 --- a/abr-testing/abr_testing/automation/jira_tool.py +++ b/abr-testing/abr_testing/automation/jira_tool.py @@ -5,7 +5,7 @@ import json import webbrowser import argparse -from typing import List, Tuple +from typing import List class JiraTicket: @@ -41,11 +41,12 @@ def issues_on_board(self, board_id: str) -> List[str]: issue_ids.append(issue_id) return issue_ids - def open_issue(self, issue_key: str) -> None: + def open_issue(self, issue_key: str) -> str: """Open issue on web browser.""" url = f"{self.url}/browse/{issue_key}" print(f"Opening at {url}.") webbrowser.open(url) + return url def create_ticket( self, @@ -58,7 +59,7 @@ def create_ticket( components: list, affects_versions: str, robot: str, - ) -> Tuple[str, str]: + ) -> str: """Create ticket.""" data = { "fields": { @@ -94,13 +95,15 @@ def create_ticket( response_str = str(response.content) issue_url = response.json().get("self") issue_key = response.json().get("key") + print(f"issue key {issue_key}") + print(f"issue url{issue_url}") if issue_key is None: print("Error: Could not create issue. No key returned.") except requests.exceptions.HTTPError: print(f"HTTP error occurred. Response content: {response_str}") except json.JSONDecodeError: print(f"JSON decoding error occurred. Response content: {response_str}") - return issue_url, issue_key + return issue_key def post_attachment_to_ticket(self, issue_id: str, attachment_path: str) -> None: """Adds attachments to ticket.""" diff --git a/abr-testing/abr_testing/data_collection/abr_calibration_logs.py b/abr-testing/abr_testing/data_collection/abr_calibration_logs.py index 6e897dd78eb..4d744b5b2f5 100644 --- a/abr-testing/abr_testing/data_collection/abr_calibration_logs.py +++ b/abr-testing/abr_testing/data_collection/abr_calibration_logs.py @@ -1,5 +1,5 @@ """Get Calibration logs from robots.""" -from typing import Dict, Any, List +from typing import Dict, Any, List, Union import argparse import os import json @@ -16,15 +16,18 @@ def check_for_duplicates( col_2: int, row: List[str], headers: List[str], -) -> List[str]: +) -> Union[List[str], None]: """Check google sheet for duplicates.""" serials = google_sheet.get_column(col_1) modify_dates = google_sheet.get_column(col_2) - for serial, modify_date in zip(serials, modify_dates): - if row[col_1 - 1] == serial and row[col_2 - 1] == modify_date: - print(f"Skipped row{row}. Already on Google Sheet.") - continue - read_robot_logs.write_to_sheets(sheet_location, google_sheet, row, headers) + # check for complete calibration. + if len(row[-1]) > 0: + for serial, modify_date in zip(serials, modify_dates): + if row[col_1 - 1] == serial and row[col_2 - 1] == modify_date: + print(f"Skipped row for instrument {serial}. Already on Google Sheet.") + return None + read_robot_logs.write_to_sheets(sheet_location, google_sheet, row, headers) + print(f"Writing calibration for: {serial}") return row @@ -64,6 +67,7 @@ def upload_calibration_offsets( instrument_row, instrument_headers, ) + # MODULE SHEET if len(calibration.get("Modules", "")) > 0: module_headers = ( @@ -198,13 +202,19 @@ def upload_calibration_offsets( except FileNotFoundError: print(f"Add .json file with robot IPs to: {storage_directory}.") sys.exit() + if ip_or_all == "ALL": ip_address_list = ip_file["ip_address_list"] for ip in ip_address_list: - saved_file_path, calibration = read_robot_logs.get_calibration_offsets( - ip, storage_directory - ) - upload_calibration_offsets(calibration, storage_directory) + print(ip) + try: + saved_file_path, calibration = read_robot_logs.get_calibration_offsets( + ip, storage_directory + ) + upload_calibration_offsets(calibration, storage_directory) + except Exception: + print(f"ERROR: Failed to read IP address: {ip}") + continue else: saved_file_path, calibration = read_robot_logs.get_calibration_offsets( ip_or_all, storage_directory diff --git a/abr-testing/abr_testing/data_collection/abr_google_drive.py b/abr-testing/abr_testing/data_collection/abr_google_drive.py index 741ac871d62..6470f1e0410 100644 --- a/abr-testing/abr_testing/data_collection/abr_google_drive.py +++ b/abr-testing/abr_testing/data_collection/abr_google_drive.py @@ -6,7 +6,7 @@ import gspread # type: ignore[import] from datetime import datetime, timedelta from abr_testing.data_collection import read_robot_logs -from typing import Set, Dict, Any, Tuple, List +from typing import Set, Dict, Any, Tuple, List, Union from abr_testing.automation import google_drive_tool, google_sheets_tool @@ -30,7 +30,9 @@ def get_modules(file_results: Dict[str, str]) -> Dict[str, Any]: def create_data_dictionary( - runs_to_save: Set[str], storage_directory: str + runs_to_save: Union[Set[str], str], + storage_directory: str, + issue_url: str, ) -> Tuple[Dict[Any, Dict[str, Any]], List]: """Pull data from run files and format into a dictionary.""" runs_and_robots = {} @@ -41,7 +43,7 @@ def create_data_dictionary( file_results = json.load(file) else: continue - run_id = file_results.get("run_id") + run_id = file_results.get("run_id", "NaN") if run_id in runs_to_save: robot = file_results.get("robot_name") protocol_name = file_results["protocol"]["metadata"].get("protocolName", "") @@ -56,6 +58,7 @@ def create_data_dictionary( error_instrument, error_level, ) = read_robot_logs.get_error_info(file_results) + all_modules = get_modules(file_results) start_time_str, complete_time_str, start_date, run_time_min = ( @@ -103,13 +106,14 @@ def create_data_dictionary( tc_dict = read_robot_logs.thermocycler_commands(file_results) hs_dict = read_robot_logs.hs_commands(file_results) tm_dict = read_robot_logs.temperature_module_commands(file_results) - notes = {"Note1": "", "Note2": ""} + notes = {"Note1": "", "Jira Link": issue_url} row_2 = {**row, **all_modules, **notes, **hs_dict, **tm_dict, **tc_dict} headers = list(row_2.keys()) runs_and_robots[run_id] = row_2 else: - os.remove(file_path) - print(f"Run ID: {run_id} has a run time of 0 minutes. Run removed.") + continue + # os.remove(file_path) + # print(f"Run ID: {run_id} has a run time of 0 minutes. Run removed.") return runs_and_robots, headers @@ -168,6 +172,14 @@ def create_data_dictionary( except gspread.exceptions.APIError: print("ERROR: Check google sheet name. Check credentials file.") sys.exit() + try: + google_sheet_lpc = google_sheets_tool.google_sheet( + credentials_path, "ABR-LPC", 0 + ) + print("Connected to google sheet ABR-LPC") + except gspread.exceptions.APIError: + print("ERROR: Check google sheet name. Check credentials file.") + sys.exit() run_ids_on_gs = google_sheet.get_column(2) run_ids_on_gs = set(run_ids_on_gs) @@ -181,7 +193,7 @@ def create_data_dictionary( ) # Add missing runs to google sheet runs_and_robots, headers = create_data_dictionary( - missing_runs_from_gs, storage_directory + missing_runs_from_gs, storage_directory, "" ) read_robot_logs.write_to_local_and_google_sheet( runs_and_robots, storage_directory, google_sheet_name, google_sheet, headers diff --git a/abr-testing/abr_testing/data_collection/abr_lpc.py b/abr-testing/abr_testing/data_collection/abr_lpc.py new file mode 100644 index 00000000000..dd880d09c37 --- /dev/null +++ b/abr-testing/abr_testing/data_collection/abr_lpc.py @@ -0,0 +1 @@ +"""Get Unique LPC Values from Run logs.""" diff --git a/abr-testing/abr_testing/data_collection/abr_robot_error.py b/abr-testing/abr_testing/data_collection/abr_robot_error.py index 3f7302e8725..b139b5a3ade 100644 --- a/abr-testing/abr_testing/data_collection/abr_robot_error.py +++ b/abr-testing/abr_testing/data_collection/abr_robot_error.py @@ -3,7 +3,13 @@ from abr_testing.data_collection import read_robot_logs, abr_google_drive, get_run_logs import requests import argparse -from abr_testing.automation import jira_tool +from abr_testing.automation import jira_tool, google_sheets_tool, google_drive_tool +import shutil +import os +import subprocess +import json +import sys +import gspread # type: ignore[import] def get_error_runs_from_robot(ip: str) -> List[str]: @@ -44,7 +50,6 @@ def get_error_info_from_robot( # JIRA Ticket Fields failure_level = "Level " + str(error_level) + " Failure" components = [failure_level, "Flex-RABR"] - components = ["Flex-RABR"] affects_version = results["API_Version"] parent = results.get("robot_name", "") print(parent) @@ -140,18 +145,19 @@ def get_error_info_from_robot( affects_version, components, whole_description_str, - saved_file_path, + run_log_file_path, ) = get_error_info_from_robot(ip, one_run, storage_directory) # get calibration data saved_file_path_calibration, calibration = read_robot_logs.get_calibration_offsets( ip, storage_directory ) + file_paths = read_robot_logs.get_logs(storage_directory, ip) print(f"Making ticket for run: {one_run} on robot {robot}.") # TODO: make argument or see if I can get rid of with using board_id. project_key = "RABR" parent_key = project_key + "-" + robot[-1] - issues_ids = ticket.issues_on_board(board_id) - issue_url, issue_key = ticket.create_ticket( + # CREATE TICKET + issue_key = ticket.create_ticket( summary, whole_description_str, project_key, @@ -162,6 +168,51 @@ def get_error_info_from_robot( affects_version, parent_key, ) - ticket.open_issue(issue_key) - ticket.post_attachment_to_ticket(issue_key, saved_file_path) - ticket.post_attachment_to_ticket(issue_key, saved_file_path_calibration) + # OPEN TICKET + issue_url = ticket.open_issue(issue_key) + # MOVE FILES TO ERROR FOLDER. + error_files = [saved_file_path_calibration, run_log_file_path] + file_paths + error_folder_path = os.path.join(storage_directory, str("RABR-238")) + os.makedirs(error_folder_path, exist_ok=True) + for source_file in error_files: + destination_file = os.path.join( + error_folder_path, os.path.basename(source_file) + ) + shutil.move(source_file, destination_file) + # OPEN FOLDER DIRECTORY + subprocess.Popen(["explorer", error_folder_path]) + # CONNECT TO GOOGLE DRIVE + credentials_path = os.path.join(storage_directory, "credentials.json") + google_sheet_name = "ABR-run-data" + try: + google_drive = google_drive_tool.google_drive( + credentials_path, + "1Cvej0eadFOTZr9ILRXJ0Wg65ymOtxL4m", + "rhyann.clarke@opentrons.ocm", + ) + print("Connected to google drive.") + except json.decoder.JSONDecodeError: + print( + "Credential file is damaged. Get from https://console.cloud.google.com/apis/credentials" + ) + sys.exit() + # CONNECT TO GOOGLE SHEET + try: + google_sheet = google_sheets_tool.google_sheet( + credentials_path, google_sheet_name, 0 + ) + print(f"Connected to google sheet: {google_sheet_name}") + except gspread.exceptions.APIError: + print("ERROR: Check google sheet name. Check credentials file.") + sys.exit() + # WRITE ERRORED RUN TO GOOGLE SHEET + error_run_log = os.path.join(error_folder_path, os.path.basename(run_log_file_path)) + google_drive.upload_file(error_run_log) + run_id = os.path.basename(error_run_log).split("_")[1].split(".")[0] + runs_and_robots, headers = abr_google_drive.create_data_dictionary( + run_id, error_folder_path, issue_url + ) + read_robot_logs.write_to_local_and_google_sheet( + runs_and_robots, storage_directory, google_sheet_name, google_sheet, headers + ) + print("Wrote run to ABR-run-data") diff --git a/abr-testing/abr_testing/data_collection/read_robot_logs.py b/abr-testing/abr_testing/data_collection/read_robot_logs.py index 0e31603b7da..48ef1d20163 100644 --- a/abr-testing/abr_testing/data_collection/read_robot_logs.py +++ b/abr-testing/abr_testing/data_collection/read_robot_logs.py @@ -14,6 +14,35 @@ import requests +def lpc_data(file_results: Dict[str, Any], protocol_info: Dict) -> List[Dict[str, Any]]: + """Get labware offsets from one run log.""" + offsets = file_results.get("labwareOffsets", "") + all_offsets: List[Dict[str, Any]] = [] + if len(offsets) > 0: + for offset in offsets: + labware_type = offset.get("definitionUri", "") + slot = offset["location"].get("slotName", "") + module_location = offset["location"].get("moduleModel", "") + adapter = offset["location"].get("definitionUri", "") + x_offset = offset["vector"].get("x", 0.0) + y_offset = offset["vector"].get("y", 0.0) + z_offset = offset["vector"].get("z", 0.0) + created_at = offset.get("createdAt", "") + row = { + "createdAt": created_at, + "Labware Type": labware_type, + "Slot": slot, + "Module": module_location, + "Adapter": adapter, + "X": x_offset, + "Y": y_offset, + "Z": z_offset, + } + row2 = {**protocol_info, **row} + all_offsets.append(row2) + return all_offsets + + def command_time(command: Dict[str, str]) -> Tuple[float, float]: """Calculate total create and complete time per command.""" try: @@ -82,11 +111,11 @@ def hs_commands(file_results: Dict[str, Any]) -> Dict[str, float]: temp_time = datetime.strptime( command.get("completedAt", ""), "%Y-%m-%dT%H:%M:%S.%f%z" ) - + hs_latch_sets = hs_latch_count / 2 # one set of open/close hs_total_rotations = sum(hs_rotations.values()) hs_total_temp_time = sum(hs_temps.values()) hs_dict = { - "Heatershaker # of Latch Engagements": hs_latch_count, + "Heatershaker # of Latch Open/Close": hs_latch_sets, "Heatershaker # of Homes": hs_home_count, "Heatershaker # of Rotations": hs_total_rotations, "Heatershaker Temp On Time (sec)": hs_total_temp_time, @@ -206,9 +235,9 @@ def thermocycler_commands(file_results: Dict[str, Any]) -> Dict[str, float]: block_total_time = sum(block_temps.values()) lid_total_time = sum(lid_temps.values()) - + lid_sets = lid_engagements / 2 tc_dict = { - "Thermocycler # of Lid Engagements": lid_engagements, + "Thermocycler # of Lid Open/Close": lid_sets, "Thermocycler Block # of Temp Changes": block_temp_changes, "Thermocycler Block Temp On Time (sec)": block_total_time, "Thermocycler Lid # of Temp Changes": lid_temp_changes, @@ -223,7 +252,6 @@ def create_abr_data_sheet( ) -> str: """Creates csv file to log ABR data.""" file_name_csv = file_name + ".csv" - print(file_name_csv) sheet_location = os.path.join(storage_directory, file_name_csv) if os.path.exists(sheet_location): print(f"File {sheet_location} located. Not overwriting.") @@ -427,3 +455,28 @@ def get_calibration_offsets( saved_file_path = os.path.join(storage_directory, save_name) json.dump(calibration, open(saved_file_path, mode="w")) return saved_file_path, calibration + + +def get_logs(storage_directory: str, ip: str) -> List[str]: + """Get Robot logs.""" + log_types = ["api.log", "server.log", "serial.log", "touchscreen.log"] + all_paths = [] + for log_type in log_types: + try: + response = requests.get( + f"http://{ip}:31950/logs/{log_type}", + headers={"log_identifier": log_type}, + params={"records": 5000}, + ) + response.raise_for_status() + log_data = response.text + log_name = ip + "_" + log_type.split(".")[0] + ".json" + file_path = os.path.join(storage_directory, log_name) + with open(file_path, mode="w", encoding="utf-8") as file: + file.write(response.text) + json.dump(log_data, open(file_path, mode="w")) + except RuntimeError: + print(f"Request exception. Did not save {log_type}") + continue + all_paths.append(file_path) + return all_paths diff --git a/abr-testing/abr_testing/tools/abr_scale.py b/abr-testing/abr_testing/tools/abr_scale.py index 0947091fe4b..75c887d4ecc 100644 --- a/abr-testing/abr_testing/tools/abr_scale.py +++ b/abr-testing/abr_testing/tools/abr_scale.py @@ -73,8 +73,12 @@ print("No google sheets credentials. Add credentials to storage notebook.") # Scale Loop + grams, is_stable = scale.read_mass() + grams, is_stable = scale.read_mass() + is_stable = False break_all = False while is_stable is False: + grams, is_stable = scale.read_mass() grams, is_stable = scale.read_mass() print(f"Scale reading: grams={grams}, is_stable={is_stable}") time_now = datetime.datetime.now() @@ -90,9 +94,12 @@ y_or_no = input("Do you want to weigh another sample? (Y/N): ") if y_or_no == "Y": # Uses same storage directory and file. + grams, is_stable = scale.read_mass() + is_stable = False robot = input("Robot: ") labware = input("Labware: ") protocol_step = input("Measurement Step (1,2,3): ") + grams, is_stable = scale.read_mass() elif y_or_no == "N": break_all = True if break_all: From f0398974be58d96bc3465e858be17376969c6d4e Mon Sep 17 00:00:00 2001 From: Josh McVey Date: Tue, 9 Apr 2024 13:34:21 -0500 Subject: [PATCH 080/194] App style 96 ch exit text (#14843) Into edge instead of release branch. Was #14840 Co-authored-by: Jamey Huffnagle --- app/src/organisms/PipetteWizardFlows/UnskippableModal.tsx | 2 +- .../PipetteWizardFlows/__tests__/UnskippableModal.test.tsx | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/src/organisms/PipetteWizardFlows/UnskippableModal.tsx b/app/src/organisms/PipetteWizardFlows/UnskippableModal.tsx index 5355349b656..497e5fc19b0 100644 --- a/app/src/organisms/PipetteWizardFlows/UnskippableModal.tsx +++ b/app/src/organisms/PipetteWizardFlows/UnskippableModal.tsx @@ -32,7 +32,7 @@ export function UnskippableModal(props: UnskippableModalProps): JSX.Element { diff --git a/app/src/organisms/PipetteWizardFlows/__tests__/UnskippableModal.test.tsx b/app/src/organisms/PipetteWizardFlows/__tests__/UnskippableModal.test.tsx index fd28aa5e8df..43fa441c7d1 100644 --- a/app/src/organisms/PipetteWizardFlows/__tests__/UnskippableModal.test.tsx +++ b/app/src/organisms/PipetteWizardFlows/__tests__/UnskippableModal.test.tsx @@ -41,7 +41,9 @@ describe('UnskippableModal', () => { screen.getByText( 'You must detach the mounting plate and reattach the z-axis carraige before using other pipettes. We do not recommend exiting this process before completion.' ) - fireEvent.click(screen.getByRole('button', { name: 'exit' })) + screen.getByText('Exit') + screen.getByText('Go back') + fireEvent.click(screen.getByRole('button', { name: 'Exit' })) expect(props.proceed).toHaveBeenCalled() }) }) From e345d32ab0507cbd86bd697b1fcdce7c99177e94 Mon Sep 17 00:00:00 2001 From: koji Date: Tue, 9 Apr 2024 14:41:30 -0400 Subject: [PATCH 081/194] fix(shared-data, app): fix runtime parameters range display (#14847) * fix(shared-data, app): fix runtime parameters range display --- app/src/pages/ProtocolDetails/Parameters.tsx | 7 +-- .../src/molecules/ParametersTable/index.tsx | 3 +- .../orderRuntimeParameterRangeOptions.test.ts | 50 +++++++++++++++++++ shared-data/js/helpers/index.ts | 1 + .../orderRuntimeParameterRangeOptions.ts | 46 +++++++++++++++++ shared-data/js/types.ts | 2 +- 6 files changed, 104 insertions(+), 5 deletions(-) create mode 100644 shared-data/js/helpers/__tests__/orderRuntimeParameterRangeOptions.test.ts create mode 100644 shared-data/js/helpers/orderRuntimeParameterRangeOptions.ts diff --git a/app/src/pages/ProtocolDetails/Parameters.tsx b/app/src/pages/ProtocolDetails/Parameters.tsx index b908b5b84d7..0b280a2af3d 100644 --- a/app/src/pages/ProtocolDetails/Parameters.tsx +++ b/app/src/pages/ProtocolDetails/Parameters.tsx @@ -4,6 +4,7 @@ import styled from 'styled-components' import { formatRunTimeParameterDefaultValue, formatRunTimeParameterMinMax, + orderRuntimeParameterRangeOptions, } from '@opentrons/shared-data' import { BORDERS, @@ -62,13 +63,13 @@ export const Parameters = (props: { protocolId: string }): JSX.Element => { makeSnackbar(t('start_setup_customize_values')) } - const getRange = (parameter: RunTimeParameter): string => { + const formatRange = (parameter: RunTimeParameter): string => { const { type } = parameter const numChoices = 'choices' in parameter ? parameter.choices.length : 0 const minMax = formatRunTimeParameterMinMax(parameter) let range: string | null = null if (numChoices === 2 && 'choices' in parameter) { - range = `${parameter.choices[0].displayName}, ${parameter.choices[1].displayName}` + range = orderRuntimeParameterRangeOptions(parameter.choices) } switch (type) { @@ -125,7 +126,7 @@ export const Parameters = (props: { protocolId: string }): JSX.Element => { - {getRange(parameter)} + {formatRange(parameter)} diff --git a/components/src/molecules/ParametersTable/index.tsx b/components/src/molecules/ParametersTable/index.tsx index 485a5efc6e5..5ae0d36d550 100644 --- a/components/src/molecules/ParametersTable/index.tsx +++ b/components/src/molecules/ParametersTable/index.tsx @@ -3,6 +3,7 @@ import styled, { css } from 'styled-components' import { formatRunTimeParameterDefaultValue, formatRunTimeParameterMinMax, + orderRuntimeParameterRangeOptions, } from '@opentrons/shared-data' import { BORDERS, COLORS } from '../../helix-design-system' import { SPACING, TYPOGRAPHY } from '../../ui-style-constants/index' @@ -38,7 +39,7 @@ export function ParametersTable({ ? t != null ? t('num_options', { num: count }) : `${count} options` - : choices.map(choice => choice.displayName).join(', ') + : orderRuntimeParameterRangeOptions(choices) } switch (type) { diff --git a/shared-data/js/helpers/__tests__/orderRuntimeParameterRangeOptions.test.ts b/shared-data/js/helpers/__tests__/orderRuntimeParameterRangeOptions.test.ts new file mode 100644 index 00000000000..2a5b62b265d --- /dev/null +++ b/shared-data/js/helpers/__tests__/orderRuntimeParameterRangeOptions.test.ts @@ -0,0 +1,50 @@ +import { describe, it, expect } from 'vitest' + +import { + isNumeric, + orderRuntimeParameterRangeOptions, +} from '../orderRuntimeParameterRangeOptions' + +import type { Choice } from '../../types' + +describe('isNumeric', () => { + it('should return true when input is "2"', () => { + const result = isNumeric('2') + expect(result).toBeTruthy() + }) + + it('should return false when input is "opentrons"', () => { + const result = isNumeric('opentrons') + expect(result).toBeFalsy() + }) +}) + +describe('orderRuntimeParameterRangeOptions', () => { + it('should return numerical order when choices are number', () => { + const mockChoices: Choice[] = [ + { displayName: '20', value: 20 }, + { displayName: '16', value: 16 }, + ] + const result = orderRuntimeParameterRangeOptions(mockChoices) + expect(result).toEqual('16, 20') + }) + + it('should return alphabetical order when choices are number', () => { + const mockChoices: Choice[] = [ + { displayName: 'Single channel 50µL', value: 'flex_1channel_50' }, + { displayName: 'Eight Channel 50µL', value: 'flex_8channel_50' }, + ] + const result = orderRuntimeParameterRangeOptions(mockChoices) + expect(result).toEqual('Eight Channel 50µL, Single channel 50µL') + }) + + it('should return empty string choices > 3', () => { + const mockChoices: Choice[] = [ + { displayName: '20', value: 20 }, + { displayName: '16', value: 16 }, + { displayName: '18', value: 18 }, + ] + const result = orderRuntimeParameterRangeOptions(mockChoices) + expect(result).toEqual('') + }) +}) diff --git a/shared-data/js/helpers/index.ts b/shared-data/js/helpers/index.ts index 854b82d5133..0cb4ec7d88a 100644 --- a/shared-data/js/helpers/index.ts +++ b/shared-data/js/helpers/index.ts @@ -31,6 +31,7 @@ export * from './getSimplestFlexDeckConfig' export * from './formatRunTimeParameterDefaultValue' export * from './formatRunTimeParameterValue' export * from './formatRunTimeParameterMinMax' +export * from './orderRuntimeParameterRangeOptions' export const getLabwareDefIsStandard = (def: LabwareDefinition2): boolean => def?.namespace === OPENTRONS_LABWARE_NAMESPACE diff --git a/shared-data/js/helpers/orderRuntimeParameterRangeOptions.ts b/shared-data/js/helpers/orderRuntimeParameterRangeOptions.ts new file mode 100644 index 00000000000..c372e992a2b --- /dev/null +++ b/shared-data/js/helpers/orderRuntimeParameterRangeOptions.ts @@ -0,0 +1,46 @@ +import type { Choice } from '../types' + +export const isNumeric = (str: string): boolean => { + return !isNaN(Number(str)) +} + +/** + * This function sorts an array of strings in numerical and alphabetical order. + * @param {Choice[]} - The array of Choice + * Choice is an object like {displayName: 'Single channel 50µL', value: 'flex_1channel_50' } + * @returns {string} The ordered string with "," + * + * examples + * [ + { displayName: '20', value: 20 }, + { displayName: '16', value: 16 }, + ] + return 16, 20 + + [ + { displayName: 'Single channel 50µL', value: 'flex_1channel_50' }, + { displayName: 'Eight Channel 50µL', value: 'flex_8channel_50' }, + ] + return Eight Channel 50µL, Single channel 50µL + */ +export const orderRuntimeParameterRangeOptions = ( + choices: Choice[] +): string => { + // when this function is called, the array length is always 2 + if (choices.length > 2) { + console.error(`expected to have length 2 but has length ${choices.length}`) + return '' + } + const displayNames = [choices[0].displayName, choices[1].displayName] + if (isNumeric(displayNames[0])) { + return displayNames + .sort((a, b) => { + const numA = Number(a) + const numB = Number(b) + return numA - numB + }) + .join(', ') + } else { + return displayNames.sort().join(', ') + } +} diff --git a/shared-data/js/types.ts b/shared-data/js/types.ts index 13fa4491a43..75466e7558e 100644 --- a/shared-data/js/types.ts +++ b/shared-data/js/types.ts @@ -597,7 +597,7 @@ export interface NumberParameter { default: number } -interface Choice { +export interface Choice { displayName: string value: number | boolean | string } From 2a82fef538b2996c12ede8c5e2ffc1d82578bdc3 Mon Sep 17 00:00:00 2001 From: Jamey Huffnagle Date: Tue, 9 Apr 2024 14:45:56 -0400 Subject: [PATCH 082/194] style(app): Adjust desktop app "moveToWell" command text font size (#14849) Closes RQA-2428 --- app/src/organisms/CommandText/index.tsx | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/app/src/organisms/CommandText/index.tsx b/app/src/organisms/CommandText/index.tsx index 06eae754759..47c54140149 100644 --- a/app/src/organisms/CommandText/index.tsx +++ b/app/src/organisms/CommandText/index.tsx @@ -190,11 +190,15 @@ export function CommandText(props: Props): JSX.Element | null { robotType ) : '' - return t('move_to_well', { - well_name: wellName, - labware: getLabwareName(robotSideAnalysis, labwareId), - labware_location: displayLocation, - }) + return ( + + {t('move_to_well', { + well_name: wellName, + labware: getLabwareName(robotSideAnalysis, labwareId), + labware_location: displayLocation, + })} + + ) } case 'moveLabware': { return ( From 30125683a0ae584b9281ceff85aea208428d1e5b Mon Sep 17 00:00:00 2001 From: Nick Diehl <47604184+ncdiehl11@users.noreply.github.com> Date: Tue, 9 Apr 2024 15:09:00 -0400 Subject: [PATCH 083/194] refactor(app): switch ODD update modal progress bar with spinner (#14838) closes RQA-2553 --- .../UpdateRobotSoftware/UpdateSoftware.tsx | 16 ++++++------- .../__tests__/UpdateRobotSoftware.test.tsx | 2 +- .../__tests__/UpdateSoftware.test.tsx | 24 ++++--------------- .../organisms/UpdateRobotSoftware/index.tsx | 9 ++----- 4 files changed, 16 insertions(+), 35 deletions(-) diff --git a/app/src/organisms/UpdateRobotSoftware/UpdateSoftware.tsx b/app/src/organisms/UpdateRobotSoftware/UpdateSoftware.tsx index 60ff6cc18de..7d625254a2f 100644 --- a/app/src/organisms/UpdateRobotSoftware/UpdateSoftware.tsx +++ b/app/src/organisms/UpdateRobotSoftware/UpdateSoftware.tsx @@ -4,25 +4,21 @@ import { useTranslation } from 'react-i18next' import { ALIGN_CENTER, BORDERS, - Box, COLORS, DIRECTION_COLUMN, Flex, + Icon, JUSTIFY_CENTER, SPACING, StyledText, TYPOGRAPHY, } from '@opentrons/components' -import { ProgressBar } from '../../atoms/ProgressBar' - interface UpdateSoftwareProps { updateType: 'downloading' | 'validating' | 'sendingFile' | 'installing' | null - processProgress: number } export function UpdateSoftware({ updateType, - processProgress, }: UpdateSoftwareProps): JSX.Element { const { t } = useTranslation('device_settings') const renderText = (): string | null => { @@ -52,6 +48,13 @@ export function UpdateSoftware({ height="33rem" borderRadius={BORDERS.borderRadius12} > + - - - ) } diff --git a/app/src/organisms/UpdateRobotSoftware/__tests__/UpdateRobotSoftware.test.tsx b/app/src/organisms/UpdateRobotSoftware/__tests__/UpdateRobotSoftware.test.tsx index 242b40c4be8..5db3c1358eb 100644 --- a/app/src/organisms/UpdateRobotSoftware/__tests__/UpdateRobotSoftware.test.tsx +++ b/app/src/organisms/UpdateRobotSoftware/__tests__/UpdateRobotSoftware.test.tsx @@ -113,7 +113,7 @@ describe('UpdateRobotSoftware', () => { render() expect(mockBeforeCommitting).toBeCalled() expect(UpdateSoftware).toBeCalledWith( - { updateType: 'installing', processProgress: 0 }, + { updateType: 'installing' }, expect.anything() ) screen.getByText('mock UpdateSoftware') diff --git a/app/src/organisms/UpdateRobotSoftware/__tests__/UpdateSoftware.test.tsx b/app/src/organisms/UpdateRobotSoftware/__tests__/UpdateSoftware.test.tsx index 913f2c26dea..680de1b0147 100644 --- a/app/src/organisms/UpdateRobotSoftware/__tests__/UpdateSoftware.test.tsx +++ b/app/src/organisms/UpdateRobotSoftware/__tests__/UpdateSoftware.test.tsx @@ -1,9 +1,8 @@ import * as React from 'react' import { screen } from '@testing-library/react' -import { describe, it, beforeEach, expect } from 'vitest' +import { describe, it, beforeEach } from 'vitest' import '@testing-library/jest-dom/vitest' import { renderWithProviders } from '../../../__testing-utils__' -import { COLORS } from '@opentrons/components' import { i18n } from '../../../i18n' import { UpdateSoftware } from '../UpdateSoftware' @@ -18,47 +17,34 @@ describe('UpdateSoftware', () => { beforeEach(() => { props = { updateType: 'downloading', - processProgress: 50, } }) - it('should render text and progressbar - downloading software', () => { + it('should render text - downloading software', () => { render(props) screen.getByText('Downloading software...') - const bar = screen.getByTestId('ProgressBar_Bar') - expect(bar).toHaveStyle(`background: ${String(COLORS.blue50)}`) - expect(bar).toHaveStyle('width: 50%') }) - it('should render text and progressbar - sending software', () => { + it('should render text - sending software', () => { props = { ...props, - processProgress: 20, updateType: 'sendingFile', } render(props) screen.getByText('Sending software...') - const bar = screen.getByTestId('ProgressBar_Bar') - expect(bar).toHaveStyle('width: 20%') }) - it('should render text and progressbar - validating software', () => { + it('should render text - validating software', () => { props = { ...props, - processProgress: 80, updateType: 'validating', } render(props) screen.getByText('Validating software...') - const bar = screen.getByTestId('ProgressBar_Bar') - expect(bar).toHaveStyle('width: 80%') }) - it('should render text and progressbar - installing software', () => { + it('should render text - installing software', () => { props = { ...props, - processProgress: 5, updateType: 'installing', } render(props) screen.getByText('Installing software...') - const bar = screen.getByTestId('ProgressBar_Bar') - expect(bar).toHaveStyle('width: 5%') }) }) diff --git a/app/src/organisms/UpdateRobotSoftware/index.tsx b/app/src/organisms/UpdateRobotSoftware/index.tsx index c88f3197491..4d61272ac6f 100644 --- a/app/src/organisms/UpdateRobotSoftware/index.tsx +++ b/app/src/organisms/UpdateRobotSoftware/index.tsx @@ -37,7 +37,7 @@ export function UpdateRobotSoftware( const dispatch = useDispatch() const session = useSelector(getRobotUpdateSession) - const { step, stage, progress, error: sessionError } = session ?? { + const { step, stage, error: sessionError } = session ?? { step: null, error: null, } @@ -76,11 +76,6 @@ export function UpdateRobotSoftware( beforeCommittingSuccessfulUpdate && beforeCommittingSuccessfulUpdate() } } - return ( - - ) + return } } From 61b137132a3130e8cea170c7cf3d4c1931735942 Mon Sep 17 00:00:00 2001 From: Seth Foster Date: Tue, 9 Apr 2024 15:32:23 -0400 Subject: [PATCH 084/194] fix(app): display app version again (#14844) When we switched to vite, we had to switch all the stuff we'd been injecting at pack time via webpack environment/define plugins to vite's `define` config functionality. The biggest thing we specify that way is the app version, which is used across the stack for display and for logic. In the commit that switched to vite, we added that injection for the app-shell vite configs but did not add it for the app vite configs. That meant that at runtime, the version value was undefined, which breaks robot update notifications and causes the app version in the general settings tab to not display (it also makes the logo wrong on internal releases but that's a bit less important). The fix is to inject the version into the app build again. This is made a little more complicated because if you're doing stuff to the app vite config, it has to work in both the vite devserver and the vite offline packaging environments, and the vite devserver doesn't allow commonjs, and the git-version script that gives us the version is commonjs. For the purposes of vite's devserver, "doesn't work with cjs" actually just means "doesn't support require()", so you can use a hybrid syntax that uses import-statements but still module.export instead of export statements. Unfortunately, the git-version script is also used in the electron-builder config for the app-shell and the app-shell-odd, and the electron-builder config is run via node, and to import an ESM from a node CJS script - which electron-builder.config.js is - you need to change your import syntax to dynamic import and you need to make the import target explicitly (to node) an ESM, aka change its extension, and you need to use full ESM syntax including exports. This also goes for the create-release script. So that means that - git-version.js becomes git-version.mjs and uses full ESM syntax - that means that everywhere it's imported we need to import it by full path with extension instead of module name - also we need to import it dynamically in the electron config - oh and we need to actually add the define configs so we get the version in the app And then finally we show the version again. Also, remove some old webpack.config.js files that aren't used anymore. Closes EXEC-385 --- app-shell-odd/vite.config.ts | 5 +- app-shell/electron-builder.config.js | 5 +- app-shell/vite.config.ts | 5 +- app/vite.config.ts | 109 +++++++++++--------- components/webpack.config.js | 38 ------- discovery-client/vite.config.ts | 7 +- discovery-client/webpack.config.js | 26 ----- labware-designer/webpack.config.js | 26 ----- labware-library/webpack.config.js | 67 ------------ protocol-designer/vite.config.ts | 5 +- scripts/deploy/create-release.js | 55 +++++++--- scripts/{git-version.js => git-version.mjs} | 28 ++--- scripts/update-releases-json.js | 2 +- shared-data/webpack.config.js | 20 ---- usb-bridge/node-client/webpack.config.js | 26 ----- 15 files changed, 124 insertions(+), 300 deletions(-) delete mode 100644 components/webpack.config.js delete mode 100644 discovery-client/webpack.config.js delete mode 100644 labware-designer/webpack.config.js delete mode 100644 labware-library/webpack.config.js rename scripts/{git-version.js => git-version.mjs} (79%) delete mode 100644 shared-data/webpack.config.js delete mode 100644 usb-bridge/node-client/webpack.config.js diff --git a/app-shell-odd/vite.config.ts b/app-shell-odd/vite.config.ts index b9575159675..a3b8351fee6 100644 --- a/app-shell-odd/vite.config.ts +++ b/app-shell-odd/vite.config.ts @@ -1,13 +1,14 @@ -import { versionForProject } from '../scripts/git-version' +import { versionForProject } from '../scripts/git-version.mjs' import pkg from './package.json' import path from 'path' -import { UserConfig, defineConfig } from 'vite' +import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' import postCssImport from 'postcss-import' import postCssApply from 'postcss-apply' import postColorModFunction from 'postcss-color-mod-function' import postCssPresetEnv from 'postcss-preset-env' import lostCss from 'lost' +import type {UserConfig} from 'vite' export default defineConfig( async (): Promise => { diff --git a/app-shell/electron-builder.config.js b/app-shell/electron-builder.config.js index 727b2d5e900..aa61720338b 100644 --- a/app-shell/electron-builder.config.js +++ b/app-shell/electron-builder.config.js @@ -1,6 +1,5 @@ 'use strict' const path = require('path') -const { versionForProject } = require('../scripts/git-version') const { OT_APP_DEPLOY_BUCKET, @@ -45,7 +44,9 @@ module.exports = async () => ({ }, ], extraMetadata: { - version: await versionForProject(project), + version: await ( + await import('../scripts/git-version.mjs') + ).versionForProject(project), productName: project === 'robot-stack' ? 'Opentrons' : 'Opentrons-OT3', }, extraResources: USE_PYTHON ? ['python'] : [], diff --git a/app-shell/vite.config.ts b/app-shell/vite.config.ts index 80ca80b0aa4..546fe19e23f 100644 --- a/app-shell/vite.config.ts +++ b/app-shell/vite.config.ts @@ -1,7 +1,8 @@ -import { versionForProject } from '../scripts/git-version' +import { versionForProject } from '../scripts/git-version.mjs' import pkg from './package.json' import path from 'path' -import { UserConfig, defineConfig } from 'vite' +import { defineConfig } from 'vite' +import type { UserConfig } from 'vite' export default defineConfig( async (): Promise => { diff --git a/app/vite.config.ts b/app/vite.config.ts index 9710acdd240..f88d492056a 100644 --- a/app/vite.config.ts +++ b/app/vite.config.ts @@ -6,57 +6,66 @@ import postCssApply from 'postcss-apply' import postColorModFunction from 'postcss-color-mod-function' import postCssPresetEnv from 'postcss-preset-env' import lostCss from 'lost' +import { versionForProject } from '../scripts/git-version.mjs' +import type { UserConfig } from 'vite' -export default defineConfig({ - // this makes imports relative rather than absolute - base: '', - build: { - // Relative to the root - outDir: 'dist', - }, - plugins: [ - react({ - include: '**/*.tsx', - babel: { - // Use babel.config.js files - configFile: true, +export default defineConfig( + async(): Promise => { + const project = process.env.OPENTRONS_PROJECT ?? 'robot-stack' + const version = await versionForProject(project) + return { + // this makes imports relative rather than absolute + base: '', + build: { + // Relative to the root + outDir: 'dist', }, - }), - ], - optimizeDeps: { - esbuildOptions: { - target: 'es2020', - }, - }, - css: { - postcss: { plugins: [ - postCssImport({ root: 'src/' }), - postCssApply(), - postColorModFunction(), - postCssPresetEnv({ stage: 0 }), - lostCss(), + react({ + include: '**/*.tsx', + babel: { + // Use babel.config.js files + configFile: true, + }, + }), ], - }, - }, - define: { - 'process.env': process.env, - global: 'globalThis', - }, - resolve: { - alias: { - '@opentrons/components/styles': path.resolve( - '../components/src/index.module.css' - ), - '@opentrons/components': path.resolve('../components/src/index.ts'), - '@opentrons/shared-data': path.resolve('../shared-data/js/index.ts'), - '@opentrons/step-generation': path.resolve( - '../step-generation/src/index.ts' - ), - '@opentrons/api-client': path.resolve('../api-client/src/index.ts'), - '@opentrons/react-api-client': path.resolve( - '../react-api-client/src/index.ts' - ), - }, - }, -}) + optimizeDeps: { + esbuildOptions: { + target: 'es2020', + }, + }, + css: { + postcss: { + plugins: [ + postCssImport({ root: 'src/' }), + postCssApply(), + postColorModFunction(), + postCssPresetEnv({ stage: 0 }), + lostCss(), + ], + }, + }, + define: { + 'process.env': process.env, + global: 'globalThis', + _PKG_VERSION_: JSON.stringify(version), + _OPENTRONS_PROJECT_: JSON.stringify(project), + }, + resolve: { + alias: { + '@opentrons/components/styles': path.resolve( + '../components/src/index.module.css' + ), + '@opentrons/components': path.resolve('../components/src/index.ts'), + '@opentrons/shared-data': path.resolve('../shared-data/js/index.ts'), + '@opentrons/step-generation': path.resolve( + '../step-generation/src/index.ts' + ), + '@opentrons/api-client': path.resolve('../api-client/src/index.ts'), + '@opentrons/react-api-client': path.resolve( + '../react-api-client/src/index.ts' + ), + }, + }, + } + }) diff --git a/components/webpack.config.js b/components/webpack.config.js deleted file mode 100644 index 648eaee3432..00000000000 --- a/components/webpack.config.js +++ /dev/null @@ -1,38 +0,0 @@ -'use strict' - -const path = require('path') -const { rules } = require('@opentrons/webpack-config') - -const ENTRY_INDEX = path.join(__dirname, 'src/barrel.ts') -const OUTPUT_PATH = path.join(__dirname, 'lib') - -module.exports = { - target: 'web', - entry: { index: ENTRY_INDEX }, - output: { - path: OUTPUT_PATH, - filename: 'opentrons-components.js', - library: '@opentrons/components', - libraryTarget: 'umd', - globalObject: 'this', - }, - mode: 'production', - module: { rules: [rules.js] }, - resolve: { - extensions: ['.wasm', '.mjs', '.js', '.ts', '.tsx', '.json'], - }, - externals: { - react: { - root: 'React', - commonjs2: 'react', - commonjs: 'react', - amd: 'react', - }, - 'react-dom': { - root: 'ReactDOM', - commonjs2: 'react-dom', - commonjs: 'react-dom', - amd: 'react-dom', - }, - }, -} diff --git a/discovery-client/vite.config.ts b/discovery-client/vite.config.ts index 7cbd9ae43c3..203012d904a 100644 --- a/discovery-client/vite.config.ts +++ b/discovery-client/vite.config.ts @@ -1,14 +1,15 @@ -import { versionForProject } from '../scripts/git-version' +import { versionForProject } from '../scripts/git-version.mjs' import pkg from './package.json' import path from 'path' -import { UserConfig, defineConfig } from 'vite' +import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' import postCssImport from 'postcss-import' import postCssApply from 'postcss-apply' import postColorModFunction from 'postcss-color-mod-function' import postCssPresetEnv from 'postcss-preset-env' import lostCss from 'lost' - +import type { UserConfig } from 'vite +' export default defineConfig( async (): Promise => { const project = process.env.OPENTRONS_PROJECT ?? 'robot-stack' diff --git a/discovery-client/webpack.config.js b/discovery-client/webpack.config.js deleted file mode 100644 index c15a3bae1c2..00000000000 --- a/discovery-client/webpack.config.js +++ /dev/null @@ -1,26 +0,0 @@ -'use strict' - -const path = require('path') -const webpackMerge = require('webpack-merge') -const { DefinePlugin } = require('webpack') -const { nodeBaseConfig } = require('@opentrons/webpack-config') -const { versionForProject } = require('../scripts/git-version') - -const ENTRY_INDEX = path.join(__dirname, 'src/index.ts') -const ENTRY_CLI = path.join(__dirname, 'src/cli.ts') -const OUTPUT_PATH = path.join(__dirname, 'lib') -const project = process.env.OPENTRONS_PROJECT ?? 'robot-stack' - -module.exports = async () => - webpackMerge(nodeBaseConfig, { - entry: { - index: ENTRY_INDEX, - cli: ENTRY_CLI, - }, - output: { path: OUTPUT_PATH }, - plugins: [ - new DefinePlugin({ - _PKG_VERSION_: JSON.stringify(await versionForProject(project)), - }), - ], - }) diff --git a/labware-designer/webpack.config.js b/labware-designer/webpack.config.js deleted file mode 100644 index aec3b7cc0cb..00000000000 --- a/labware-designer/webpack.config.js +++ /dev/null @@ -1,26 +0,0 @@ -'use strict' - -const path = require('path') -const webpackMerge = require('webpack-merge') -const HtmlWebpackPlugin = require('html-webpack-plugin') -const ScriptExtHtmlWebpackPlugin = require('script-ext-html-webpack-plugin') - -const { baseConfig } = require('@opentrons/webpack-config') -const { productName: title, description, author } = require('./package.json') - -const JS_ENTRY = path.join(__dirname, 'src/index.tsx') -const HTML_ENTRY = path.join(__dirname, 'src/index.hbs') -const OUTPUT_PATH = path.join(__dirname, 'dist') - -module.exports = webpackMerge(baseConfig, { - entry: [JS_ENTRY], - - output: { - path: OUTPUT_PATH, - }, - - plugins: [ - new HtmlWebpackPlugin({ title, description, author, template: HTML_ENTRY }), - new ScriptExtHtmlWebpackPlugin({ defaultAttribute: 'defer' }), - ], -}) diff --git a/labware-library/webpack.config.js b/labware-library/webpack.config.js deleted file mode 100644 index c5fb0d8c7e8..00000000000 --- a/labware-library/webpack.config.js +++ /dev/null @@ -1,67 +0,0 @@ -'use strict' -const path = require('path') -const webpack = require('webpack') -const merge = require('webpack-merge') -const HtmlWebpackPlugin = require('html-webpack-plugin') -// const glob = require('glob') - -const { baseConfig } = require('@opentrons/webpack-config') -// const {baseConfig, DEV_MODE} = require('@opentrons/webpack-config') -const pkg = require('./package.json') - -const { versionForProject } = require('../scripts/git-version') - -const JS_ENTRY = path.join(__dirname, 'src/index.tsx') -const HTML_ENTRY = path.join(__dirname, 'src/index.hbs') -const OUT_PATH = path.join(__dirname, 'dist') - -const LABWARE_LIBRARY_ENV_VAR_PREFIX = 'OT_LL' - -const passThruEnvVars = Object.keys(process.env) - .filter(v => v.startsWith(LABWARE_LIBRARY_ENV_VAR_PREFIX)) - .concat(['NODE_ENV', 'CYPRESS']) - -const testAliases = - process.env.CYPRESS === '1' - ? { - 'file-saver': path.resolve(__dirname, 'cypress/mocks/file-saver.js'), - } - : {} - -module.exports = async () => { - const envVarsWithDefaults = { - OT_LL_VERSION: await versionForProject('labware-library'), - OT_LL_BUILD_DATE: new Date().toUTCString(), - } - - const envVars = passThruEnvVars.reduce( - (acc, envVar) => ({ [envVar]: '', ...acc }), - { ...envVarsWithDefaults } - ) - - return merge(baseConfig, { - entry: JS_ENTRY, - - output: { - path: OUT_PATH, - publicPath: '/', - }, - - plugins: [ - new webpack.EnvironmentPlugin(envVars), - - new HtmlWebpackPlugin({ - template: HTML_ENTRY, - title: pkg.productName, - description: pkg.description, - author: pkg.author.name, - gtmId: process.env.GTM_ID, - favicon: './src/images/favicon.ico', - }), - ], - - resolve: { - alias: testAliases, - }, - }) -} diff --git a/protocol-designer/vite.config.ts b/protocol-designer/vite.config.ts index 70d055a6fd8..7f7b8dd680d 100644 --- a/protocol-designer/vite.config.ts +++ b/protocol-designer/vite.config.ts @@ -1,12 +1,13 @@ import path from 'path' -import { UserConfig, defineConfig } from 'vite' +import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' import postCssImport from 'postcss-import' import postCssApply from 'postcss-apply' import postColorModFunction from 'postcss-color-mod-function' import postCssPresetEnv from 'postcss-preset-env' import lostCss from 'lost' -import { versionForProject } from '../scripts/git-version' +import { versionForProject } from '../scripts/git-version.mjs' +import type { UserConfig } from 'vite' const testAliases: {} | { 'file-saver': string } = process.env.CYPRESS === '1' diff --git a/scripts/deploy/create-release.js b/scripts/deploy/create-release.js index eb4db62bd2a..3b804506a2e 100644 --- a/scripts/deploy/create-release.js +++ b/scripts/deploy/create-release.js @@ -22,12 +22,6 @@ const parseArgs = require('./lib/parseArgs') const conventionalChangelog = require('conventional-changelog') const semver = require('semver') const { Octokit } = require('@octokit/rest') -const { - detailsFromTag, - tagFromDetails, - prefixForProject, - monorepoGit, -} = require('../git-version') const USAGE = '\nUsage:\n node ./scripts/deploy/create-release [--deploy] [--allow-old]' @@ -81,9 +75,35 @@ function versionPrevious(currentVersion, previousVersions) { return releasesOfGEQKind.length === 0 ? null : releasesOfGEQKind[0] } +async function gitVersion() { + let imported + if (imported === undefined) { + imported = await import('../git-version.mjs') + } + return imported +} + +async function monorepoGit() { + return await (await gitVersion()).monorepoGit() +} + +async function detailsFromTag(tag) { + return await (await gitVersion()).detailsFromTag(tag) +} + +async function tagFromDetails(project, version) { + return (await gitVersion()).tagFromDetails(project, version) +} + +async function prefixForProject(project) { + return (await gitVersion()).prefixForProject(project) +} + async function versionDetailsFromGit(tag, allowOld) { if (!allowOld) { - const last100 = await monorepoGit().log({ from: 'HEAD~100', to: 'HEAD' }) + const git = await monorepoGit() + const last100 = await git.log({ from: 'HEAD~100', to: 'HEAD' }) + if (!last100.all.some(commit => commit.refs.includes('tag: ' + tag))) { throw new Error( `Cannot find tag ${tag} in last 100 commits. You must run this script from a ref with ` + @@ -94,9 +114,8 @@ async function versionDetailsFromGit(tag, allowOld) { } const [project, currentVersion] = detailsFromTag(tag) - - const allTags = (await monorepoGit().tags([prefixForProject(project) + '*'])) - .all + const prefix = await prefixForProject(project) + const allTags = (await monorepoGit().tags([prefix + '*'])).all if (!allTags.includes(tag)) { throw new Error( `Tag ${tag} does not exist - create it before running this script` @@ -123,14 +142,15 @@ async function buildChangelog(project, currentVersion, previousVersion) { `## ${currentVersion}` + `\nFirst release for ${titleForProject(project)}` ) } - const previousTag = tagFromDetails(project, previousVersion) - + const previousTag = await tagFromDetails(project, previousVersion) + const currentTag = await tagFromDetails(project, currentVersion) + const prefix = await prefixForProject(Project) const changelogStream = conventionalChangelog( - { preset: 'angular', tagPrefix: prefixForProject(project) }, + { preset: 'angular', tagPrefix: prefix }, { version: currentVersion, - currentTag: tagFromDetails(project, currentVersion), - previousTag: previousTag, + currentTag, + previousTag, host: 'https://github.com', owner: REPO_DETAILS.owner, repository: REPO_DETAILS.repo, @@ -203,6 +223,7 @@ async function main() { currentVersion, previousVersion, ] = await versionDetailsFromGit(tag, allowOld) + const prefix = await prefixForProject(project) const changelog = await buildChangelog( project, currentVersion, @@ -211,8 +232,8 @@ async function main() { const truncatedChangelog = truncateAndAnnotate( changelog, 10000, - prefixForProject(project) + previousVersion, - prefixForProject(project) + currentVersion + prefix + previousVersion, + prefix + currentVersion ) return await createRelease( token, diff --git a/scripts/git-version.js b/scripts/git-version.mjs similarity index 79% rename from scripts/git-version.js rename to scripts/git-version.mjs index a2dab912f23..7b4d364d0da 100644 --- a/scripts/git-version.js +++ b/scripts/git-version.mjs @@ -15,23 +15,24 @@ // What that all boils down to is that we need, and this module provides, an interface to get the version of a // given project that currently exists in the monorepo. -const git = require('simple-git') -const { dirname } = require('path') -const REPO_BASE = dirname(__dirname) +import git from 'simple-git' +import { dirname } from 'path' +import { fileURLToPath } from 'url' +const REPO_BASE = dirname(dirname(fileURLToPath(import.meta.url))) -function monorepoGit() { +export function monorepoGit() { return git({ baseDir: REPO_BASE }) } -const detailsFromTag = tag => +export const detailsFromTag = tag => tag.includes('@') ? tag.split('@') : ['robot-stack', tag.substring(1)] -function tagFromDetails(project, version) { +export function tagFromDetails(project, version) { const prefix = prefixForProject(project) return `${prefix}${version}` } -function prefixForProject(project) { +export function prefixForProject(project) { if (project === 'robot-stack') { return 'v' } else { @@ -39,7 +40,7 @@ function prefixForProject(project) { } } -async function latestTagForProject(project) { +export async function latestTagForProject(project) { return ( await monorepoGit().raw([ 'describe', @@ -50,7 +51,7 @@ async function latestTagForProject(project) { ).trim() } -async function versionForProject(project) { +export async function versionForProject(project) { return latestTagForProject(project) .then(tag => detailsFromTag(tag)[1]) .catch(error => { @@ -60,12 +61,3 @@ async function versionForProject(project) { return '0.0.0-dev' }) } - -module.exports = { - detailsFromTag, - tagFromDetails, - prefixForProject, - latestTagForProject, - versionForProject, - monorepoGit, -} diff --git a/scripts/update-releases-json.js b/scripts/update-releases-json.js index 3286256c42b..0e529d5447e 100644 --- a/scripts/update-releases-json.js +++ b/scripts/update-releases-json.js @@ -4,7 +4,7 @@ const fs = require('fs/promises') // Updates a releases historical manifest with a release's version. -const versionFinder = require('./git-version') +const versionFinder = require('./git-version.mjs') const parseArgs = require('./deploy/lib/parseArgs') const USAGE = diff --git a/shared-data/webpack.config.js b/shared-data/webpack.config.js deleted file mode 100644 index 18aa6478319..00000000000 --- a/shared-data/webpack.config.js +++ /dev/null @@ -1,20 +0,0 @@ -'use strict' - -const path = require('path') -const webpackMerge = require('webpack-merge') -const { baseConfig } = require('@opentrons/webpack-config') - -const ENTRY_INDEX = path.join(__dirname, 'js/index.ts') -const OUTPUT_PATH = path.join(__dirname, 'lib') - -module.exports = async () => - webpackMerge(baseConfig, { - entry: { index: ENTRY_INDEX }, - output: { - path: OUTPUT_PATH, - filename: 'opentrons-shared-data.js', - library: '@opentrons/shared-data', - libraryTarget: 'umd', - globalObject: 'this', - }, - }) diff --git a/usb-bridge/node-client/webpack.config.js b/usb-bridge/node-client/webpack.config.js deleted file mode 100644 index c01e57beb07..00000000000 --- a/usb-bridge/node-client/webpack.config.js +++ /dev/null @@ -1,26 +0,0 @@ -'use strict' - -const path = require('path') -const webpackMerge = require('webpack-merge') -const { DefinePlugin } = require('webpack') -const { nodeBaseConfig } = require('@opentrons/webpack-config') -const { versionForProject } = require('../../scripts/git-version') - -const ENTRY_INDEX = path.join(__dirname, 'src/index.ts') -const ENTRY_CLI = path.join(__dirname, 'src/cli.ts') -const OUTPUT_PATH = path.join(__dirname, 'lib') -const project = process.env.OPENTRONS_PROJECT ?? 'robot-stack' - -module.exports = async () => - webpackMerge(nodeBaseConfig, { - entry: { - index: ENTRY_INDEX, - cli: ENTRY_CLI, - }, - output: { path: OUTPUT_PATH }, - plugins: [ - new DefinePlugin({ - _PKG_VERSION_: JSON.stringify(await versionForProject(project)), - }), - ], - }) From 476149e748bccbf2c75fbdc5120554595042fc98 Mon Sep 17 00:00:00 2001 From: Jethary Rader <66035149+jerader@users.noreply.github.com> Date: Tue, 9 Apr 2024 17:12:35 -0400 Subject: [PATCH 085/194] feat(protocol-designer): create container for all tipracks (#14848) closes AUTH-313 --- .../StepEditForm/fields/TiprackField.tsx | 72 +++++++++++++------ .../fields/__tests__/TiprackField.test.tsx | 60 ++++++++++++++++ .../components/StepEditForm/forms/MixForm.tsx | 5 +- .../forms/MoveLiquidForm/index.tsx | 5 +- .../modals/CreateFileWizard/index.tsx | 8 +-- .../src/localization/en/tooltip.json | 1 + protocol-designer/src/ui/labware/selectors.ts | 14 ++-- 7 files changed, 134 insertions(+), 31 deletions(-) create mode 100644 protocol-designer/src/components/StepEditForm/fields/__tests__/TiprackField.test.tsx diff --git a/protocol-designer/src/components/StepEditForm/fields/TiprackField.tsx b/protocol-designer/src/components/StepEditForm/fields/TiprackField.tsx index a9dceb482a2..464b15b4f7f 100644 --- a/protocol-designer/src/components/StepEditForm/fields/TiprackField.tsx +++ b/protocol-designer/src/components/StepEditForm/fields/TiprackField.tsx @@ -1,32 +1,62 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' import { useSelector } from 'react-redux' -import { FormGroup, DropdownField } from '@opentrons/components' +import { + FormGroup, + DropdownField, + useHoverTooltip, + Tooltip, + Box, +} from '@opentrons/components' import { selectors as uiLabwareSelectors } from '../../../ui/labware' -import styles from '../StepEditForm.module.css' - +import { getPipetteEntities } from '../../../step-forms/selectors' import type { FieldProps } from '../types' -export function TiprackField(props: FieldProps): JSX.Element { - const { name, value, onFieldBlur, onFieldFocus, updateValue } = props - const { t } = useTranslation('form') +import styles from '../StepEditForm.module.css' + +interface TiprackFieldProps extends FieldProps { + pipetteId?: unknown +} +export function TiprackField(props: TiprackFieldProps): JSX.Element { + const { + name, + value, + onFieldBlur, + onFieldFocus, + updateValue, + pipetteId, + } = props + const { t } = useTranslation(['form', 'tooltip']) + const [targetProps, tooltipProps] = useHoverTooltip() + const pipetteEntities = useSelector(getPipetteEntities) const options = useSelector(uiLabwareSelectors.getTiprackOptions) + const defaultTipracks = + pipetteId != null ? pipetteEntities[pipetteId as string].tiprackDefURI : [] + const pipetteOptions = options.filter(option => + defaultTipracks.includes(option.defURI) + ) + const hasMissingTiprack = defaultTipracks.length > pipetteOptions.length return ( - - ) => { - updateValue(e.currentTarget.value) - }} - /> - + + + ) => { + updateValue(e.currentTarget.value) + }} + /> + + {hasMissingTiprack ? ( + {t('tooltip:missing_tiprack')} + ) : null} + ) } diff --git a/protocol-designer/src/components/StepEditForm/fields/__tests__/TiprackField.test.tsx b/protocol-designer/src/components/StepEditForm/fields/__tests__/TiprackField.test.tsx new file mode 100644 index 00000000000..979155a4d88 --- /dev/null +++ b/protocol-designer/src/components/StepEditForm/fields/__tests__/TiprackField.test.tsx @@ -0,0 +1,60 @@ +import * as React from 'react' +import { describe, it, vi, beforeEach } from 'vitest' +import { screen } from '@testing-library/react' +import { i18n } from '../../../../localization' +import { getPipetteEntities } from '../../../../step-forms/selectors' +import { renderWithProviders } from '../../../../__testing-utils__' +import { getTiprackOptions } from '../../../../ui/labware/selectors' +import { TiprackField } from '../TiprackField' + +vi.mock('../../../../ui/labware/selectors') +vi.mock('../../../../step-forms/selectors') + +const render = (props: React.ComponentProps) => { + return renderWithProviders(, { + i18nInstance: i18n, + })[0] +} +const mockMockId = 'mockId' +describe('TiprackField', () => { + let props: React.ComponentProps + + beforeEach(() => { + props = { + disabled: false, + value: null, + name: 'tipRackt', + updateValue: vi.fn(), + onFieldBlur: vi.fn(), + onFieldFocus: vi.fn(), + pipetteId: mockMockId, + } + vi.mocked(getPipetteEntities).mockReturnValue({ + [mockMockId]: { + name: 'p50_single_flex', + spec: {} as any, + id: mockMockId, + tiprackLabwareDef: [], + tiprackDefURI: ['mockDefURI1', 'mockDefURI2'], + }, + }) + vi.mocked(getTiprackOptions).mockReturnValue([ + { + value: 'mockValue', + name: 'tiprack1', + defURI: 'mockDefURI1', + }, + { + value: 'mockValue', + name: 'tiprack2', + defURI: 'mockDefURI2', + }, + ]) + }) + it('renders the dropdown field and texts', () => { + render(props) + screen.getByText('tip rack') + screen.getByText('tiprack1') + screen.getByText('tiprack2') + }) +}) diff --git a/protocol-designer/src/components/StepEditForm/forms/MixForm.tsx b/protocol-designer/src/components/StepEditForm/forms/MixForm.tsx index ef1b408cfe4..f0eb043b081 100644 --- a/protocol-designer/src/components/StepEditForm/forms/MixForm.tsx +++ b/protocol-designer/src/components/StepEditForm/forms/MixForm.tsx @@ -52,7 +52,10 @@ export const MixForm = (props: StepFormProps): JSX.Element => {
    - + {is96Channel ? ( ) : null} diff --git a/protocol-designer/src/components/StepEditForm/forms/MoveLiquidForm/index.tsx b/protocol-designer/src/components/StepEditForm/forms/MoveLiquidForm/index.tsx index 67bd45ec663..66b8f1e34c2 100644 --- a/protocol-designer/src/components/StepEditForm/forms/MoveLiquidForm/index.tsx +++ b/protocol-designer/src/components/StepEditForm/forms/MoveLiquidForm/index.tsx @@ -42,7 +42,10 @@ export const MoveLiquidForm = (props: StepFormProps): JSX.Element => {
    - + {is96Channel ? ( ) : null} diff --git a/protocol-designer/src/components/modals/CreateFileWizard/index.tsx b/protocol-designer/src/components/modals/CreateFileWizard/index.tsx index eea2264199a..b19ab426f65 100644 --- a/protocol-designer/src/components/modals/CreateFileWizard/index.tsx +++ b/protocol-designer/src/components/modals/CreateFileWizard/index.tsx @@ -240,15 +240,15 @@ export function CreateFileWizard(): JSX.Element | null { const newTiprackModels: string[] = uniq( pipettes.flatMap(pipette => pipette.tiprackDefURI) ) + const FLEX_MIDDLE_SLOTS = ['C2', 'B2', 'A2'] + const OT2_MIDDLE_SLOTS = ['2', '5', '8', '11'] newTiprackModels.forEach((tiprackDefURI, index) => { - const ot2Slots = index === 0 ? '2' : '5' - const flexSlots = index === 0 ? 'C2' : 'B2' dispatch( labwareIngredActions.createContainer({ slot: values.fields.robotType === FLEX_ROBOT_TYPE - ? flexSlots - : ot2Slots, + ? FLEX_MIDDLE_SLOTS[index] + : OT2_MIDDLE_SLOTS[index], labwareDefURI: tiprackDefURI, adapterUnderLabwareDefURI: values.pipettesByMount.left.pipetteName === 'p1000_96' diff --git a/protocol-designer/src/localization/en/tooltip.json b/protocol-designer/src/localization/en/tooltip.json index 7ef580d81ce..8e293d8efdd 100644 --- a/protocol-designer/src/localization/en/tooltip.json +++ b/protocol-designer/src/localization/en/tooltip.json @@ -8,6 +8,7 @@ "disabled_you_can_add_one_type": "Only one module of each type is allowed on the deck at a time", "not_enough_space_for_temp": "There is not enough space on the deck to add more temperature modules", "not_in_beta": "ⓘ Coming Soon", + "missing_tiprack": "Missing a tiprack? Make sure it is added to the deck", "step_description": { "heaterShaker": "Set heat, shake, or labware latch commands for the Heater-Shaker module", diff --git a/protocol-designer/src/ui/labware/selectors.ts b/protocol-designer/src/ui/labware/selectors.ts index dd4be8f0c62..27b3ea9f3ae 100644 --- a/protocol-designer/src/ui/labware/selectors.ts +++ b/protocol-designer/src/ui/labware/selectors.ts @@ -241,17 +241,22 @@ export const getDisposalOptions = createSelector( } ) -export const getTiprackOptions: Selector = createSelector( +export interface TiprackOption { + name: string + value: string + defURI: string +} +export const getTiprackOptions: Selector = createSelector( stepFormSelectors.getLabwareEntities, getLabwareNicknamesById, (labwareEntities, nicknamesById) => { const options = reduce( labwareEntities, ( - acc: Options, + acc: TiprackOption[], labwareEntity: LabwareEntity, labwareId: string - ): Options => { + ): TiprackOption[] => { const labwareDefURI = labwareEntity.labwareDefURI const optionValues = acc.map(option => option.value) @@ -266,12 +271,13 @@ export const getTiprackOptions: Selector = createSelector( { name: nicknamesById[labwareId], value: labwareId, + defURI: labwareDefURI, }, ] } }, [] ) - return _sortLabwareDropdownOptions(options) + return options } ) From 57a8152a1abaa0fc3ecce4153d6aa90fd963e5a4 Mon Sep 17 00:00:00 2001 From: Jethary Rader <66035149+jerader@users.noreply.github.com> Date: Tue, 9 Apr 2024 17:41:18 -0400 Subject: [PATCH 086/194] =?UTF-8?q?refactor(protocol-designer,=20component?= =?UTF-8?q?s):=20infoItem=20to=20nicely=20accommoda=E2=80=A6=20(#14850)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …te multiple tipracks closes AUTH-314 --- components/src/instrument/InfoItem.tsx | 24 ---- components/src/instrument/InstrumentInfo.tsx | 110 +++++++++++------- .../__tests__/InstrumentInfo.test.tsx | 54 +++++++++ components/src/instrument/index.ts | 1 - .../src/step-forms/selectors/index.ts | 1 - 5 files changed, 122 insertions(+), 68 deletions(-) delete mode 100644 components/src/instrument/InfoItem.tsx create mode 100644 components/src/instrument/__tests__/InstrumentInfo.test.tsx diff --git a/components/src/instrument/InfoItem.tsx b/components/src/instrument/InfoItem.tsx deleted file mode 100644 index 82b5a491a37..00000000000 --- a/components/src/instrument/InfoItem.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import * as React from 'react' - -import styles from './instrument.module.css' - -export interface InfoItemProps { - title: string | null - value: string - className?: string -} - -/** - * Used by `InstrumentInfo` for its titled values. - * But if you're using this, you probably want `LabeledValue` instead. - */ -export function InfoItem(props: InfoItemProps): JSX.Element { - const { title, value, className } = props - - return ( -
    - {title != null ?

    {title}

    : null} - {value} -
    - ) -} diff --git a/components/src/instrument/InstrumentInfo.tsx b/components/src/instrument/InstrumentInfo.tsx index d5d26a3b4b4..57ff12e0ed4 100644 --- a/components/src/instrument/InstrumentInfo.tsx +++ b/components/src/instrument/InstrumentInfo.tsx @@ -1,77 +1,103 @@ import * as React from 'react' import { LEFT, RIGHT } from '@opentrons/shared-data' -import { InfoItem } from './InfoItem' -import { InstrumentDiagram } from './InstrumentDiagram' -import styles from './instrument.module.css' import { Flex } from '../primitives' -import { SPACING } from '../ui-style-constants' +import { SPACING, TYPOGRAPHY } from '../ui-style-constants' +import { StyledText } from '../atoms' import { DIRECTION_COLUMN, JUSTIFY_CENTER } from '../styles' +import { InstrumentDiagram } from './InstrumentDiagram' import type { Mount } from '../robot-types' import type { InstrumentDiagramProps } from './InstrumentDiagram' +import styles from './instrument.module.css' + export interface InstrumentInfoProps { /** 'left' or 'right' */ mount: Mount - /** if true, show labels 'LEFT PIPETTE' / 'RIGHT PIPETTE' */ - showMountLabel?: boolean | null /** human-readable description, eg 'p300 Single-channel' */ description: string - /** paired tiprack models */ - tiprackModels?: string[] - /** if disabled, pipette & its info are grayed out */ - isDisabled: boolean /** specs of mounted pipette */ pipetteSpecs?: InstrumentDiagramProps['pipetteSpecs'] | null - /** classes to apply */ - className?: string - /** classes to apply to the info group child */ - infoClassName?: string + /** paired tiprack models */ + tiprackModels?: string[] /** children to display under the info */ children?: React.ReactNode + /** if true, show labels 'LEFT PIPETTE' / 'RIGHT PIPETTE' */ + showMountLabel?: boolean | null } +const MAX_WIDTH = '14rem' + export function InstrumentInfo(props: InstrumentInfoProps): JSX.Element { - const has96Channel = props.pipetteSpecs?.channels === 96 + const { + mount, + showMountLabel, + description, + tiprackModels, + pipetteSpecs, + children, + } = props + + const has96Channel = pipetteSpecs?.channels === 96 return ( - {props.mount === RIGHT && props.pipetteSpecs && ( + {mount === RIGHT && pipetteSpecs ? ( - )} + ) : null} + {/* NOTE: the color is our legacy c-font-dark, which matches the other colors in this component **/} + + + + {showMountLabel && !has96Channel ? `${mount} pipette` : 'pipette'} + + + {description} + + - - - {props.tiprackModels != null - ? props.tiprackModels.map((model, index) => ( - - )) - : null} + + + {'Tip rack'} + +
      + {tiprackModels != null && tiprackModels.length > 0 ? ( + tiprackModels.map((model, index) => ( +
    • + + {model} + +
    • + )) + ) : ( + + {'None'} + + )} +
    +
    - {props.children} - {props.mount === LEFT && props.pipetteSpecs && ( + {children} + {mount === LEFT && pipetteSpecs ? ( - )} + ) : null}
    ) } diff --git a/components/src/instrument/__tests__/InstrumentInfo.test.tsx b/components/src/instrument/__tests__/InstrumentInfo.test.tsx new file mode 100644 index 00000000000..bf92c48d4cb --- /dev/null +++ b/components/src/instrument/__tests__/InstrumentInfo.test.tsx @@ -0,0 +1,54 @@ +import * as React from 'react' +import { screen } from '@testing-library/react' +import { describe, beforeEach, it, vi } from 'vitest' +import { LEFT, RIGHT, fixtureP1000SingleV2Specs } from '@opentrons/shared-data' +import { renderWithProviders } from '../../testing/utils' +import { InstrumentInfo } from '../InstrumentInfo' +import { InstrumentDiagram } from '../InstrumentDiagram' + +vi.mock('../InstrumentDiagram') +const render = (props: React.ComponentProps) => { + return renderWithProviders()[0] +} + +describe('InstrumentInfo', () => { + let props: React.ComponentProps + + beforeEach(() => { + props = { + mount: LEFT, + description: 'mock description', + pipetteSpecs: fixtureP1000SingleV2Specs, + tiprackModels: ['mock1', 'mock2'], + showMountLabel: true, + } + vi.mocked(InstrumentDiagram).mockReturnValue( +
    mock instrumentDiagram
    + ) + }) + it('renders a p1000 pipette with 2 tiprack models for left mount', () => { + render(props) + screen.getByText('mock instrumentDiagram') + screen.getByText('left pipette') + screen.getByText('mock description') + screen.getByText('Tip rack') + screen.getByText('mock1') + screen.getByText('mock2') + }) + it('renders a p1000 pipette with 1 tiprack model for right mount', () => { + props.mount = RIGHT + props.tiprackModels = ['mock1'] + render(props) + screen.getByText('mock instrumentDiagram') + screen.getByText('right pipette') + screen.getByText('mock description') + screen.getByText('Tip rack') + screen.getByText('mock1') + }) + it('renders none for pip and tiprack if none are selected', () => { + props.pipetteSpecs = undefined + props.tiprackModels = undefined + render(props) + screen.getByText('None') + }) +}) diff --git a/components/src/instrument/index.ts b/components/src/instrument/index.ts index 1153df43ae7..d566fb66e5b 100644 --- a/components/src/instrument/index.ts +++ b/components/src/instrument/index.ts @@ -1,4 +1,3 @@ -export * from './InfoItem' export * from './InstrumentDiagram' export * from './InstrumentGroup' export * from './InstrumentInfo' diff --git a/protocol-designer/src/step-forms/selectors/index.ts b/protocol-designer/src/step-forms/selectors/index.ts index a81846be991..1c0be8ca60c 100644 --- a/protocol-designer/src/step-forms/selectors/index.ts +++ b/protocol-designer/src/step-forms/selectors/index.ts @@ -406,7 +406,6 @@ export const getPipettesForInstrumentGroup: Selector< mount: pipetteOnDeck.mount, pipetteSpecs: pipetteSpec, description: _getPipetteDisplayName(pipetteOnDeck.name), - isDisabled: false, tiprackModels: tiprackDefs?.map((def: LabwareDefinition2) => getLabwareDisplayName(def) ), From f81da99b373b7a95fef8bbe866d85d234eeeef0b Mon Sep 17 00:00:00 2001 From: koji Date: Wed, 10 Apr 2024 08:40:30 -0400 Subject: [PATCH 087/194] fix(shared-data, app): fix small issues in app (#14851) * fix(shared-data, app): fix small issues in app --- .../NumericalKeyboard.stories.tsx | 1 - .../__tests__/HistoricalProtocolRun.test.tsx | 4 +- .../Devices/__tests__/RobotOverview.test.tsx | 4 +- .../ModuleCard/TemperatureModuleData.tsx | 2 - ...formatRunTimeParameterDefaultValue.test.ts | 145 ++++++++++++++++++ .../formatRunTimeParameterValue.test.ts | 16 +- .../formatRunTimeParameterDefaultValue.ts | 10 ++ .../helpers/formatRunTimeParameterMinMax.ts | 21 +++ .../js/helpers/formatRunTimeParameterValue.ts | 10 ++ .../orderRuntimeParameterRangeOptions.ts | 26 ++-- 10 files changed, 211 insertions(+), 28 deletions(-) create mode 100644 shared-data/js/helpers/__tests__/formatRunTimeParameterDefaultValue.test.ts diff --git a/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/NumericalKeyboard.stories.tsx b/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/NumericalKeyboard.stories.tsx index d7659866c6a..53b3d714c4c 100644 --- a/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/NumericalKeyboard.stories.tsx +++ b/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/NumericalKeyboard.stories.tsx @@ -37,7 +37,6 @@ type Story = StoryObj const Keyboard = (args): JSX.Element => { const { isDecimal, hasHyphen } = args - console.log(isDecimal, hasHyphen) const [showKeyboard, setShowKeyboard] = React.useState(false) const [value, setValue] = React.useState('') const keyboardRef = React.useRef(null) diff --git a/app/src/organisms/Devices/__tests__/HistoricalProtocolRun.test.tsx b/app/src/organisms/Devices/__tests__/HistoricalProtocolRun.test.tsx index bc59f8cf884..dccbb3dfefc 100644 --- a/app/src/organisms/Devices/__tests__/HistoricalProtocolRun.test.tsx +++ b/app/src/organisms/Devices/__tests__/HistoricalProtocolRun.test.tsx @@ -18,8 +18,8 @@ vi.mock('../../../redux/protocol-storage') vi.mock('../../RunTimeControl/hooks') vi.mock('../HistoricalProtocolRunOverflowMenu') vi.mock('react-router-dom', async importOriginal => { - const reactRouterDom = importOriginal() - return await { + const reactRouterDom = await importOriginal() + return { ...reactRouterDom, useHistory: () => ({ push: mockPush } as any), } diff --git a/app/src/organisms/Devices/__tests__/RobotOverview.test.tsx b/app/src/organisms/Devices/__tests__/RobotOverview.test.tsx index b02e5ce600a..66f6d18b7d0 100644 --- a/app/src/organisms/Devices/__tests__/RobotOverview.test.tsx +++ b/app/src/organisms/Devices/__tests__/RobotOverview.test.tsx @@ -51,8 +51,8 @@ import type { State } from '../../../redux/types' import type * as ReactApiClient from '@opentrons/react-api-client' vi.mock('@opentrons/react-api-client', async importOriginal => { - const actual = importOriginal() - return await { + const actual = await importOriginal() + return { ...actual, useAuthorization: vi.fn(), } diff --git a/app/src/organisms/ModuleCard/TemperatureModuleData.tsx b/app/src/organisms/ModuleCard/TemperatureModuleData.tsx index b6b70c3ae48..c595e4513b4 100644 --- a/app/src/organisms/ModuleCard/TemperatureModuleData.tsx +++ b/app/src/organisms/ModuleCard/TemperatureModuleData.tsx @@ -29,8 +29,6 @@ export const TemperatureModuleData = ( let pulse switch (moduleStatus) { case 'idle': { - backgroundColor = COLORS.grey30 - iconColor = COLORS.grey60 textColor = COLORS.grey60 break } diff --git a/shared-data/js/helpers/__tests__/formatRunTimeParameterDefaultValue.test.ts b/shared-data/js/helpers/__tests__/formatRunTimeParameterDefaultValue.test.ts new file mode 100644 index 00000000000..d83239e3ec9 --- /dev/null +++ b/shared-data/js/helpers/__tests__/formatRunTimeParameterDefaultValue.test.ts @@ -0,0 +1,145 @@ +import { describe, it, expect, vi } from 'vitest' +import { formatRunTimeParameterDefaultValue } from '../formatRunTimeParameterDefaultValue' + +import type { RunTimeParameter } from '../../types' + +const capitalizeFirstLetter = (str: string): string => { + return str.charAt(0).toUpperCase() + str.slice(1) +} + +const mockTFunction = vi.fn(str => capitalizeFirstLetter(str)) + +describe('formatRunTimeParameterDefaultValue', () => { + it('should return value with suffix when type is int', () => { + const mockData = { + value: 6, + displayName: 'PCR Cycles', + variableName: 'PCR_CYCLES', + description: 'number of PCR cycles on a thermocycler', + type: 'int', + min: 1, + max: 10, + default: 6, + suffix: 'samples', + } as RunTimeParameter + const result = formatRunTimeParameterDefaultValue(mockData, mockTFunction) + expect(result).toEqual('6 samples') + }) + + it('should return value with suffix when type is float', () => { + const mockData = { + value: 6.5, + displayName: 'EtoH Volume', + variableName: 'ETOH_VOLUME', + description: '70% ethanol volume', + type: 'float', + suffix: 'mL', + min: 1.5, + max: 10.0, + default: 6.5, + } as RunTimeParameter + const result = formatRunTimeParameterDefaultValue(mockData, mockTFunction) + expect(result).toEqual('6.5 mL') + }) + + it('should return value when type is str', () => { + const mockData = { + value: 'left', + displayName: 'pipette mount', + variableName: 'mount', + description: 'pipette mount', + type: 'str', + choices: [ + { + displayName: 'Left', + value: 'left', + }, + { + displayName: 'Right', + value: 'right', + }, + ], + default: 'left', + } as RunTimeParameter + const result = formatRunTimeParameterDefaultValue(mockData, mockTFunction) + expect(result).toEqual('Left') + }) + + it('should return value when type is int choice with suffix', () => { + const mockData = { + value: 5, + displayName: 'num', + variableName: 'number', + description: 'its just number', + type: 'int', + suffix: 'mL', + min: 1, + max: 10, + choices: [ + { + displayName: 'one', + value: 1, + }, + { + displayName: 'six', + value: 6, + }, + ], + default: 5, + } as RunTimeParameter + const result = formatRunTimeParameterDefaultValue(mockData, mockTFunction) + expect(result).toEqual('5 mL') + }) + + it('should return value when type is float choice with suffix', () => { + const mockData = { + value: 5.0, + displayName: 'num', + variableName: 'number', + description: 'its just number', + type: 'float', + suffix: 'mL', + min: 1.0, + max: 10.0, + choices: [ + { + displayName: 'one', + value: 1.0, + }, + { + displayName: 'six', + value: 6.0, + }, + ], + default: 5.0, + } as RunTimeParameter + const result = formatRunTimeParameterDefaultValue(mockData, mockTFunction) + expect(result).toEqual('5 mL') + }) + + it('should return value when type is boolean true', () => { + const mockData = { + value: true, + displayName: 'Deactivate Temperatures', + variableName: 'DEACTIVATE_TEMP', + description: 'deactivate temperature on the module', + type: 'bool', + default: true, + } as RunTimeParameter + const result = formatRunTimeParameterDefaultValue(mockData, mockTFunction) + expect(result).toEqual('On') + }) + + it('should return value when type is boolean false', () => { + const mockData = { + value: false, + displayName: 'Dry Run', + variableName: 'DRYRUN', + description: 'Is this a dry or wet run? Wet is true, dry is false', + type: 'bool', + default: false, + } as RunTimeParameter + const result = formatRunTimeParameterDefaultValue(mockData, mockTFunction) + expect(result).toEqual('Off') + }) +}) diff --git a/shared-data/js/helpers/__tests__/formatRunTimeParameterValue.test.ts b/shared-data/js/helpers/__tests__/formatRunTimeParameterValue.test.ts index 2f78d99e11c..8e228cb6dbc 100644 --- a/shared-data/js/helpers/__tests__/formatRunTimeParameterValue.test.ts +++ b/shared-data/js/helpers/__tests__/formatRunTimeParameterValue.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect, vi } from 'vitest' -import { formatRunTimeParameterDefaultValue } from '../formatRunTimeParameterDefaultValue' +import { formatRunTimeParameterValue } from '../formatRunTimeParameterValue' import type { RunTimeParameter } from '../../types' @@ -21,7 +21,7 @@ describe('utils-formatRunTimeParameterDefaultValue', () => { max: 10, default: 6, } as RunTimeParameter - const result = formatRunTimeParameterDefaultValue(mockData, mockTFunction) + const result = formatRunTimeParameterValue(mockData, mockTFunction) expect(result).toEqual('6') }) @@ -37,7 +37,7 @@ describe('utils-formatRunTimeParameterDefaultValue', () => { max: 10.0, default: 6.5, } as RunTimeParameter - const result = formatRunTimeParameterDefaultValue(mockData, mockTFunction) + const result = formatRunTimeParameterValue(mockData, mockTFunction) expect(result).toEqual('6.5 mL') }) @@ -60,7 +60,7 @@ describe('utils-formatRunTimeParameterDefaultValue', () => { ], default: 'left', } as RunTimeParameter - const result = formatRunTimeParameterDefaultValue(mockData, mockTFunction) + const result = formatRunTimeParameterValue(mockData, mockTFunction) expect(result).toEqual('Left') }) @@ -86,7 +86,7 @@ describe('utils-formatRunTimeParameterDefaultValue', () => { ], default: 5, } as RunTimeParameter - const result = formatRunTimeParameterDefaultValue(mockData, mockTFunction) + const result = formatRunTimeParameterValue(mockData, mockTFunction) expect(result).toEqual('5 mL') }) @@ -112,7 +112,7 @@ describe('utils-formatRunTimeParameterDefaultValue', () => { ], default: 5.0, } as RunTimeParameter - const result = formatRunTimeParameterDefaultValue(mockData, mockTFunction) + const result = formatRunTimeParameterValue(mockData, mockTFunction) expect(result).toEqual('5 mL') }) @@ -125,7 +125,7 @@ describe('utils-formatRunTimeParameterDefaultValue', () => { type: 'bool', default: true, } as RunTimeParameter - const result = formatRunTimeParameterDefaultValue(mockData, mockTFunction) + const result = formatRunTimeParameterValue(mockData, mockTFunction) expect(result).toEqual('On') }) @@ -138,7 +138,7 @@ describe('utils-formatRunTimeParameterDefaultValue', () => { type: 'bool', default: false, } as RunTimeParameter - const result = formatRunTimeParameterDefaultValue(mockData, mockTFunction) + const result = formatRunTimeParameterValue(mockData, mockTFunction) expect(result).toEqual('Off') }) }) diff --git a/shared-data/js/helpers/formatRunTimeParameterDefaultValue.ts b/shared-data/js/helpers/formatRunTimeParameterDefaultValue.ts index aa7d16a256f..3ac5cda5bfa 100644 --- a/shared-data/js/helpers/formatRunTimeParameterDefaultValue.ts +++ b/shared-data/js/helpers/formatRunTimeParameterDefaultValue.ts @@ -1,5 +1,15 @@ import type { RunTimeParameter } from '../types' +/** + * Formats the runtime parameter's default value. + * + * @param {RunTimeParameter} runTimeParameter - The runtime parameter whose default value is to be formatted. + * @param {Function} [t] - An optional function for localization. + * + * @returns {string} The formatted default value of the runtime parameter. + * + */ + export const formatRunTimeParameterDefaultValue = ( runTimeParameter: RunTimeParameter, t?: any diff --git a/shared-data/js/helpers/formatRunTimeParameterMinMax.ts b/shared-data/js/helpers/formatRunTimeParameterMinMax.ts index 36444f89601..632dec5c020 100644 --- a/shared-data/js/helpers/formatRunTimeParameterMinMax.ts +++ b/shared-data/js/helpers/formatRunTimeParameterMinMax.ts @@ -1,4 +1,25 @@ import type { RunTimeParameter } from '../types' +/** + * Formats the runtime parameter's minimum and maximum values. + * + * @param {RunTimeParameter} runTimeParameter - The runtime parameter whose min and max values are to be formatted. + * + * @returns {string} The formatted min-max value of the runtime parameter. + * + * @example + * const runTimeParameter = { + * value: 6.5, + * displayName: 'EtoH Volume', + * variableName: 'ETOH_VOLUME', + * description: '70% ethanol volume', + * type: 'float', + * suffix: 'mL', + * min: 1.5, + * max: 10.0, + * default: 6.5, + * } + * console.log(formatRunTimeParameterMinMax(runTimeParameter)); // "1.5-10.0" + */ export const formatRunTimeParameterMinMax = ( runTimeParameter: RunTimeParameter diff --git a/shared-data/js/helpers/formatRunTimeParameterValue.ts b/shared-data/js/helpers/formatRunTimeParameterValue.ts index a75bee5fd68..a6a3ad4d7ec 100644 --- a/shared-data/js/helpers/formatRunTimeParameterValue.ts +++ b/shared-data/js/helpers/formatRunTimeParameterValue.ts @@ -1,5 +1,15 @@ import type { RunTimeParameter } from '../types' +/** + * Formats the runtime parameter value. + * + * @param {RunTimeParameter} runTimeParameter - The runtime parameter to be formatted. + * @param {Function} t - A function for localization. + * + * @returns {string} The formatted runtime parameter value. + * + */ + export const formatRunTimeParameterValue = ( runTimeParameter: RunTimeParameter, t: any diff --git a/shared-data/js/helpers/orderRuntimeParameterRangeOptions.ts b/shared-data/js/helpers/orderRuntimeParameterRangeOptions.ts index c372e992a2b..826fc958dd1 100644 --- a/shared-data/js/helpers/orderRuntimeParameterRangeOptions.ts +++ b/shared-data/js/helpers/orderRuntimeParameterRangeOptions.ts @@ -9,19 +9,19 @@ export const isNumeric = (str: string): boolean => { * @param {Choice[]} - The array of Choice * Choice is an object like {displayName: 'Single channel 50µL', value: 'flex_1channel_50' } * @returns {string} The ordered string with "," - * - * examples - * [ - { displayName: '20', value: 20 }, - { displayName: '16', value: 16 }, - ] - return 16, 20 - - [ - { displayName: 'Single channel 50µL', value: 'flex_1channel_50' }, - { displayName: 'Eight Channel 50µL', value: 'flex_8channel_50' }, - ] - return Eight Channel 50µL, Single channel 50µL + * + * @example + * const numChoices = [ + * { displayName: '20', value: 20 }, + * { displayName: '16', value: 16 }, + * ] + * console.log(orderRuntimeParameterRangeOptions(numChoices) // 16,20 + * + * const strChoices = [ + * { displayName: 'Single channel 50µL', value: 'flex_1channel_50' }, + * { displayName: 'Eight Channel 50µL', value: 'flex_8channel_50' }, + * ] + * console.log(orderRuntimeParameterRangeOptions(strChoices) // Eight Channel 50µL, Single channel 50µL */ export const orderRuntimeParameterRangeOptions = ( choices: Choice[] From 754417586779af5cfaab5107f5e8c40a3f4a8492 Mon Sep 17 00:00:00 2001 From: koji Date: Wed, 10 Apr 2024 12:35:08 -0400 Subject: [PATCH 088/194] fix(discovery-client): fix import statement (#14856) * fix(discovery-client): fix import statement --- discovery-client/vite.config.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/discovery-client/vite.config.ts b/discovery-client/vite.config.ts index 203012d904a..c67977a8359 100644 --- a/discovery-client/vite.config.ts +++ b/discovery-client/vite.config.ts @@ -8,8 +8,8 @@ import postCssApply from 'postcss-apply' import postColorModFunction from 'postcss-color-mod-function' import postCssPresetEnv from 'postcss-preset-env' import lostCss from 'lost' -import type { UserConfig } from 'vite -' +import type { UserConfig } from 'vite' + export default defineConfig( async (): Promise => { const project = process.env.OPENTRONS_PROJECT ?? 'robot-stack' From e4233194900fc00a0441f52b248525294aa757e7 Mon Sep 17 00:00:00 2001 From: koji Date: Wed, 10 Apr 2024 12:43:04 -0400 Subject: [PATCH 089/194] feat(opentrons-ai-client, opentrons-ai-server): add folders for opentrons-ai (#14788) * feat(opentrons-ai-client, opentrons-ai-server): add folders for opentrons-ai --- opentrons-ai-client/Makefile | 59 +++++++++++++++++ opentrons-ai-client/README.md | 64 +++++++++++++++++++ opentrons-ai-client/babel.config.cjs | 21 ++++++ opentrons-ai-client/index.html | 13 ++++ opentrons-ai-client/package.json | 38 +++++++++++ opentrons-ai-client/src/App.test.tsx | 18 ++++++ opentrons-ai-client/src/App.tsx | 9 +++ .../src/__testing-utils__/index.ts | 2 + .../src/__testing-utils__/matchers.ts | 24 +++++++ .../__testing-utils__/renderWithProviders.tsx | 53 +++++++++++++++ .../src/assets/localization/en/index.ts | 7 ++ .../localization/en/protocol_generator.json | 23 +++++++ .../src/assets/localization/en/shared.json | 3 + .../src/assets/localization/index.ts | 5 ++ opentrons-ai-client/src/i18n.ts | 45 +++++++++++++ opentrons-ai-client/src/main.tsx | 14 ++++ opentrons-ai-client/tsconfig-data.json | 12 ++++ opentrons-ai-client/tsconfig.json | 16 +++++ opentrons-ai-client/typings/images.d.ts | 15 +++++ .../typings/styled-components.d.ts | 1 + opentrons-ai-client/vite.config.ts | 43 +++++++++++++ opentrons-ai-server/Makefile | 2 + opentrons-ai-server/README.md | 39 +++++++++++ tsconfig-eslint.json | 1 + 24 files changed, 527 insertions(+) create mode 100644 opentrons-ai-client/Makefile create mode 100644 opentrons-ai-client/README.md create mode 100644 opentrons-ai-client/babel.config.cjs create mode 100644 opentrons-ai-client/index.html create mode 100644 opentrons-ai-client/package.json create mode 100644 opentrons-ai-client/src/App.test.tsx create mode 100644 opentrons-ai-client/src/App.tsx create mode 100644 opentrons-ai-client/src/__testing-utils__/index.ts create mode 100644 opentrons-ai-client/src/__testing-utils__/matchers.ts create mode 100644 opentrons-ai-client/src/__testing-utils__/renderWithProviders.tsx create mode 100644 opentrons-ai-client/src/assets/localization/en/index.ts create mode 100644 opentrons-ai-client/src/assets/localization/en/protocol_generator.json create mode 100644 opentrons-ai-client/src/assets/localization/en/shared.json create mode 100644 opentrons-ai-client/src/assets/localization/index.ts create mode 100644 opentrons-ai-client/src/i18n.ts create mode 100644 opentrons-ai-client/src/main.tsx create mode 100644 opentrons-ai-client/tsconfig-data.json create mode 100644 opentrons-ai-client/tsconfig.json create mode 100644 opentrons-ai-client/typings/images.d.ts create mode 100644 opentrons-ai-client/typings/styled-components.d.ts create mode 100644 opentrons-ai-client/vite.config.ts create mode 100644 opentrons-ai-server/Makefile create mode 100644 opentrons-ai-server/README.md diff --git a/opentrons-ai-client/Makefile b/opentrons-ai-client/Makefile new file mode 100644 index 00000000000..9c15fa32e41 --- /dev/null +++ b/opentrons-ai-client/Makefile @@ -0,0 +1,59 @@ +# opentrons ai client makefile + +# using bash instead of /bin/bash in SHELL prevents macOS optimizing away our PATH update +SHELL := bash + +# add node_modules/.bin to PATH +PATH := $(shell cd .. && yarn bin):$(PATH) + +benchmark_output := $(shell node -e 'console.log(new Date());') + +# These variables can be overriden when make is invoked to customize the +# behavior of jest +tests ?= +cov_opts ?= --coverage=true +test_opts ?= + +# standard targets +##################################################################### + +.PHONY: all +all: clean build + +.PHONY: setup +setup: + yarn + +.PHONY: clean +clean: + shx rm -rf dist + +# artifacts +##################################################################### + +.PHONY: build +build: export NODE_ENV := production +build: + vite build + git rev-parse HEAD > dist/.commit + +# development +##################################################################### + +.PHONY: dev +dev: export NODE_ENV := development +dev: + vite serve + +# production assets server +.PHONY: serve +serve: all + node ../scripts/serve-static dist + +.PHONY: test +test: + $(MAKE) -C .. test-js-ai-client tests="$(tests)" test_opts="$(test_opts)" + +.PHONY: test-cov +test-cov: + make -C .. test-js-ai-client tests=$(tests) test_opts="$(test_opts)" cov_opts="$(cov_opts)" diff --git a/opentrons-ai-client/README.md b/opentrons-ai-client/README.md new file mode 100644 index 00000000000..c2ff2908418 --- /dev/null +++ b/opentrons-ai-client/README.md @@ -0,0 +1,64 @@ +# Opentrons AI Frontend + +[![JavaScript Style Guide][style-guide-badge]][style-guide] + +[Download][] | [Support][] + +## Overview + +The Opentrons AI application helps you to create a protocol with natural language. + +## Developing + +To get started: clone the `Opentrons/opentrons` repository, set up your computer for development as specified in the [contributing guide][contributing-guide-setup], and then: + +```shell +# change into the cloned directory +cd opentrons +# prerequisite: install dependencies as specified in project setup +make setup +# launch the dev server +make -C opentrons-ai-client dev +``` + +## Stack and structure + +The UI stack is built using: + +- [React][] +- [Babel][] +- [Vite][] + +Some important directories: + +- `opentrons-ai-server` — Opentrons AI application's server + +## Copy management + +We use [i18next](https://www.i18next.com) for copy management and internationalization. + +## Testing + +Tests for the Opentrons App are run from the top level along with all other JS project tests. + +- `make test-js` - Run all JavaScript tests + +Test tasks can also be run with the following arguments: + +| Argument | Default | Description | Example | +| -------- | -------- | ----------------------- | --------------------------------- | +| watch | `false` | Run tests in watch mode | `make test-unit watch=true` | +| cover | `!watch` | Calculate code coverage | `make test watch=true cover=true` | + +## Building + +TBD + +[style-guide]: https://standardjs.com +[style-guide-badge]: https://img.shields.io/badge/code_style-standard-brightgreen.svg?style=flat-square&maxAge=3600 +[contributing-guide-setup]: ../CONTRIBUTING.md#development-setup +[contributing-guide-running-the-api]: ../CONTRIBUTING.md#opentrons-api +[react]: https://react.dev/ +[babel]: https://babeljs.io/ +[vite]: https://vitejs.dev/ +[bundle-analyzer]: https://github.com/webpack-contrib/webpack-bundle-analyzer diff --git a/opentrons-ai-client/babel.config.cjs b/opentrons-ai-client/babel.config.cjs new file mode 100644 index 00000000000..11739e6bf00 --- /dev/null +++ b/opentrons-ai-client/babel.config.cjs @@ -0,0 +1,21 @@ +'use strict' + +module.exports = { + env: { + // Must have babel-plugin-styled-components in each env, + // see here for further details: s https://styled-components.com/docs/tooling#babel-plugin + production: { + plugins: ['babel-plugin-styled-components', 'babel-plugin-unassert'], + }, + development: { + plugins: ['babel-plugin-styled-components'], + }, + test: { + plugins: [ + // disable ssr, displayName to fix toHaveStyleRule + // https://github.com/styled-components/jest-styled-components/issues/294 + ['babel-plugin-styled-components', { ssr: false, displayName: false }], + ], + }, + }, +} diff --git a/opentrons-ai-client/index.html b/opentrons-ai-client/index.html new file mode 100644 index 00000000000..57e7f83f591 --- /dev/null +++ b/opentrons-ai-client/index.html @@ -0,0 +1,13 @@ + + + + + + + Opentrons AI + + +
    + + + diff --git a/opentrons-ai-client/package.json b/opentrons-ai-client/package.json new file mode 100644 index 00000000000..e3c056e8bfe --- /dev/null +++ b/opentrons-ai-client/package.json @@ -0,0 +1,38 @@ +{ + "name": "opentrons-ai-client", + "type": "module", + "version": "0.0.0-dev", + "description": "Opentrons AI application UI", + "source": "src/index.tsx", + "types": "lib/index.d.ts", + "repository": { + "type": "git", + "url": "https://github.com/Opentrons/opentrons.git" + }, + "author": { + "name": "Opentrons Labworks", + "email": "engineering@opentrons.com" + }, + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/Opentrons/opentrons/issues" + }, + "homepage": "https://github.com/Opentrons/opentrons", + "dependencies": { + "@fontsource/dejavu-sans": "5.0.3", + "@fontsource/public-sans": "5.0.3", + "@opentrons/components": "link:../components", + "i18next": "^19.8.3", + "react": "18.2.0", + "react-dom": "18.2.0", + "react-error-boundary": "^4.0.10", + "react-i18next": "13.5.0", + "styled-components": "5.3.6" + }, + "engines": { + "node": ">=18.19.0" + }, + "devDependencies": { + "@types/styled-components": "^5.1.26" + } +} diff --git a/opentrons-ai-client/src/App.test.tsx b/opentrons-ai-client/src/App.test.tsx new file mode 100644 index 00000000000..03b731311c0 --- /dev/null +++ b/opentrons-ai-client/src/App.test.tsx @@ -0,0 +1,18 @@ +import React from 'react' +import { screen } from '@testing-library/react' +import { describe, it } from 'vitest' + +import { renderWithProviders } from './__testing-utils__' + +import { App } from './App' + +const render = (): ReturnType => { + return renderWithProviders() +} + +describe('App', () => { + it('should render text', () => { + render() + screen.getByText('Opentrons AI') + }) +}) diff --git a/opentrons-ai-client/src/App.tsx b/opentrons-ai-client/src/App.tsx new file mode 100644 index 00000000000..f31fbd35940 --- /dev/null +++ b/opentrons-ai-client/src/App.tsx @@ -0,0 +1,9 @@ +import React from 'react' +import { Flex, StyledText } from '@opentrons/components' +export function App(): JSX.Element { + return ( + + Opentrons AI + + ) +} diff --git a/opentrons-ai-client/src/__testing-utils__/index.ts b/opentrons-ai-client/src/__testing-utils__/index.ts new file mode 100644 index 00000000000..e17c0ffbc31 --- /dev/null +++ b/opentrons-ai-client/src/__testing-utils__/index.ts @@ -0,0 +1,2 @@ +export * from './renderWithProviders' +export * from './matchers' diff --git a/opentrons-ai-client/src/__testing-utils__/matchers.ts b/opentrons-ai-client/src/__testing-utils__/matchers.ts new file mode 100644 index 00000000000..66234dbc915 --- /dev/null +++ b/opentrons-ai-client/src/__testing-utils__/matchers.ts @@ -0,0 +1,24 @@ +import type { Matcher } from '@testing-library/react' + +// Match things like

    Some nested text

    +// Use with either string match: getByText(nestedTextMatcher("Some nested text")) +// or regexp: getByText(nestedTextMatcher(/Some nested text/)) +export const nestedTextMatcher = (textMatch: string | RegExp): Matcher => ( + content, + node +) => { + const hasText = (n: typeof node): boolean => { + if (n == null || n.textContent === null) return false + return typeof textMatch === 'string' + ? Boolean(n?.textContent.match(textMatch)) + : textMatch.test(n.textContent) + } + const nodeHasText = hasText(node) + const childrenDontHaveText = + node != null && Array.from(node.children).every(child => !hasText(child)) + + return nodeHasText && childrenDontHaveText +} + +// need componentPropsMatcher +// need partialComponentPropsMatcher diff --git a/opentrons-ai-client/src/__testing-utils__/renderWithProviders.tsx b/opentrons-ai-client/src/__testing-utils__/renderWithProviders.tsx new file mode 100644 index 00000000000..65a2e01855e --- /dev/null +++ b/opentrons-ai-client/src/__testing-utils__/renderWithProviders.tsx @@ -0,0 +1,53 @@ +// render using targetted component using @testing-library/react +// with wrapping providers for i18next and redux +import * as React from 'react' +import { QueryClient, QueryClientProvider } from 'react-query' +import { I18nextProvider } from 'react-i18next' +import { Provider } from 'react-redux' +import { vi } from 'vitest' +import { render } from '@testing-library/react' +import { createStore } from 'redux' + +import type { PreloadedState, Store } from 'redux' +import type { RenderOptions, RenderResult } from '@testing-library/react' + +export interface RenderWithProvidersOptions extends RenderOptions { + initialState?: State + i18nInstance: React.ComponentProps['i18n'] +} + +export function renderWithProviders( + Component: React.ReactElement, + options?: RenderWithProvidersOptions +): [RenderResult, Store] { + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + const { initialState = {}, i18nInstance = null } = options || {} + + const store: Store = createStore( + vi.fn(), + initialState as PreloadedState + ) + store.dispatch = vi.fn() + store.getState = vi.fn(() => initialState) as () => State + + const queryClient = new QueryClient() + + const ProviderWrapper: React.ComponentType> = ({ + children, + }) => { + const BaseWrapper = ( + + {children} + + ) + if (i18nInstance != null) { + return ( + {BaseWrapper} + ) + } else { + return BaseWrapper + } + } + + return [render(Component, { wrapper: ProviderWrapper }), store] +} diff --git a/opentrons-ai-client/src/assets/localization/en/index.ts b/opentrons-ai-client/src/assets/localization/en/index.ts new file mode 100644 index 00000000000..b5aa26621dd --- /dev/null +++ b/opentrons-ai-client/src/assets/localization/en/index.ts @@ -0,0 +1,7 @@ +import shared from './shared.json' +import protocol_generator from './protocol_generator.json' + +export const en = { + shared, + protocol_generator, +} diff --git a/opentrons-ai-client/src/assets/localization/en/protocol_generator.json b/opentrons-ai-client/src/assets/localization/en/protocol_generator.json new file mode 100644 index 00000000000..c8ac35504bb --- /dev/null +++ b/opentrons-ai-client/src/assets/localization/en/protocol_generator.json @@ -0,0 +1,23 @@ +{ + "api": "API: An API level is 2.15", + "application": "Application: Your protocol's name, describing what it does.", + "commands": "Commands: List the protocol's steps, specifying quantities in microliters and giving exact source and destination locations.", + "make_sure_your_prompt": "Make sure your prompt includes the following:", + "metadata": "Metadata: Three pieces of information.", + "modules": "Modules: Thermocycler or Temperature Module.", + "opentronsai_asks_you": "OpentronsAI asks you to provide it!", + "ot2_pipettes": "OT-2 pipettes: Include volume, number of channels, and generation.", + "prc_flex": "PRC (Flex)", + "prc": "PCR", + "reagent_transfer_flex": "Reagent Transfer (Flex)", + "reagent_transfer": "Reagent Transfer", + "robot": "Robot: OT-2.", + "sidebar_body": "Write a prompt in natural language to generate a Reagent Transfer or a PCR protocol for the OT-2 or Opentrons Flex using the Opentrons Python Protocol API.", + "sidebar_header": "Use natural language to generate protocols with OpentronsAI powered by OpenAI", + "stuck": "Stuck? Try these example prompts to get started.", + "tipracks_and_labware": "Tip racks and labware: Use names from the Opentrons Labware Library.", + "type_your_prompt": "Type your prompt...", + "well_allocations": "Well allocations: Describe where liquids should go in labware.", + "what_if_you": "What if you don’t provide all of those pieces of information?", + "what_typeof_protocol": "What type of protocol do you need?" +} diff --git a/opentrons-ai-client/src/assets/localization/en/shared.json b/opentrons-ai-client/src/assets/localization/en/shared.json new file mode 100644 index 00000000000..46cb365873f --- /dev/null +++ b/opentrons-ai-client/src/assets/localization/en/shared.json @@ -0,0 +1,3 @@ +{ + "send": "Send" +} diff --git a/opentrons-ai-client/src/assets/localization/index.ts b/opentrons-ai-client/src/assets/localization/index.ts new file mode 100644 index 00000000000..e92a7077ed9 --- /dev/null +++ b/opentrons-ai-client/src/assets/localization/index.ts @@ -0,0 +1,5 @@ +import { en } from './en' + +export const resources = { + en, +} diff --git a/opentrons-ai-client/src/i18n.ts b/opentrons-ai-client/src/i18n.ts new file mode 100644 index 00000000000..0f7ef3bf6df --- /dev/null +++ b/opentrons-ai-client/src/i18n.ts @@ -0,0 +1,45 @@ +import i18n from 'i18next' +import capitalize from 'lodash/capitalize' +import startCase from 'lodash/startCase' +import { initReactI18next } from 'react-i18next' +import { resources } from './assets/localization' +import { titleCase } from '@opentrons/shared-data' + +i18n.use(initReactI18next).init( + { + resources, + lng: 'en', + fallbackLng: 'en', + debug: process.env.NODE_ENV === 'development', + ns: ['shared'], + defaultNS: 'shared', + interpolation: { + escapeValue: false, // not needed for react as it escapes by default + format: function (value, format, lng) { + if (format === 'upperCase') return value.toUpperCase() + if (format === 'lowerCase') return value.toLowerCase() + if (format === 'capitalize') return capitalize(value) + if (format === 'sentenceCase') return startCase(value) + if (format === 'titleCase') return titleCase(value) + return value + }, + }, + keySeparator: false, // use namespaces and context instead + saveMissing: true, + missingKeyHandler: (lng, ns, key) => { + process.env.NODE_ENV === 'test' + ? console.error(`Missing ${lng} Translation: key={${key}} ns={${ns}}`) + : console.warn(`Missing ${lng} Translation: key={${key}} ns={${ns}}`) + }, + }, + err => { + if (err) { + console.error( + 'Internationalization was not initialized properly. error: ', + err + ) + } + } +) + +export { i18n } diff --git a/opentrons-ai-client/src/main.tsx b/opentrons-ai-client/src/main.tsx new file mode 100644 index 00000000000..466bd35e081 --- /dev/null +++ b/opentrons-ai-client/src/main.tsx @@ -0,0 +1,14 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import { App } from './App' + +const rootElement = document.getElementById('root') +if (rootElement) { + ReactDOM.createRoot(rootElement).render( + + + + ) +} else { + console.error('Root element not found') +} diff --git a/opentrons-ai-client/tsconfig-data.json b/opentrons-ai-client/tsconfig-data.json new file mode 100644 index 00000000000..79a9673faa9 --- /dev/null +++ b/opentrons-ai-client/tsconfig-data.json @@ -0,0 +1,12 @@ +{ + "extends": "../tsconfig-base.json", + "references": [], + "compilerOptions": { + "composite": true, + "emitDeclarationOnly": false, + "rootDir": ".", + "outDir": "lib" + }, + "include": ["src/**/*.json", "fixtures/**/*.json", "vite.config.ts"], + "exclude": ["**/*.ts", "**/*.tsx"] +} diff --git a/opentrons-ai-client/tsconfig.json b/opentrons-ai-client/tsconfig.json new file mode 100644 index 00000000000..b3c6dc275a8 --- /dev/null +++ b/opentrons-ai-client/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../tsconfig-base.json", + "references": [ + { + "path": "./tsconfig-data.json" + }, + { + "path": "../components" + } + ], + "compilerOptions": { + "rootDir": "src", + "outDir": "lib" + }, + "include": ["typings", "src"] +} diff --git a/opentrons-ai-client/typings/images.d.ts b/opentrons-ai-client/typings/images.d.ts new file mode 100644 index 00000000000..9dcd2f68792 --- /dev/null +++ b/opentrons-ai-client/typings/images.d.ts @@ -0,0 +1,15 @@ +declare module '*.png' { + const image: string + // eslint-disable-next-line import/no-default-export + export default image +} +declare module '*.svg' { + const image: string + // eslint-disable-next-line import/no-default-export + export default image +} +declare module '*.webm' { + const image: string + // eslint-disable-next-line import/no-default-export + export default image +} diff --git a/opentrons-ai-client/typings/styled-components.d.ts b/opentrons-ai-client/typings/styled-components.d.ts new file mode 100644 index 00000000000..5d6296f94be --- /dev/null +++ b/opentrons-ai-client/typings/styled-components.d.ts @@ -0,0 +1 @@ +import 'styled-components/cssprop' diff --git a/opentrons-ai-client/vite.config.ts b/opentrons-ai-client/vite.config.ts new file mode 100644 index 00000000000..ee557f68d62 --- /dev/null +++ b/opentrons-ai-client/vite.config.ts @@ -0,0 +1,43 @@ +import path from 'path' +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +export default defineConfig({ + // this makes imports relative rather than absolute + base: '', + build: { + // Relative to the root + outDir: 'dist', + }, + plugins: [ + react({ + include: '**/*.tsx', + babel: { + // Use babel.config.js files + configFile: true, + }, + }), + ], + optimizeDeps: { + esbuildOptions: { + target: 'es2020', + }, + }, + css: { + postcss: { + plugins: [], + }, + }, + define: { + 'process.env': process.env, + global: 'globalThis', + }, + resolve: { + alias: { + '@opentrons/components/styles': path.resolve( + '../components/src/index.module.css' + ), + '@opentrons/components': path.resolve('../components/src/index.ts'), + }, + }, +}) diff --git a/opentrons-ai-server/Makefile b/opentrons-ai-server/Makefile new file mode 100644 index 00000000000..9de2141f6a0 --- /dev/null +++ b/opentrons-ai-server/Makefile @@ -0,0 +1,2 @@ +# opentrons ai server makefile +# TBD \ No newline at end of file diff --git a/opentrons-ai-server/README.md b/opentrons-ai-server/README.md new file mode 100644 index 00000000000..e00cdc1af3d --- /dev/null +++ b/opentrons-ai-server/README.md @@ -0,0 +1,39 @@ +# Opentrons AI Backend + +## Overview + +The Opentrons AI application's server. + +## Developing + +To get started: clone the `Opentrons/opentrons` repository, set up your computer for development as specified in the [contributing guide][contributing-guide-setup], and then: + +```shell +# change into the cloned directory +cd opentrons +# prerequisite: install dependencies as specified in project setup +make setup +# launch the dev server +make -C opentrons-ai-server dev +``` + +## Stack and structure + +The UI stack is built using: + +- [OpenAI Python API library][] + +Some important directories: + +- `opentrons-ai-client` — Opentrons AI application's client-side + +## Testing + +TBD + +## Building + +TBD + +[pytest]: https://docs.pytest.org/en/ +[openai python api library]: https://pypi.org/project/openai/ diff --git a/tsconfig-eslint.json b/tsconfig-eslint.json index 4468d4f6fd4..541feb786c0 100644 --- a/tsconfig-eslint.json +++ b/tsconfig-eslint.json @@ -19,6 +19,7 @@ "labware-designer/typings", "labware-library/src", "labware-library/typings", + "opentrons-ai-client/src", "shared-data/deck", "shared-data/js", "shared-data/protocol", From 8f50b081ed804153aa001ef85e27a8cc9dbc1449 Mon Sep 17 00:00:00 2001 From: Caila Marashaj <98041399+caila-marashaj@users.noreply.github.com> Date: Wed, 10 Apr 2024 12:53:44 -0400 Subject: [PATCH 090/194] fix(api): ensure the right mount is enabled for initial homing (#14822) --- api/src/opentrons/hardware_control/ot3api.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/api/src/opentrons/hardware_control/ot3api.py b/api/src/opentrons/hardware_control/ot3api.py index e6ae891359b..24b613411c1 100644 --- a/api/src/opentrons/hardware_control/ot3api.py +++ b/api/src/opentrons/hardware_control/ot3api.py @@ -1521,8 +1521,14 @@ async def _home_axis(self, axis: Axis) -> None: # G, Q should be handled in the backend through `self._home()` assert axis not in [Axis.G, Axis.Q] + # TODO(CM): This is a temporary fix in response to the right mount causing + # errors while trying to home on startup or attachment. We should remove this + # when we fix this issue in the firmware. + enable_right_mount_on_startup = ( + self._gantry_load == GantryLoad.HIGH_THROUGHPUT and axis == Axis.Z_R + ) encoder_ok = self._backend.check_encoder_status([axis]) - if encoder_ok: + if encoder_ok or enable_right_mount_on_startup: # enable motor (if needed) and update estimation await self._enable_before_update_estimation(axis) From a4bc70004fd25145a2b6b382665b4c40b51e9ecc Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 10 Apr 2024 14:07:21 -0500 Subject: [PATCH 091/194] fix(app-testing): snapshot failure capture (#14852) This PR is an automated snapshot update request. Please review the changes and merge if they are acceptable or find you bug and fix it. Co-authored-by: y3rsh --- ...sis_snapshot[0f71566d05][OT2_P20S_None_2_7_Walkthrough].json | 2 +- ...t[20cefcac62][OT2_P300M_P20S_TC_HS_TM_2_13_SmokeTestV3].json | 2 +- ...ysis_snapshot[8df082e960][OT2_P300MLeft_MM_TM_2_4_Zymo].json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0f71566d05][OT2_P20S_None_2_7_Walkthrough].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0f71566d05][OT2_P20S_None_2_7_Walkthrough].json index e52cb9863b1..d1786c8ca62 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0f71566d05][OT2_P20S_None_2_7_Walkthrough].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0f71566d05][OT2_P20S_None_2_7_Walkthrough].json @@ -3293,7 +3293,7 @@ "errorInfo": { "args": "()", "class": "AssertionError", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/protocol_engine.py\", line 442, in finish\n await exit_stack.aclose()\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 656, in aclose\n await self.__aexit__(None, None, None)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 714, in __aexit__\n raise exc_details[1]\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 697, in __aexit__\n cb_suppress = await cb(*exc_details)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 608, in _exit_wrapper\n await callback(*args, **kwds)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 102, in stop\n await p.teardown()\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 112, in teardown\n await self._action_dispatching_task\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 160, in _dispatch_all_actions\n self.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 37, in dispatch\n return self._action_dispatcher.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/actions/action_dispatcher.py\", line 30, in dispatch\n self._sink.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/state.py\", line 207, in handle_action\n substore.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/commands.py\", line 257, in handle_action\n self._state.command_history.set_command_running(running_command)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/command_history.py\", line 175, in set_command_running\n assert self.get_running_command() is None\n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/protocol_engine.py\", line 503, in finish\n await exit_stack.aclose()\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 656, in aclose\n await self.__aexit__(None, None, None)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 714, in __aexit__\n raise exc_details[1]\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 697, in __aexit__\n cb_suppress = await cb(*exc_details)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 608, in _exit_wrapper\n await callback(*args, **kwds)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 102, in stop\n await p.teardown()\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 112, in teardown\n await self._action_dispatching_task\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 160, in _dispatch_all_actions\n self.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 37, in dispatch\n return self._action_dispatcher.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/actions/action_dispatcher.py\", line 30, in dispatch\n self._sink.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/state.py\", line 209, in handle_action\n substore.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/commands.py\", line 261, in handle_action\n self._state.command_history.set_command_running(running_command)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/command_history.py\", line 175, in set_command_running\n assert self.get_running_command() is None\n" }, "errorType": "PythonException", "wrappedErrors": [] diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[20cefcac62][OT2_P300M_P20S_TC_HS_TM_2_13_SmokeTestV3].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[20cefcac62][OT2_P300M_P20S_TC_HS_TM_2_13_SmokeTestV3].json index 2f1e2018f18..d1aaa472fe9 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[20cefcac62][OT2_P300M_P20S_TC_HS_TM_2_13_SmokeTestV3].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[20cefcac62][OT2_P300M_P20S_TC_HS_TM_2_13_SmokeTestV3].json @@ -11889,7 +11889,7 @@ "errorInfo": { "args": "()", "class": "AssertionError", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/protocol_engine.py\", line 442, in finish\n await exit_stack.aclose()\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 656, in aclose\n await self.__aexit__(None, None, None)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 714, in __aexit__\n raise exc_details[1]\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 697, in __aexit__\n cb_suppress = await cb(*exc_details)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 608, in _exit_wrapper\n await callback(*args, **kwds)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 102, in stop\n await p.teardown()\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 112, in teardown\n await self._action_dispatching_task\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 160, in _dispatch_all_actions\n self.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 37, in dispatch\n return self._action_dispatcher.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/actions/action_dispatcher.py\", line 30, in dispatch\n self._sink.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/state.py\", line 207, in handle_action\n substore.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/commands.py\", line 257, in handle_action\n self._state.command_history.set_command_running(running_command)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/command_history.py\", line 175, in set_command_running\n assert self.get_running_command() is None\n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/protocol_engine.py\", line 503, in finish\n await exit_stack.aclose()\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 656, in aclose\n await self.__aexit__(None, None, None)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 714, in __aexit__\n raise exc_details[1]\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 697, in __aexit__\n cb_suppress = await cb(*exc_details)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 608, in _exit_wrapper\n await callback(*args, **kwds)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 102, in stop\n await p.teardown()\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 112, in teardown\n await self._action_dispatching_task\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 160, in _dispatch_all_actions\n self.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 37, in dispatch\n return self._action_dispatcher.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/actions/action_dispatcher.py\", line 30, in dispatch\n self._sink.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/state.py\", line 209, in handle_action\n substore.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/commands.py\", line 261, in handle_action\n self._state.command_history.set_command_running(running_command)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/command_history.py\", line 175, in set_command_running\n assert self.get_running_command() is None\n" }, "errorType": "PythonException", "wrappedErrors": [] diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8df082e960][OT2_P300MLeft_MM_TM_2_4_Zymo].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8df082e960][OT2_P300MLeft_MM_TM_2_4_Zymo].json index a2af41a1a02..0ccb1065979 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8df082e960][OT2_P300MLeft_MM_TM_2_4_Zymo].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8df082e960][OT2_P300MLeft_MM_TM_2_4_Zymo].json @@ -10913,7 +10913,7 @@ "errorInfo": { "args": "()", "class": "AssertionError", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/protocol_engine.py\", line 442, in finish\n await exit_stack.aclose()\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 656, in aclose\n await self.__aexit__(None, None, None)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 714, in __aexit__\n raise exc_details[1]\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 697, in __aexit__\n cb_suppress = await cb(*exc_details)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 608, in _exit_wrapper\n await callback(*args, **kwds)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 102, in stop\n await p.teardown()\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 112, in teardown\n await self._action_dispatching_task\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 160, in _dispatch_all_actions\n self.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 37, in dispatch\n return self._action_dispatcher.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/actions/action_dispatcher.py\", line 30, in dispatch\n self._sink.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/state.py\", line 207, in handle_action\n substore.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/commands.py\", line 257, in handle_action\n self._state.command_history.set_command_running(running_command)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/command_history.py\", line 175, in set_command_running\n assert self.get_running_command() is None\n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/protocol_engine.py\", line 503, in finish\n await exit_stack.aclose()\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 656, in aclose\n await self.__aexit__(None, None, None)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 714, in __aexit__\n raise exc_details[1]\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 697, in __aexit__\n cb_suppress = await cb(*exc_details)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 608, in _exit_wrapper\n await callback(*args, **kwds)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 102, in stop\n await p.teardown()\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 112, in teardown\n await self._action_dispatching_task\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 160, in _dispatch_all_actions\n self.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 37, in dispatch\n return self._action_dispatcher.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/actions/action_dispatcher.py\", line 30, in dispatch\n self._sink.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/state.py\", line 209, in handle_action\n substore.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/commands.py\", line 261, in handle_action\n self._state.command_history.set_command_running(running_command)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/command_history.py\", line 175, in set_command_running\n assert self.get_running_command() is None\n" }, "errorType": "PythonException", "wrappedErrors": [] From a2c5a0222289f298536395fe7678a926c42d779c Mon Sep 17 00:00:00 2001 From: koji Date: Thu, 11 Apr 2024 09:37:33 -0400 Subject: [PATCH 092/194] fix(app): fix rtp slideout issue (#14855) * fix(app): fix rtp slideout issue --- app/src/organisms/ChooseProtocolSlideout/index.tsx | 6 ++---- .../__tests__/ChooseRobotSlideout.test.tsx | 14 +++++++------- app/src/organisms/ChooseRobotSlideout/index.tsx | 2 -- 3 files changed, 9 insertions(+), 13 deletions(-) diff --git a/app/src/organisms/ChooseProtocolSlideout/index.tsx b/app/src/organisms/ChooseProtocolSlideout/index.tsx index fd9085e07cb..d743ef17468 100644 --- a/app/src/organisms/ChooseProtocolSlideout/index.tsx +++ b/app/src/organisms/ChooseProtocolSlideout/index.tsx @@ -222,7 +222,6 @@ export function ChooseProtocolSlideoutComponent( setRunTimeParametersOverrides(clone) }} title={runtimeParam.displayName} - caption={runtimeParam.description} width="100%" dropdownType="neutral" /> @@ -253,7 +252,6 @@ export function ChooseProtocolSlideoutComponent( key={runtimeParam.variableName} type="number" units={runtimeParam.suffix} - placeholder={value.toString()} value={value} title={runtimeParam.displayName} tooltipText={runtimeParam.description} @@ -313,14 +311,14 @@ export function ChooseProtocolSlideoutComponent( }} height="0.813rem" label={ - runtimeParam.value + Boolean(runtimeParam.value) ? t('protocol_details:on') : t('protocol_details:off') } paddingTop={SPACING.spacing2} // manual alignment of SVG with value label /> - {runtimeParam.value + {Boolean(runtimeParam.value) ? t('protocol_details:on') : t('protocol_details:off')} diff --git a/app/src/organisms/ChooseRobotSlideout/__tests__/ChooseRobotSlideout.test.tsx b/app/src/organisms/ChooseRobotSlideout/__tests__/ChooseRobotSlideout.test.tsx index 18bdf233f75..6c97f4e62c3 100644 --- a/app/src/organisms/ChooseRobotSlideout/__tests__/ChooseRobotSlideout.test.tsx +++ b/app/src/organisms/ChooseRobotSlideout/__tests__/ChooseRobotSlideout.test.tsx @@ -226,14 +226,14 @@ describe('ChooseRobotSlideout', () => { }) screen.getByText(param.displayName) - if (param.type === 'bool' || 'choices' in param) { + if (param.type === 'bool') { screen.getByText(param.description) - } else { - if (param.type === 'int') { - screen.getByText(`${param.min}-${param.max}`) - } else { - screen.getByText(`${param.min.toFixed(1)}-${param.max.toFixed(1)}`) - } + } + if (param.type === 'int') { + screen.getByText(`${param.min}-${param.max}`) + } + if (param.type === 'float') { + screen.getByText(`${param.min.toFixed(1)}-${param.max.toFixed(1)}`) } }) }) diff --git a/app/src/organisms/ChooseRobotSlideout/index.tsx b/app/src/organisms/ChooseRobotSlideout/index.tsx index d19a62a514d..82a7a795363 100644 --- a/app/src/organisms/ChooseRobotSlideout/index.tsx +++ b/app/src/organisms/ChooseRobotSlideout/index.tsx @@ -363,7 +363,6 @@ export function ChooseRobotSlideout( } }} title={runtimeParam.displayName} - caption={runtimeParam.description} width="100%" dropdownType="neutral" /> @@ -394,7 +393,6 @@ export function ChooseRobotSlideout( key={runtimeParam.variableName} type="number" units={runtimeParam.suffix} - placeholder={value.toString()} value={value} title={runtimeParam.displayName} tooltipText={runtimeParam.description} From 4c83fc149a0e0510d558002f0999c0cd999b4002 Mon Sep 17 00:00:00 2001 From: koji Date: Thu, 11 Apr 2024 10:08:04 -0400 Subject: [PATCH 093/194] fix(app-shell-odd): fix typo in vite-config (#14864) * fix(app-shell-odd): fix typo in vite-config --- app-shell-odd/vite.config.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app-shell-odd/vite.config.ts b/app-shell-odd/vite.config.ts index a3b8351fee6..7848c92bd8d 100644 --- a/app-shell-odd/vite.config.ts +++ b/app-shell-odd/vite.config.ts @@ -8,7 +8,7 @@ import postCssApply from 'postcss-apply' import postColorModFunction from 'postcss-color-mod-function' import postCssPresetEnv from 'postcss-preset-env' import lostCss from 'lost' -import type {UserConfig} from 'vite' +import type { UserConfig } from 'vite' export default defineConfig( async (): Promise => { @@ -80,7 +80,7 @@ export default defineConfig( '../discovery-client/src/index.ts' ), '@opentrons/usb-bridge/node-client': path.resolve( - '../usb-bridge/node-client/src/inxex.ts' + '../usb-bridge/node-client/src/index.ts' ), }, }, From 8bb14f4edb795e5d126311c4429e3046c74c8f80 Mon Sep 17 00:00:00 2001 From: Nick Diehl <47604184+ncdiehl11@users.noreply.github.com> Date: Thu, 11 Apr 2024 10:10:08 -0400 Subject: [PATCH 094/194] feat(app): add input screen for ODD numerical runtime parameters (#14858) closes AUTH-121, AUTH-122, AUTH-224, AUTH-320 --- .../localization/en/protocol_setup.json | 5 +- app/src/atoms/InputField/index.tsx | 61 ++--- .../ChooseProtocolSlideout/index.tsx | 1 + .../organisms/ChooseRobotSlideout/index.tsx | 1 + app/src/organisms/Devices/utils.ts | 14 ++ .../ProtocolSetupParameters/ChooseEnum.tsx | 7 +- .../ProtocolSetupParameters/ChooseNumber.tsx | 164 +++++++++++++ .../ViewOnlyParameters.tsx | 16 +- .../__tests__/ChooseEnum.test.tsx | 4 +- .../__tests__/ViewOnlyParameters.test.tsx | 6 +- .../ProtocolSetupParameters/index.tsx | 221 +++++------------- app/src/organisms/RunTimeControl/hooks.ts | 3 +- app/src/pages/ProtocolDetails/fixtures.ts | 2 +- app/src/pages/ProtocolSetup/index.tsx | 1 + shared-data/js/types.ts | 12 +- 15 files changed, 299 insertions(+), 219 deletions(-) create mode 100644 app/src/organisms/ProtocolSetupParameters/ChooseNumber.tsx diff --git a/app/src/assets/localization/en/protocol_setup.json b/app/src/assets/localization/en/protocol_setup.json index 99b496a3479..fe3f490b1eb 100644 --- a/app/src/assets/localization/en/protocol_setup.json +++ b/app/src/assets/localization/en/protocol_setup.json @@ -227,7 +227,8 @@ "resolve": "Resolve", "restart_setup_and_try": "Restart setup and try using different parameter values.", "restart_setup": "Restart setup", - "restore_default": "Restore default values", + "restore_defaults": "Restore default values", + "restore_default": "Restore default value", "robot_cal_description": "Robot calibration establishes how the robot knows where it is in relation to the deck. Accurate Robot calibration is essential to run protocols successfully. Robot calibration has 3 parts: Deck calibration, Tip Length calibration and Pipette Offset calibration.", "robot_cal_help_title": "How Robot Calibration Works", "robot_calibration_step_description_pipettes_only": "Review required instruments and calibrations for this protocol.", @@ -265,6 +266,8 @@ "usb_port_connected": "USB Port {{port}}", "value": "Value", "values_are_view_only": "Values are view-only", + "value_out_of_range_generic": "Value must be in range", + "value_out_of_range": "Value must be between {{min}}-{{max}}", "view_current_offsets": "View current offsets", "view_moam": "View setup instructions for placing modules of the same type to the robot.", "view_setup_instructions": "View setup instructions", diff --git a/app/src/atoms/InputField/index.tsx b/app/src/atoms/InputField/index.tsx index c1ff5fbeddd..9be59bf1903 100644 --- a/app/src/atoms/InputField/index.tsx +++ b/app/src/atoms/InputField/index.tsx @@ -101,15 +101,16 @@ function Input(props: InputFieldProps): JSX.Element { tooltipText, ...inputProps } = props - const error = props.error != null + const hasError = props.error != null const value = props.isIndeterminate ?? false ? '' : props.value ?? '' const placeHolder = props.isIndeterminate ?? false ? '-' : props.placeholder const [targetProps, tooltipProps] = useHoverTooltip() const OUTER_CSS = css` @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { + grid-gap: ${SPACING.spacing8}; &:focus-within { - filter: ${error + filter: ${hasError ? 'none' : `drop-shadow(0px 0px 10px ${COLORS.blue50})`}; } @@ -121,7 +122,7 @@ function Input(props: InputFieldProps): JSX.Element { background-color: ${COLORS.white}; border-radius: ${BORDERS.borderRadius4}; padding: ${SPACING.spacing8}; - border: 1px ${BORDERS.styleSolid} ${error ? COLORS.red50 : COLORS.grey50}; + border: 1px ${BORDERS.styleSolid} ${hasError ? COLORS.red50 : COLORS.grey50}; font-size: ${TYPOGRAPHY.fontSizeP}; width: 100%; height: 2rem; @@ -144,17 +145,20 @@ function Input(props: InputFieldProps): JSX.Element { } &:hover { - border: 1px ${BORDERS.styleSolid} ${error ? COLORS.red50 : COLORS.grey60}; + border: 1px ${BORDERS.styleSolid} + ${hasError ? COLORS.red50 : COLORS.grey60}; } &:focus-visible { - border: 1px ${BORDERS.styleSolid} ${error ? COLORS.red50 : COLORS.grey60}; + border: 1px ${BORDERS.styleSolid} + ${hasError ? COLORS.red50 : COLORS.grey60}; outline: 2px ${BORDERS.styleSolid} ${COLORS.blue50}; outline-offset: 3px; } &:focus-within { - border: 1px ${BORDERS.styleSolid} ${error ? COLORS.red50 : COLORS.blue50}; + border: 1px ${BORDERS.styleSolid} + ${hasError ? COLORS.red50 : COLORS.blue50}; } &:disabled { @@ -168,15 +172,16 @@ function Input(props: InputFieldProps): JSX.Element { @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { height: ${size === 'small' ? '4.25rem' : '5rem'}; - box-shadow: ${error ? BORDERS.shadowBig : 'none'}; + box-shadow: ${hasError ? BORDERS.shadowBig : 'none'}; font-size: ${TYPOGRAPHY.fontSize28}; padding: ${SPACING.spacing16} ${SPACING.spacing24}; - border: 2px ${BORDERS.styleSolid} ${error ? COLORS.red50 : COLORS.grey50}; + border: 2px ${BORDERS.styleSolid} + ${hasError ? COLORS.red50 : COLORS.grey50}; &:focus-within { box-shadow: none; - border: ${error ? '2px' : '3px'} ${BORDERS.styleSolid} - ${error ? COLORS.red50 : COLORS.blue50}; + border: ${hasError ? '2px' : '3px'} ${BORDERS.styleSolid} + ${hasError ? COLORS.red50 : COLORS.blue50}; } & input { @@ -191,19 +196,17 @@ function Input(props: InputFieldProps): JSX.Element { ` const FORM_BOTTOM_SPACE_STYLE = css` - padding: ${SPACING.spacing4} 0rem; + padding-top: ${SPACING.spacing4}; @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { + padding: ${SPACING.spacing8} 0rem; padding-bottom: 0; } ` const TITLE_STYLE = css` - color: ${error ? COLORS.red50 : COLORS.black90}; + color: ${hasError ? COLORS.red50 : COLORS.black90}; padding-bottom: ${SPACING.spacing8}; - font-size: ${TYPOGRAPHY.fontSizeLabel}; - font-weight: ${TYPOGRAPHY.fontWeightSemiBold}; - line-height: ${TYPOGRAPHY.lineHeight12}; - align-text: ${textAlign}; + text-align: ${textAlign}; @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { font-size: ${TYPOGRAPHY.fontSize22}; font-weight: ${TYPOGRAPHY.fontWeightRegular}; @@ -214,9 +217,11 @@ function Input(props: InputFieldProps): JSX.Element { const ERROR_TEXT_STYLE = css` color: ${COLORS.red50}; + padding-top: ${SPACING.spacing4}; @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { font-size: ${TYPOGRAPHY.fontSize22}; color: ${COLORS.red50}; + padding-top: ${SPACING.spacing8}; } ` @@ -239,9 +244,14 @@ function Input(props: InputFieldProps): JSX.Element { {title != null ? ( - + {title} - + {tooltipText != null ? ( <> @@ -277,16 +287,6 @@ function Input(props: InputFieldProps): JSX.Element { {props.units} ) : null} - {props.error != null ? ( - - {props.error} - - ) : null} {props.caption != null ? ( ) : null} + {hasError ? ( + + {props.error} + + ) : null} ) } diff --git a/app/src/organisms/ChooseProtocolSlideout/index.tsx b/app/src/organisms/ChooseProtocolSlideout/index.tsx index d743ef17468..c1b2c2eea72 100644 --- a/app/src/organisms/ChooseProtocolSlideout/index.tsx +++ b/app/src/organisms/ChooseProtocolSlideout/index.tsx @@ -252,6 +252,7 @@ export function ChooseProtocolSlideoutComponent( key={runtimeParam.variableName} type="number" units={runtimeParam.suffix} + placeholder={runtimeParam.default.toString()} value={value} title={runtimeParam.displayName} tooltipText={runtimeParam.description} diff --git a/app/src/organisms/ChooseRobotSlideout/index.tsx b/app/src/organisms/ChooseRobotSlideout/index.tsx index 82a7a795363..c8f5a674257 100644 --- a/app/src/organisms/ChooseRobotSlideout/index.tsx +++ b/app/src/organisms/ChooseRobotSlideout/index.tsx @@ -393,6 +393,7 @@ export function ChooseRobotSlideout( key={runtimeParam.variableName} type="number" units={runtimeParam.suffix} + placeholder={runtimeParam.default.toString()} value={value} title={runtimeParam.displayName} tooltipText={runtimeParam.description} diff --git a/app/src/organisms/Devices/utils.ts b/app/src/organisms/Devices/utils.ts index a4d72e0d279..61c133f176b 100644 --- a/app/src/organisms/Devices/utils.ts +++ b/app/src/organisms/Devices/utils.ts @@ -9,7 +9,9 @@ import type { Instruments, PipetteData, PipetteOffsetCalibration, + RunTimeParameterCreateData, } from '@opentrons/api-client' +import type { RunTimeParameter } from '@opentrons/shared-data' /** * formats a string if it is in ISO 8601 date format @@ -89,3 +91,15 @@ export function getShowPipetteCalibrationWarning( }) ?? false ) } + +export function getRunTimeParameterValuesForRun( + runTimeParameters: RunTimeParameter[] +): RunTimeParameterCreateData { + return runTimeParameters.reduce( + (acc, param) => + param.value !== param.default + ? { ...acc, [param.variableName]: param.value } + : acc, + {} + ) +} diff --git a/app/src/organisms/ProtocolSetupParameters/ChooseEnum.tsx b/app/src/organisms/ProtocolSetupParameters/ChooseEnum.tsx index 60e1d7a1b03..1e49e0d8eb0 100644 --- a/app/src/organisms/ProtocolSetupParameters/ChooseEnum.tsx +++ b/app/src/organisms/ProtocolSetupParameters/ChooseEnum.tsx @@ -29,12 +29,7 @@ export function ChooseEnum({ const { makeSnackbar } = useToaster() const { t } = useTranslation(['protocol_setup', 'shared']) - if (parameter.type !== 'str') { - console.error( - `parameter type is expected to be a string for parameter ${parameter.displayName}` - ) - } - const options = parameter.type === 'str' ? parameter.choices : undefined + const options = 'choices' in parameter ? parameter.choices : null const handleOnClick = (newValue: string | number | boolean): void => { setParameter(newValue, parameter.variableName) } diff --git a/app/src/organisms/ProtocolSetupParameters/ChooseNumber.tsx b/app/src/organisms/ProtocolSetupParameters/ChooseNumber.tsx new file mode 100644 index 00000000000..da3c34a14c1 --- /dev/null +++ b/app/src/organisms/ProtocolSetupParameters/ChooseNumber.tsx @@ -0,0 +1,164 @@ +import * as React from 'react' +import { useTranslation } from 'react-i18next' +import { + ALIGN_CENTER, + DIRECTION_COLUMN, + Flex, + SPACING, + StyledText, + TYPOGRAPHY, +} from '@opentrons/components' +import { InputField } from '../../atoms/InputField' +import { useToaster } from '../ToasterOven' +import { ChildNavigation } from '../ChildNavigation' +import { NumericalKeyboard } from '../../atoms/SoftwareKeyboard' +import type { NumberParameter } from '@opentrons/shared-data' + +interface ChooseNumberProps { + handleGoBack: () => void + parameter: NumberParameter + setParameter: (value: number, variableName: string) => void +} + +export function ChooseNumber({ + handleGoBack, + parameter, + setParameter, +}: ChooseNumberProps): JSX.Element | null { + const { makeSnackbar } = useToaster() + + const { i18n, t } = useTranslation(['protocol_setup', 'shared']) + const keyboardRef = React.useRef(null) + const [paramValue, setParamValue] = React.useState( + String(parameter.value) + ) + + // We need to arbitrarily set the value of the keyboard to a string the + // same length as the initial parameter value (as string) when the component mounts + // so that the delete button operates properly on the exisiting input field value. + const [prevKeyboardValue, setPrevKeyboardValue] = React.useState('') + React.useEffect(() => { + const arbitraryInput = new Array(paramValue).join('*') + // @ts-expect-error keyboard should expose for `setInput` method + keyboardRef.current?.setInput(arbitraryInput) + setPrevKeyboardValue(arbitraryInput) + }, []) + + if (parameter.type !== 'int' && parameter.type !== 'float') { + console.log(`Incorrect parameter type: ${parameter.type}`) + return null + } + const handleClickGoBack = (newValue: number): void => { + if (error != null) { + makeSnackbar(t('value_out_of_range_generic')) + } else { + setParameter(newValue, parameter.variableName) + handleGoBack() + } + } + + const handleKeyboardInput = (e: string): void => { + if (prevKeyboardValue.length < e.length) { + const lastDigit = e.slice(-1) + if ( + !'.-'.includes(lastDigit) || + (lastDigit === '.' && !paramValue.includes('.')) || + (lastDigit === '-' && paramValue.length === 0) + ) { + setParamValue(paramValue + lastDigit) + } + } else { + setParamValue(paramValue.slice(0, paramValue.length - 1)) + } + setPrevKeyboardValue(e) + } + + const paramValueAsNumber = Number(paramValue) + const resetValueDisabled = parameter.default === paramValueAsNumber + const { min, max } = parameter + const error = + paramValue === '' || + Number.isNaN(paramValueAsNumber) || + paramValueAsNumber < min || + paramValueAsNumber > max + ? t(`value_out_of_range`, { + min: parameter.type === 'int' ? min : min.toFixed(1), + max: parameter.type === 'int' ? max : max.toFixed(1), + }) + : null + + return ( + <> + { + handleClickGoBack(paramValueAsNumber) + }} + buttonType="tertiaryLowLight" + buttonText={t('restore_default')} + onClickButton={() => + resetValueDisabled + ? makeSnackbar(t('no_custom_values')) + : setParamValue(String(parameter.default)) + } + /> + + + + {parameter.description} + + { + const updatedValue = + parameter.type === 'int' + ? Math.round(e.target.valueAsNumber) + : e.target.valueAsNumber + setParamValue( + Number.isNaN(updatedValue) ? '' : String(updatedValue) + ) + }} + /> + + + { + handleKeyboardInput(e) + }} + /> + + + + ) +} diff --git a/app/src/organisms/ProtocolSetupParameters/ViewOnlyParameters.tsx b/app/src/organisms/ProtocolSetupParameters/ViewOnlyParameters.tsx index 09dcaf26c47..3ce9169f77f 100644 --- a/app/src/organisms/ProtocolSetupParameters/ViewOnlyParameters.tsx +++ b/app/src/organisms/ProtocolSetupParameters/ViewOnlyParameters.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' -import { formatRunTimeParameterDefaultValue } from '@opentrons/shared-data' +import { formatRunTimeParameterValue } from '@opentrons/shared-data' import { ALIGN_CENTER, BORDERS, @@ -16,7 +16,6 @@ import { import { useMostRecentCompletedAnalysis } from '../LabwarePositionCheck/useMostRecentCompletedAnalysis' import { ChildNavigation } from '../ChildNavigation' import { useToaster } from '../ToasterOven' -import { mockData } from './index' import type { SetupScreens } from '../../pages/ProtocolSetup' @@ -36,8 +35,7 @@ export function ViewOnlyParameters({ makeSnackbar(t('reset_setup')) } - // TODO(jr, 3/18/24): remove mockData - const parameters = mostRecentAnalysis?.runTimeParameters ?? mockData + const parameters = mostRecentAnalysis?.runTimeParameters ?? [] return ( <> @@ -68,9 +66,6 @@ export function ViewOnlyParameters({ {t('value')}
    {parameters.map((parameter, index) => { - // TODO(jr, 3/20/24): plug in the info if the - // parameter changed from the default - const hasCustomValue = true return ( - - {formatRunTimeParameterDefaultValue(parameter, t)} + + {formatRunTimeParameterValue(parameter, t)} - {hasCustomValue ? ( + {parameter.value !== parameter.default ? ( { }) it('calls the prop if reset default is clicked when the default has changed', () => { render(props) - fireEvent.click(screen.getByText('Restore default values')) + fireEvent.click(screen.getByText('Restore default value')) expect(props.setParameter).toHaveBeenCalled() }) it('calls does not call prop if reset default is clicked when the default has not changed', () => { @@ -61,7 +61,7 @@ describe('ChooseEnum', () => { rawValue: 'none', } render(props) - fireEvent.click(screen.getByText('Restore default values')) + fireEvent.click(screen.getByText('Restore default value')) expect(props.setParameter).not.toHaveBeenCalled() }) it('should render the text and buttons for choice param', () => { diff --git a/app/src/organisms/ProtocolSetupParameters/__tests__/ViewOnlyParameters.test.tsx b/app/src/organisms/ProtocolSetupParameters/__tests__/ViewOnlyParameters.test.tsx index 90893117b6f..6e20fe65658 100644 --- a/app/src/organisms/ProtocolSetupParameters/__tests__/ViewOnlyParameters.test.tsx +++ b/app/src/organisms/ProtocolSetupParameters/__tests__/ViewOnlyParameters.test.tsx @@ -60,6 +60,8 @@ describe('ViewOnlyParameters', () => { fireEvent.click(screen.getAllByRole('button')[0]) expect(props.setSetupScreen).toHaveBeenCalled() }) - // TODO(jr, 3/20/24):test the update chip when - // custom value boolean is wired up + it('renders chip for updated values', () => { + render(props) + screen.getByTestId('Chip_USE_GRIPPER') + }) }) diff --git a/app/src/organisms/ProtocolSetupParameters/index.tsx b/app/src/organisms/ProtocolSetupParameters/index.tsx index 1312844b2ab..ac1f3fd700f 100644 --- a/app/src/organisms/ProtocolSetupParameters/index.tsx +++ b/app/src/organisms/ProtocolSetupParameters/index.tsx @@ -12,159 +12,18 @@ import { import { formatRunTimeParameterValue } from '@opentrons/shared-data' import { ProtocolSetupStep } from '../../pages/ProtocolSetup' +import { getRunTimeParameterValuesForRun } from '../Devices/utils' import { ChildNavigation } from '../ChildNavigation' import { ResetValuesModal } from './ResetValuesModal' import { ChooseEnum } from './ChooseEnum' +import { ChooseNumber } from './ChooseNumber' -import type { RunTimeParameter } from '@opentrons/shared-data' +import type { NumberParameter, RunTimeParameter } from '@opentrons/shared-data' import type { LabwareOffsetCreateData } from '@opentrons/api-client' -export const mockData: RunTimeParameter[] = [ - { - value: false, - displayName: 'Dry Run', - variableName: 'DRYRUN', - description: 'Is this a dry or wet run? Wet is true, dry is false', - type: 'bool', - default: false, - }, - { - value: true, - displayName: 'Use Gripper', - variableName: 'USE_GRIPPER', - description: 'For using the gripper.', - type: 'bool', - default: true, - }, - { - value: true, - displayName: 'Trash Tips', - variableName: 'TIP_TRASH', - description: - 'to throw tip into the trash or to not throw tip into the trash', - type: 'bool', - default: true, - }, - { - value: true, - displayName: 'Deactivate Temperatures', - variableName: 'DEACTIVATE_TEMP', - description: 'deactivate temperature on the module', - type: 'bool', - default: true, - }, - { - value: 4, - displayName: 'Columns of Samples', - variableName: 'COLUMNS', - description: 'How many columns do you want?', - type: 'int', - min: 1, - max: 14, - default: 4, - }, - { - value: 6, - displayName: 'PCR Cycles', - variableName: 'PCR_CYCLES', - description: 'number of PCR cycles on a thermocycler', - type: 'int', - min: 1, - max: 10, - default: 6, - }, - { - value: 6.5, - displayName: 'EtoH Volume', - variableName: 'ETOH_VOLUME', - description: '70% ethanol volume', - type: 'float', - suffix: 'mL', - min: 1.5, - max: 10.0, - default: 6.5, - }, - { - value: 'none', - displayName: 'Default Module Offsets', - variableName: 'DEFAULT_OFFSETS', - description: 'default module offsets for temp, H-S, and none', - type: 'str', - choices: [ - { - displayName: 'No offsets', - value: 'none', - }, - { - displayName: 'temp offset', - value: '1', - }, - { - displayName: 'heater-shaker offset', - value: '2', - }, - ], - default: 'none', - }, - { - value: 'left', - displayName: 'pipette mount', - variableName: 'mont', - description: 'pipette mount', - type: 'str', - choices: [ - { - displayName: 'Left', - value: 'left', - }, - { - displayName: 'Right', - value: 'right', - }, - ], - default: 'left', - }, - { - value: 'flex', - displayName: 'short test case', - variableName: 'short 2 options', - description: 'this play 2 short options', - type: 'str', - choices: [ - { - displayName: 'OT-2', - value: 'ot2', - }, - { - displayName: 'Flex', - value: 'flex', - }, - ], - default: 'flex', - }, - { - value: 'flex', - displayName: 'long test case', - variableName: 'long 2 options', - description: 'this play 2 long options', - type: 'str', - choices: [ - { - displayName: 'I am kind of long text version', - value: 'ot2', - }, - { - displayName: 'I am kind of long text version. Today is 3/15', - value: 'flex', - }, - ], - default: 'flex', - }, -] - interface ProtocolSetupParametersProps { protocolId: string - runTimeParameters?: RunTimeParameter[] + runTimeParameters: RunTimeParameter[] labwareOffsets?: LabwareOffsetCreateData[] } @@ -181,23 +40,24 @@ export function ProtocolSetupParameters({ chooseValueScreen, setChooseValueScreen, ] = React.useState(null) + const [ + showNumericalInputScreen, + setShowNumericalInputScreen, + ] = React.useState(null) const [resetValuesModal, showResetValuesModal] = React.useState( false ) - - // todo (nd:04/01/2024): remove mock and look at runTimeParameters prop - // const parameters = runTimeParameters ?? [] - const parameters = runTimeParameters ?? mockData + const [startSetup, setStartSetup] = React.useState(false) const [ runTimeParametersOverrides, setRunTimeParametersOverrides, - ] = React.useState(parameters) + ] = React.useState(runTimeParameters) const updateParameters = ( value: boolean | string | number, variableName: string ): void => { - const updatedParameters = parameters.map(parameter => { + const updatedParameters = runTimeParametersOverrides.map(parameter => { if (parameter.variableName === variableName) { return { ...parameter, value } } @@ -212,10 +72,19 @@ export function ProtocolSetupParameters({ setChooseValueScreen(updatedParameter) } } + if ( + showNumericalInputScreen && + showNumericalInputScreen.variableName === variableName + ) { + const updatedParameter = updatedParameters.find( + parameter => parameter.variableName === variableName + ) + if (updatedParameter != null) { + setShowNumericalInputScreen(updatedParameter as NumberParameter) + } + } } - // TODO(jr, 3/20/24): modify useCreateRunMutation to take in optional run time parameters - // newRunTimeParameters will be the param to plug in! const { createRun, isLoading } = useCreateRunMutation({ onSuccess: data => { queryClient @@ -226,8 +95,29 @@ export function ProtocolSetupParameters({ }, }) const handleConfirmValues = (): void => { - createRun({ protocolId, labwareOffsets }) + setStartSetup(true) + createRun({ + protocolId, + labwareOffsets, + runTimeParameterValues: getRunTimeParameterValuesForRun( + runTimeParametersOverrides + ), + }) } + + const handleSetParameter = (parameter: RunTimeParameter): void => { + if ('choices' in parameter) { + setChooseValueScreen(parameter) + } else if (parameter.type === 'bool') { + updateParameters(!parameter.value, parameter.variableName) + } else if (parameter.type === 'int' || parameter.type === 'float') { + setShowNumericalInputScreen(parameter) + } else { + // bad param + console.log('error') + } + } + let children = ( <> history.goBack()} onClickButton={handleConfirmValues} buttonText={t('confirm_values')} - iconName={isLoading ? 'ot-spinner' : undefined} + iconName={isLoading || startSetup ? 'ot-spinner' : undefined} iconPlacement="startIcon" secondaryButtonProps={{ buttonType: 'tertiaryLowLight', - buttonText: t('restore_default'), + buttonText: t('restore_defaults'), onClick: () => showResetValuesModal(true), }} /> @@ -249,6 +139,7 @@ export function ProtocolSetupParameters({ flexDirection={DIRECTION_COLUMN} gridGap={SPACING.spacing8} paddingX={SPACING.spacing40} + paddingBottom={SPACING.spacing40} > {runTimeParametersOverrides.map((parameter, index) => { return ( @@ -257,11 +148,7 @@ export function ProtocolSetupParameters({ hasIcon={!(parameter.type === 'bool')} status="general" title={parameter.displayName} - onClickSetupStep={() => - parameter.type === 'bool' - ? updateParameters(!parameter.value, parameter.variableName) - : setChooseValueScreen(parameter) - } + onClickSetupStep={() => handleSetParameter(parameter)} detail={formatRunTimeParameterValue(parameter, t)} description={parameter.description} fontSize="h4" @@ -272,7 +159,7 @@ export function ProtocolSetupParameters({ ) - if (chooseValueScreen != null && chooseValueScreen.type === 'str') { + if (chooseValueScreen != null) { children = ( setChooseValueScreen(null)} @@ -282,7 +169,15 @@ export function ProtocolSetupParameters({ /> ) } - // TODO(jr, 4/1/24): add the int/float component + if (showNumericalInputScreen != null) { + children = ( + setShowNumericalInputScreen(null)} + parameter={showNumericalInputScreen} + setParameter={updateParameters} + /> + ) + } return ( <> diff --git a/app/src/organisms/RunTimeControl/hooks.ts b/app/src/organisms/RunTimeControl/hooks.ts index c56f552b3ae..db042a2ce65 100644 --- a/app/src/organisms/RunTimeControl/hooks.ts +++ b/app/src/organisms/RunTimeControl/hooks.ts @@ -187,5 +187,6 @@ export function useRunErrors(runId: string | null): RunData['errors'] { export function useProtocolHasRunTimeParameters(runId: string | null): boolean { const mostRecentAnalysis = useMostRecentCompletedAnalysis(runId) - return mostRecentAnalysis?.runTimeParameters != null + const runTimeParameters = mostRecentAnalysis?.runTimeParameters ?? [] + return runTimeParameters.length > 0 } diff --git a/app/src/pages/ProtocolDetails/fixtures.ts b/app/src/pages/ProtocolDetails/fixtures.ts index d1752853bda..dd23bc4623e 100644 --- a/app/src/pages/ProtocolDetails/fixtures.ts +++ b/app/src/pages/ProtocolDetails/fixtures.ts @@ -14,7 +14,7 @@ export const mockRunTimeParameterData: RunTimeParameter[] = [ variableName: 'USE_GRIPPER', description: '', type: 'bool', - default: true, + default: false, value: true, }, { diff --git a/app/src/pages/ProtocolSetup/index.tsx b/app/src/pages/ProtocolSetup/index.tsx index f2fb24feaa5..97499316f27 100644 --- a/app/src/pages/ProtocolSetup/index.tsx +++ b/app/src/pages/ProtocolSetup/index.tsx @@ -258,6 +258,7 @@ function PrepareToRun({ const history = useHistory() const { makeSnackbar } = useToaster() const hasRunTimeParameters = useProtocolHasRunTimeParameters(runId) + console.log(hasRunTimeParameters) // Watch for scrolling to toggle dropshadow const scrollRef = React.useRef(null) const [isScrolled, setIsScrolled] = React.useState(false) diff --git a/shared-data/js/types.ts b/shared-data/js/types.ts index 75466e7558e..318db1d04e4 100644 --- a/shared-data/js/types.ts +++ b/shared-data/js/types.ts @@ -590,7 +590,7 @@ export interface AnalysisError { createdAt: string } -export interface NumberParameter { +export interface NumberParameter extends BaseRunTimeParameter { type: NumberParameterType min: number max: number @@ -602,13 +602,13 @@ export interface Choice { value: number | boolean | string } -interface ChoiceParameter { +interface ChoiceParameter extends BaseRunTimeParameter { type: RunTimeParameterType choices: Choice[] default: number | boolean | string } -interface BooleanParameter { +interface BooleanParameter extends BaseRunTimeParameter { type: BooleanParameterType default: boolean } @@ -621,7 +621,6 @@ type RunTimeParameterType = | BooleanParameterType | StringParameterType -type ParameterType = NumberParameter | ChoiceParameter | BooleanParameter interface BaseRunTimeParameter { displayName: string variableName: string @@ -630,7 +629,10 @@ interface BaseRunTimeParameter { suffix?: string } -export type RunTimeParameter = BaseRunTimeParameter & ParameterType +export type RunTimeParameter = + | BooleanParameter + | ChoiceParameter + | NumberParameter // TODO(BC, 10/25/2023): this type (and others in this file) probably belong in api-client, not here export interface CompletedProtocolAnalysis { From a81cc18f3be565baf8fa684cbdbe6bb6fddc5a06 Mon Sep 17 00:00:00 2001 From: Seth Foster Date: Thu, 11 Apr 2024 11:00:14 -0400 Subject: [PATCH 095/194] fix(shared-data): adapt 96 3.6 to new schema (#14869) The schema changes in edge weren't in release and need to be manually merged. Closes RQA-2558 --- .../general/ninety_six_channel/p1000/3_6.json | 31 +++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_6.json b/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_6.json index a00dce8ef17..c59dfce42ab 100644 --- a/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_6.json +++ b/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_6.json @@ -7,8 +7,35 @@ "pressFit": { "presses": 1, "increment": 0.0, - "speed": 10.0, - "distance": 13.0, + "speedByTipCount": { + "1": 10.0, + "2": 10.0, + "3": 10.0, + "4": 10.0, + "5": 10.0, + "6": 10.0, + "7": 10.0, + "8": 10.0, + "12": 10.0, + "16": 10.0, + "24": 10.0, + "48": 10.0 + }, + "distanceByTipCount": { + "1": 13.0, + "2": 13.0, + "3": 13.0, + "4": 13.0, + "5": 13.0, + "6": 13.0, + "7": 13.0, + "8": 13.0, + "12": 13.0, + "16": 13.0, + "24": 13.0, + "48": 13.0 + }, + "currentByTipCount": { "1": 0.2, "2": 0.25, From ee6ff25b5358c27de8d2a7f19276fdab38fb68d2 Mon Sep 17 00:00:00 2001 From: Max Marrone Date: Thu, 11 Apr 2024 11:20:58 -0400 Subject: [PATCH 096/194] chore(api,shared-data): Require Python >=3.10, not >=3.8 (#14867) --- api/setup.py | 4 +--- shared-data/python/setup.py | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/api/setup.py b/api/setup.py index ae53321ca22..1811b6b4e2d 100755 --- a/api/setup.py +++ b/api/setup.py @@ -46,8 +46,6 @@ def get_version(): "Intended Audience :: Science/Research", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Topic :: Scientific/Engineering", ] @@ -87,7 +85,7 @@ def read(*parts): if __name__ == "__main__": setup( - python_requires=">=3.8", + python_requires=">=3.10", name=DISTNAME, description=DESCRIPTION, license=LICENSE, diff --git a/shared-data/python/setup.py b/shared-data/python/setup.py index 8aebebcb408..4e1720cb610 100644 --- a/shared-data/python/setup.py +++ b/shared-data/python/setup.py @@ -130,8 +130,6 @@ def get_version(): "Intended Audience :: Science/Research", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Topic :: Scientific/Engineering", ] @@ -151,7 +149,7 @@ def get_version(): if __name__ == "__main__": setup( - python_requires=">=3.8", + python_requires=">=3.10", name=DISTNAME, description=DESCRIPTION, license=LICENSE, From 332355ecfed9d4439671e6d31ab656a74f6bbd62 Mon Sep 17 00:00:00 2001 From: Jethary Rader <66035149+jerader@users.noreply.github.com> Date: Thu, 11 Apr 2024 12:09:22 -0400 Subject: [PATCH 097/194] fix(protocol-designer): auto-generate trashBin for flex if no pipetting commands exist (#14857) closes AUTH-267 --- .../modals/CreateFileWizard/utils.ts | 15 +- .../src/step-forms/reducers/index.ts | 69 +++++++-- .../src/step-forms/test/utils.test.ts | 127 ++++++++++++++- .../src/step-forms/utils/index.ts | 145 ++++++++++++++++-- 4 files changed, 320 insertions(+), 36 deletions(-) diff --git a/protocol-designer/src/components/modals/CreateFileWizard/utils.ts b/protocol-designer/src/components/modals/CreateFileWizard/utils.ts index 2e0e8d54a72..20abcf27cb3 100644 --- a/protocol-designer/src/components/modals/CreateFileWizard/utils.ts +++ b/protocol-designer/src/components/modals/CreateFileWizard/utils.ts @@ -66,14 +66,14 @@ export const getCrashableModuleSelected = ( } export const MOVABLE_TRASH_CUTOUTS = [ - { - value: 'cutoutA1', - slot: 'A1', - }, { value: 'cutoutA3', slot: 'A3', }, + { + value: 'cutoutA1', + slot: 'A1', + }, { value: 'cutoutB1', slot: 'B1', @@ -241,13 +241,6 @@ export const getTrashSlot = (values: FormState): string => { ? [WASTE_CHUTE_CUTOUT as string] : [] - if ( - !cutouts.includes(FLEX_TRASH_DEFAULT_SLOT) && - !moduleSlots.includes('A3') - ) { - return FLEX_TRASH_DEFAULT_SLOT - } - const unoccupiedSlot = MOVABLE_TRASH_CUTOUTS.find( cutout => !cutouts.includes(cutout.value) && diff --git a/protocol-designer/src/step-forms/reducers/index.ts b/protocol-designer/src/step-forms/reducers/index.ts index a19cd27eeac..5d42a31b086 100644 --- a/protocol-designer/src/step-forms/reducers/index.ts +++ b/protocol-designer/src/step-forms/reducers/index.ts @@ -67,8 +67,10 @@ import { createPresavedStepForm, getDeckItemIdInSlot, getIdsInRange, + getUnoccupiedSlotForMoveableTrash, } from '../utils' -import { + +import type { CreateModuleAction, CreatePipettesAction, DeleteModuleAction, @@ -1379,6 +1381,12 @@ export const additionalEquipmentInvariantProperties = handleActions { const stagingAreaId = `${uuid()}:stagingArea` const cutoutId = getCutoutIdByAddressableArea( @@ -1531,11 +1539,11 @@ export const additionalEquipmentInvariantProperties = handleActions { it('gets id in array of length 1', () => { expect(getIdsInRange(['X'], 'X', 'X')).toEqual(['X']) @@ -29,3 +31,126 @@ describe('getIdsInRange', () => { expect(getIdsInRange(orderedIds, 'T', 'T')).toEqual(['T']) }) }) +describe('getUnoccupiedSlotForMoveableTrash', () => { + it('returns slot C1 when all other slots are occupied by modules, labware, moveLabware, and staging areas', () => { + const mockPDFile: any = { + commands: [ + { + key: '7353ae60-c85e-45c4-8d69-59ff3a97debd', + commandType: 'loadModule', + params: { + model: 'thermocyclerModuleV2', + location: { slotName: 'B1' }, + moduleId: + '771f390f-01a9-4615-9c4e-4dbfc95844b5:thermocyclerModuleType', + }, + }, + { + key: '82e5d08f-ceae-4eb8-8600-b61a973d47d9', + commandType: 'loadModule', + params: { + model: 'heaterShakerModuleV1', + location: { slotName: 'D1' }, + moduleId: + 'b9df03af-3844-4ae8-a1cf-cae61a6b4992:heaterShakerModuleType', + }, + }, + { + key: '49bc2a29-a7d2-42a6-8610-e07a9ad166df', + commandType: 'loadModule', + params: { + model: 'temperatureModuleV2', + location: { slotName: 'D3' }, + moduleId: + '52bea856-eea6-473c-80df-b316f3559692:temperatureModuleType', + }, + }, + { + key: '864fadd7-f2c1-400a-b2ef-24d0c887a3c8', + commandType: 'loadLabware', + params: { + displayName: 'Opentrons Flex 96 Tip Rack 50 µL', + labwareId: + '88881828-037c-4445-ba57-121164f4a53a:opentrons/opentrons_flex_96_tiprack_50ul/1', + loadName: 'opentrons_flex_96_tiprack_50ul', + namespace: 'opentrons', + version: 1, + location: { slotName: 'C2' }, + }, + }, + { + key: '79994418-d664-4884-9441-4b0fa62bd143', + commandType: 'loadLabware', + params: { + displayName: 'Bio-Rad 96 Well Plate 200 µL PCR', + labwareId: + '733c04a8-ae8c-449f-a1f9-ca3783fdda58:opentrons/biorad_96_wellplate_200ul_pcr/2', + loadName: 'biorad_96_wellplate_200ul_pcr', + namespace: 'opentrons', + version: 2, + location: { addressableAreaName: 'A4' }, + }, + }, + { + key: 'b2170a2c-d202-4129-9cd7-ffa4e35d57bb', + commandType: 'loadLabware', + params: { + displayName: 'Corning 24 Well Plate 3.4 mL Flat', + labwareId: + '32e97c67-866e-4153-bcb7-2b86b1d3f1fe:opentrons/corning_24_wellplate_3.4ml_flat/2', + loadName: 'corning_24_wellplate_3.4ml_flat', + namespace: 'opentrons', + version: 2, + location: { slotName: 'B3' }, + }, + }, + { + key: 'fb1807fe-ca16-4f75-b44d-803d704c7d98', + commandType: 'loadLabware', + params: { + displayName: 'Opentrons Flex 96 Tip Rack 50 µL', + labwareId: + '11fdsa8b1-bf4b-4a6c-80cb-b8e5bdfe309b:opentrons/opentrons_flex_96_tiprack_50ul/1', + loadName: 'opentrons_flex_96_tiprack_50ul', + namespace: 'opentrons', + version: 1, + location: { + labwareId: + '32e97c67-866e-4153-bcb7-2b86b1d3f1fe:opentrons/corning_24_wellplate_3.4ml_flat/2', + }, + }, + }, + { + commandType: 'moveLabware', + key: '1395243a-958f-4305-9687-52cdaf39f2b6', + params: { + labwareId: + '733c04a8-ae8c-449f-a1f9-ca3783fdda58:opentrons/biorad_96_wellplate_200ul_pcr/2', + strategy: 'usingGripper', + newLocation: { slotName: 'C1' }, + }, + }, + { + commandType: 'moveLabware', + key: '4e39e7ec-4ada-4e3c-8369-1ff7421061a9', + params: { + labwareId: + '32e97c67-866e-4153-bcb7-2b86b1d3f1fe:opentrons/corning_24_wellplate_3.4ml_flat/2', + strategy: 'usingGripper', + newLocation: { addressableAreaName: 'A4' }, + }, + }, + ] as CreateCommand[], + } + const mockStagingAreaSlotNames: AddressableAreaName[] = ['A4', 'B4'] + const mockHasWasteChuteCommands = false + + expect( + getUnoccupiedSlotForMoveableTrash( + mockPDFile, + mockHasWasteChuteCommands, + mockStagingAreaSlotNames + ) + ).toStrictEqual('C3') + }) +}) diff --git a/protocol-designer/src/step-forms/utils/index.ts b/protocol-designer/src/step-forms/utils/index.ts index 73596b481c6..d9b2d108132 100644 --- a/protocol-designer/src/step-forms/utils/index.ts +++ b/protocol-designer/src/step-forms/utils/index.ts @@ -6,20 +6,23 @@ import { getPipetteSpecsV2, GEN_ONE_MULTI_PIPETTES, THERMOCYCLER_MODULE_TYPE, + THERMOCYCLER_MODULE_V2, + WASTE_CHUTE_CUTOUT, + FLEX_ROBOT_TYPE, } from '@opentrons/shared-data' import { SPAN7_8_10_11_SLOT, TC_SPAN_SLOTS } from '../../constants' import { hydrateField } from '../../steplist/fieldLevel' import { LabwareDefByDefURI } from '../../labware-defs' -import type { DeckSlotId, ModuleType } from '@opentrons/shared-data' +import { getCutoutIdByAddressableArea } from '../../utils' import type { - AdditionalEquipmentOnDeck, - InitialDeckSetup, - ModuleOnDeck, - FormPipettesByMount, - FormPipette, - LabwareOnDeck as LabwareOnDeckType, -} from '../types' -import type { DeckSlot } from '../../types' + AddressableAreaName, + CutoutId, + DeckSlotId, + LoadLabwareCreateCommand, + LoadModuleCreateCommand, + ModuleType, + MoveLabwareCreateCommand, +} from '@opentrons/shared-data' import type { NormalizedPipette, NormalizedPipetteById, @@ -28,9 +31,54 @@ import type { InvariantContext, ModuleEntity, } from '@opentrons/step-generation' +import type { DeckSlot } from '../../types' import type { FormData } from '../../form-types' +import type { PDProtocolFile } from '../../file-types' +import type { + AdditionalEquipmentOnDeck, + InitialDeckSetup, + ModuleOnDeck, + FormPipettesByMount, + FormPipette, + LabwareOnDeck as LabwareOnDeckType, +} from '../types' export { createPresavedStepForm } from './createPresavedStepForm' +const MOVABLE_TRASH_CUTOUTS = [ + { + value: 'cutoutA3', + slot: 'A3', + }, + { + value: 'cutoutA1', + slot: 'A1', + }, + { + value: 'cutoutB1', + slot: 'B1', + }, + { + value: 'cutoutB3', + slot: 'B3', + }, + { + value: 'cutoutC1', + slot: 'C1', + }, + { + value: 'cutoutC3', + slot: 'C3', + }, + { + value: 'cutoutD1', + slot: 'D1', + }, + { + value: 'cutoutD3', + slot: 'D3', + }, +] + const slotToCutoutOt2Map: { [key: string]: string } = { '1': 'cutout1', '2': 'cutout2', @@ -248,3 +296,82 @@ export function getHydratedForm( // @ts-expect-error(sa, 2021-6-14):type this properly in #3161 return hydratedForm } + +export const getUnoccupiedSlotForMoveableTrash = ( + file: PDProtocolFile, + hasWasteChuteCommands: boolean, + stagingAreaSlotNames: AddressableAreaName[] +): string => { + const wasteChuteSlot = hasWasteChuteCommands ? [WASTE_CHUTE_CUTOUT] : [] + const stagingAreaCutoutIds = stagingAreaSlotNames.map(slotName => + getCutoutIdByAddressableArea( + slotName, + 'stagingAreaRightSlot', + FLEX_ROBOT_TYPE + ) + ) + const allLoadLabwareSlotNames = Object.values(file.commands) + .filter( + (command): command is LoadLabwareCreateCommand => + command.commandType === 'loadLabware' + ) + .reduce((acc: string[], command) => { + const location = command.params.location + if ( + location !== 'offDeck' && + location !== null && + 'slotName' in location + ) { + return [...acc, location.slotName] + } + return acc + }, []) + + const allLoadModuleSlotNames = Object.values(file.commands) + .filter( + (command): command is LoadModuleCreateCommand => + command.commandType === 'loadModule' + ) + .flatMap(command => { + // special-casing Thermocycler + if (command.params.model === THERMOCYCLER_MODULE_V2) { + return ['A1', command.params.location.slotName] + } else { + return command.params.location.slotName + } + }) + + const allMoveLabwareLocations = Object.values(file.commands) + .filter( + (command): command is MoveLabwareCreateCommand => + command.commandType === 'moveLabware' + ) + .reduce((acc: string[], command) => { + const newLocation = command.params.newLocation + if ( + newLocation !== 'offDeck' && + newLocation !== null && + 'slotName' in newLocation + ) { + return [...acc, newLocation.slotName] + } + return acc + }, []) + + const unoccupiedSlot = MOVABLE_TRASH_CUTOUTS.find( + cutout => + !allLoadLabwareSlotNames.includes(cutout.slot) && + !allLoadModuleSlotNames.includes(cutout.slot) && + !allMoveLabwareLocations.includes(cutout.slot) && + !wasteChuteSlot.includes(cutout.value as typeof WASTE_CHUTE_CUTOUT) && + !stagingAreaCutoutIds.includes(cutout.value as CutoutId) + ) + if (unoccupiedSlot == null) { + console.error( + 'Expected to find an unoccupied slot for auto-generating a trash bin but could not' + ) + return '' + } + + return unoccupiedSlot.slot +} From 9b45ea17e779bba60d16691d135c755ed8bb83d9 Mon Sep 17 00:00:00 2001 From: Rhyann Clarke <146747548+rclarke0@users.noreply.github.com> Date: Thu, 11 Apr 2024 14:14:53 -0400 Subject: [PATCH 098/194] Module ramp rate to google sheet (#14868) # Overview Calculates module ramp rates based on run log and uploads to google sheet. # Test Plan Ramp rate script tested on all three modules with different robots. # Changelog Created module ramp rate script to find ramp rate runs in run log folder and upload ramp rates to abr-run-data sheet Also changed IP address in error recording to a user input rather than an input in order to allow the command to be created into a desktop shortcut. # Review requests # Risk assessment --- .../data_collection/abr_google_drive.py | 11 +- .../data_collection/abr_robot_error.py | 20 +-- .../data_collection/module_ramp_rates.py | 154 ++++++++++++++++++ 3 files changed, 173 insertions(+), 12 deletions(-) create mode 100644 abr-testing/abr_testing/data_collection/module_ramp_rates.py diff --git a/abr-testing/abr_testing/data_collection/abr_google_drive.py b/abr-testing/abr_testing/data_collection/abr_google_drive.py index 6470f1e0410..a186019b35b 100644 --- a/abr-testing/abr_testing/data_collection/abr_google_drive.py +++ b/abr-testing/abr_testing/data_collection/abr_google_drive.py @@ -43,6 +43,8 @@ def create_data_dictionary( file_results = json.load(file) else: continue + if not isinstance(file_results, dict): + continue run_id = file_results.get("run_id", "NaN") if run_id in runs_to_save: robot = file_results.get("robot_name") @@ -107,7 +109,14 @@ def create_data_dictionary( hs_dict = read_robot_logs.hs_commands(file_results) tm_dict = read_robot_logs.temperature_module_commands(file_results) notes = {"Note1": "", "Jira Link": issue_url} - row_2 = {**row, **all_modules, **notes, **hs_dict, **tm_dict, **tc_dict} + row_2 = { + **row, + **all_modules, + **notes, + **hs_dict, + **tm_dict, + **tc_dict, + } headers = list(row_2.keys()) runs_and_robots[run_id] = row_2 else: diff --git a/abr-testing/abr_testing/data_collection/abr_robot_error.py b/abr-testing/abr_testing/data_collection/abr_robot_error.py index b139b5a3ade..231b8077eed 100644 --- a/abr-testing/abr_testing/data_collection/abr_robot_error.py +++ b/abr-testing/abr_testing/data_collection/abr_robot_error.py @@ -91,13 +91,6 @@ def get_error_info_from_robot( nargs=1, help="Path to long term storage directory for run logs.", ) - parser.add_argument( - "robot_ip", - metavar="ROBOT_IP", - type=str, - nargs=1, - help="IP address of robot as string.", - ) parser.add_argument( "jira_api_token", metavar="JIRA_API_TOKEN", @@ -130,14 +123,18 @@ def get_error_info_from_robot( ) args = parser.parse_args() storage_directory = args.storage_directory[0] - ip = args.robot_ip[0] + ip = str(input("Enter Robot IP: ")) url = "https://opentrons.atlassian.net" api_token = args.jira_api_token[0] email = args.email[0] board_id = args.board_id[0] reporter_id = args.reporter_id[0] ticket = jira_tool.JiraTicket(url, api_token, email) - error_runs = get_error_runs_from_robot(ip) + try: + error_runs = get_error_runs_from_robot(ip) + except requests.exceptions.InvalidURL: + print("Invalid IP address.") + sys.exit() one_run = error_runs[-1] # Most recent run with error. ( summary, @@ -147,7 +144,7 @@ def get_error_info_from_robot( whole_description_str, run_log_file_path, ) = get_error_info_from_robot(ip, one_run, storage_directory) - # get calibration data + # Get Calibration Data saved_file_path_calibration, calibration = read_robot_logs.get_calibration_offsets( ip, storage_directory ) @@ -156,6 +153,7 @@ def get_error_info_from_robot( # TODO: make argument or see if I can get rid of with using board_id. project_key = "RABR" parent_key = project_key + "-" + robot[-1] + # TODO: read board to see if ticket for run id already exists. # CREATE TICKET issue_key = ticket.create_ticket( summary, @@ -172,7 +170,7 @@ def get_error_info_from_robot( issue_url = ticket.open_issue(issue_key) # MOVE FILES TO ERROR FOLDER. error_files = [saved_file_path_calibration, run_log_file_path] + file_paths - error_folder_path = os.path.join(storage_directory, str("RABR-238")) + error_folder_path = os.path.join(storage_directory, issue_key) os.makedirs(error_folder_path, exist_ok=True) for source_file in error_files: destination_file = os.path.join( diff --git a/abr-testing/abr_testing/data_collection/module_ramp_rates.py b/abr-testing/abr_testing/data_collection/module_ramp_rates.py new file mode 100644 index 00000000000..dc402071bb7 --- /dev/null +++ b/abr-testing/abr_testing/data_collection/module_ramp_rates.py @@ -0,0 +1,154 @@ +"""Get ramp rates of modules.""" +from abr_testing.automation import google_sheets_tool +from abr_testing.data_collection import read_robot_logs +import gspread # type: ignore[import] +import argparse +import os +import sys +import json +from datetime import datetime +from typing import Dict, Any +import requests + + +def ramp_rate(file_results: Dict[str, Any]) -> Dict[int, float]: + """Get ramp rates.""" + i = 0 + commands = file_results["commands"] + for command in commands: + commandType = command["commandType"] + if ( + commandType == "thermocycler/setTargetBlockTemperature" + or commandType == "temperatureModule/setTargetTemperature" + or commandType == "heaterShaker/setTargetTemperature" + ): + temp = command["params"].get("celsius", 0.0) + if ( + commandType == "thermocycler/waitForBlockTemperature" + or commandType == "temperatureModule/waitForTemperature" + or commandType == "heaterShaker/waitForTemperature" + ): + start_time = datetime.strptime( + command.get("startedAt", ""), "%Y-%m-%dT%H:%M:%S.%f%z" + ) + end_time = datetime.strptime( + command.get("completedAt", ""), "%Y-%m-%dT%H:%M:%S.%f%z" + ) + duration = (end_time - start_time).total_seconds() + i += 1 + temps_and_durations[duration] = temp + ramp_rates = {} + times = list(temps_and_durations.keys()) + for i in range(len(times) - 1): + time1 = times[i] + time2 = times[i + 1] + temp1 = temps_and_durations[time1] + temp2 = temps_and_durations[time2] + ramp_rate = (temp2 - temp1) / (time2) + ramp_rates[i] = ramp_rate + return ramp_rates + + +if __name__ == "__main__": + # SCRIPT ARGUMENTS + parser = argparse.ArgumentParser(description="Read run logs on google drive.") + parser.add_argument( + "storage_directory", + metavar="STORAGE_DIRECTORY", + type=str, + nargs=1, + help="Path to long term storage directory for run logs.", + ) + parser.add_argument( + "google_sheet_name", + metavar="GOOGLE_SHEET_NAME", + type=str, + nargs=1, + help="Google sheet name.", + ) + parser.add_argument( + "email", metavar="EMAIL", type=str, nargs=1, help="opentrons gmail." + ) + args = parser.parse_args() + storage_directory = args.storage_directory[0] + google_sheet_name = args.google_sheet_name[0] + # FIND CREDENTIALS FILE + try: + credentials_path = os.path.join(storage_directory, "credentials.json") + except FileNotFoundError: + print(f"Add credentials.json file to: {storage_directory}.") + sys.exit() + # CONNECT TO GOOGLE SHEET + try: + google_sheet = google_sheets_tool.google_sheet( + credentials_path, google_sheet_name, 1 + ) + print(f"Connected to google sheet: {google_sheet_name}") + except gspread.exceptions.APIError: + print("ERROR: Check google sheet name. Check credentials file.") + sys.exit() + run_ids_on_sheet = google_sheet.get_column(2) + runs_and_robots = {} + for filename in os.listdir(storage_directory): + file_path = os.path.join(storage_directory, filename) + if file_path.endswith(".json"): + with open(file_path) as file: + file_results = json.load(file) + else: + continue + # CHECK if file is ramp rate run + run_id = file_results.get("run_id", None) + temps_and_durations: Dict[float, float] = dict() + if run_id is not None and run_id not in run_ids_on_sheet: + + ramp_rates = ramp_rate(file_results) + protocol_name = file_results["protocol"]["metadata"].get("protocolName", "") + if "Ramp Rate" in protocol_name: + ip = filename.split("_")[0] + if len(ramp_rates) > 1: + cooling_ramp_rate = abs(min(ramp_rates.values())) + heating_ramp_rate = abs(max(ramp_rates.values())) + start_time = datetime.strptime( + file_results.get("startedAt", ""), "%Y-%m-%dT%H:%M:%S.%f%z" + ) + start_date = str(start_time.date()) + module_serial_number = file_results["modules"][0].get( + "serialNumber", "NaN" + ) + try: + response = requests.get( + f"http://{ip}:31950/modules", + headers={"opentrons-version": "3"}, + ) + modules = response.json() + for module in modules["data"]: + if module["serialNumber"] == module_serial_number: + firmwareVersion = module["firmwareVersion"] + else: + firmwareVersion = "NaN" + except requests.exceptions.ConnectionError: + firmwareVersion = "NaN" + row = { + "Robot": file_results.get("robot_name", ""), + "Run_ID": run_id, + "Protocol_Name": file_results["protocol"]["metadata"].get( + "protocolName", "" + ), + "Software Version": file_results.get("API_Version", ""), + "Firmware Version": firmwareVersion, + "Date": start_date, + "Serial Number": module_serial_number, + "Approx. Average Heating Ramp Rate (C/s)": heating_ramp_rate, + "Approx. Average Cooling Ramp Rate (C/s)": cooling_ramp_rate, + } + headers = list(row.keys()) + runs_and_robots[run_id] = row + read_robot_logs.write_to_local_and_google_sheet( + runs_and_robots, + storage_directory, + google_sheet_name, + google_sheet, + headers, + ) + else: + continue From fa6066f91ff1a6fc88060c2e14b4ae12d3f3c625 Mon Sep 17 00:00:00 2001 From: Jethary Rader <66035149+jerader@users.noreply.github.com> Date: Thu, 11 Apr 2024 14:34:49 -0400 Subject: [PATCH 099/194] feat(protocol-designer): export and announcement modal for PD 8.1 (#14870) closes AUTH-8 AUTH-9 --- .../cypress/integration/migrations.spec.js | 2 +- .../components/FileSidebar/FileSidebar.tsx | 8 ++--- .../__tests__/FileSidebar.test.tsx | 7 ++++ .../AnnouncementModal/announcements.tsx | 34 +++++++++++++++++++ .../src/localization/en/alert.json | 4 +-- .../src/localization/en/modal.json | 8 +++++ protocol-designer/src/tutorial/index.ts | 3 +- 7 files changed, 58 insertions(+), 8 deletions(-) diff --git a/protocol-designer/cypress/integration/migrations.spec.js b/protocol-designer/cypress/integration/migrations.spec.js index 6c1d01a0ee7..303c7b91701 100644 --- a/protocol-designer/cypress/integration/migrations.spec.js +++ b/protocol-designer/cypress/integration/migrations.spec.js @@ -127,7 +127,7 @@ describe('Protocol fixtures migrate and match snapshots', () => { cy.get('div') .contains( - 'This protocol can only run on app and robot server version 7.1 or higher' + 'This protocol can only run on app and robot server version 7.2.0 or higher' ) .should('exist') cy.get('button').contains('continue', { matchCase: false }).click() diff --git a/protocol-designer/src/components/FileSidebar/FileSidebar.tsx b/protocol-designer/src/components/FileSidebar/FileSidebar.tsx index e05a80e3163..31bdfa60723 100644 --- a/protocol-designer/src/components/FileSidebar/FileSidebar.tsx +++ b/protocol-designer/src/components/FileSidebar/FileSidebar.tsx @@ -237,9 +237,9 @@ export function v8WarningContent(t: any): JSX.Element { return (

    - {t(`hint.export_v8_protocol_7_1.body1`)}{' '} - {t(`hint.export_v8_protocol_7_1.body2`)} - {t(`hint.export_v8_protocol_7_1.body3`)} + {t(`hint.export_v8_1_protocol_7_2.body1`)}{' '} + {t(`hint.export_v8_1_protocol_7_2.body2`)} + {t(`hint.export_v8_1_protocol_7_2.body3`)}

    ) @@ -350,7 +350,7 @@ export function FileSidebar(): JSX.Element { content: React.ReactNode } => { return { - hintKey: 'export_v8_protocol_7_1', + hintKey: 'export_v8_1_protocol_7_2', content: v8WarningContent(t), } } diff --git a/protocol-designer/src/components/FileSidebar/__tests__/FileSidebar.test.tsx b/protocol-designer/src/components/FileSidebar/__tests__/FileSidebar.test.tsx index a9d2978b981..827af5a2aa8 100644 --- a/protocol-designer/src/components/FileSidebar/__tests__/FileSidebar.test.tsx +++ b/protocol-designer/src/components/FileSidebar/__tests__/FileSidebar.test.tsx @@ -74,6 +74,13 @@ describe('FileSidebar', () => { vi.resetAllMocks() cleanup() }) + it('renders the file sidebar and exports with blocking hint for exporting', () => { + vi.mocked(useBlockingHint).mockReturnValue(
    mock blocking hint
    ) + render() + fireEvent.click(screen.getByRole('button', { name: 'Export' })) + expect(vi.mocked(useBlockingHint)).toHaveBeenCalled() + screen.getByText('mock blocking hint') + }) it('renders the file sidebar and buttons work as expected with no warning upon export', () => { render() screen.getByText('Protocol File') diff --git a/protocol-designer/src/components/modals/AnnouncementModal/announcements.tsx b/protocol-designer/src/components/modals/AnnouncementModal/announcements.tsx index aab430bf549..b10c6d75407 100644 --- a/protocol-designer/src/components/modals/AnnouncementModal/announcements.tsx +++ b/protocol-designer/src/components/modals/AnnouncementModal/announcements.tsx @@ -265,5 +265,39 @@ export const useAnnouncements = (): Announcement[] => { ), }, + { + announcementKey: 'customParamsAndMultiTipAndModule8.1', + image: , + heading: t('announcements.header', { pd: PD }), + message: ( + <> +

    + {t('announcements.customParamsAndMultiTipAndModule.body1', { + pd: PD, + })} +

    +
      +
    • {t('announcements.customParamsAndMultiTipAndModule.body2')}
    • +
    • + }} + /> +
    • +
    • {t('announcements.customParamsAndMultiTipAndModule.body4')}
    • +
    • {t('announcements.customParamsAndMultiTipAndModule.body5')}
    • +
    +

    + }} + values={{ app: APP }} + /> +

    + + ), + }, ] } diff --git a/protocol-designer/src/localization/en/alert.json b/protocol-designer/src/localization/en/alert.json index 4548d19e57c..999c43500b0 100644 --- a/protocol-designer/src/localization/en/alert.json +++ b/protocol-designer/src/localization/en/alert.json @@ -53,10 +53,10 @@ "title": "Missing labware", "body": "One or more module has no labware on it. We recommend you add labware before proceeding" }, - "export_v8_protocol_7_1": { + "export_v8_1_protocol_7_2": { "title": "Robot and app update may be required", "body1": "This protocol can only run on app and robot server version", - "body2": "7.1 or higher", + "body2": "7.2.0 or higher", "body3": ". Please ensure your robot is updated to the correct version." }, "change_magnet_module_model": { diff --git a/protocol-designer/src/localization/en/modal.json b/protocol-designer/src/localization/en/modal.json index a07cb3b1310..fd85b5a8001 100644 --- a/protocol-designer/src/localization/en/modal.json +++ b/protocol-designer/src/localization/en/modal.json @@ -42,6 +42,14 @@ "deckConfigAnd96Channel": { "body1": "Introducing the {{pd}} 8.0 with deck configuration and 96-channel pipette support!", "body2": "All protocols now require {{app}} version 7.1+ to run." + }, + "customParamsAndMultiTipAndModule": { + "body1": "Introducing {{pd}} 8.1. Starting today, you will be able to:", + "body2": "Customize blowout speed and height.", + "body3": "Adjust horizontal position within a well when aspirating, dispensing, or mixing.", + "body4": "Assign up to three types of tip racks to a single pipette.", + "body5": "Add multiple Temperature Modules to the deck (Flex only).", + "body6": "All protocols require {{app}} version 7.2.0 or later to run." } }, "labware_selection": { diff --git a/protocol-designer/src/tutorial/index.ts b/protocol-designer/src/tutorial/index.ts index 58a0f522c60..ecc17f49bb4 100644 --- a/protocol-designer/src/tutorial/index.ts +++ b/protocol-designer/src/tutorial/index.ts @@ -11,7 +11,7 @@ type HintKey = // normal hints | 'waste_chute_warning' // blocking hints | 'custom_labware_with_modules' - | 'export_v8_protocol_7_1' + | 'export_v8_1_protocol_7_2' | 'change_magnet_module_model' // DEPRECATED HINTS (keep a record to avoid name collisions with old persisted dismissal states) // 'export_v4_protocol' @@ -20,5 +20,6 @@ type HintKey = // normal hints // | 'export_v6_protocol_6_10' // | 'export_v6_protocol_6_20' // | 'export_v7_protocol_7_0' +// | 'export_v8_protocol_7_1' export { actions, rootReducer, selectors } export type { RootState, HintKey } From 9be2f8fbe9d2afc6f2ee41307e202e721611db02 Mon Sep 17 00:00:00 2001 From: koji Date: Thu, 11 Apr 2024 15:27:45 -0400 Subject: [PATCH 100/194] fix(app): remove unnecessary console.log (#14880) * fix(app): remove unnecessary console.log --- app/src/organisms/ChooseProtocolSlideout/index.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/organisms/ChooseProtocolSlideout/index.tsx b/app/src/organisms/ChooseProtocolSlideout/index.tsx index c1b2c2eea72..6a1c1a0aa8c 100644 --- a/app/src/organisms/ChooseProtocolSlideout/index.tsx +++ b/app/src/organisms/ChooseProtocolSlideout/index.tsx @@ -105,7 +105,6 @@ export function ChooseProtocolSlideoutComponent( const runTimeParametersFromAnalysis = selectedProtocol?.mostRecentAnalysis?.runTimeParameters ?? [] - console.log('runTimeParametersFromAnalysis', runTimeParametersFromAnalysis) const hasRunTimeParameters = runTimeParametersFromAnalysis.length > 0 From 044b37fc01d83bcd9f9cfb2400ea3854de966469 Mon Sep 17 00:00:00 2001 From: Jethary Rader <66035149+jerader@users.noreply.github.com> Date: Fri, 12 Apr 2024 08:46:48 -0400 Subject: [PATCH 101/194] fix(protocol-designer, components): discarding vs delete step form button text (#14872) closes AUTH-319 --- components/src/lists/TitledList.tsx | 23 ++++++++++++------- .../src/localization/en/modal.json | 2 +- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/components/src/lists/TitledList.tsx b/components/src/lists/TitledList.tsx index 58a12d19b6e..4fbe4ab58ee 100644 --- a/components/src/lists/TitledList.tsx +++ b/components/src/lists/TitledList.tsx @@ -2,10 +2,13 @@ import * as React from 'react' import cx from 'classnames' -import styles from './lists.module.css' import { Icon } from '../icons' +import { StyledText } from '../atoms' +import { COLORS } from '../helix-design-system' import type { IconName, IconProps } from '../icons' +import styles from './lists.module.css' + // TODO(bc, 2021-03-31): reconsider whether this belongs in components library // it is bloated with application specific functionality @@ -98,6 +101,15 @@ export function TitledList(props: TitledListProps): JSX.Element { iconProps && iconProps.className ) + let textColor = '' + if (disabled) { + // the below hex code is for our legacy --c-font-disabled to match other text colors + textColor = '#9c9c9c' + } else if (props.selected && !disabled) { + // the below hex code is for our legacy --c-highlight to match other text colors + textColor = '#5fd8ee' + } + return (
    )} -

    + {props.title} -

    + {collapsible && (
    Date: Fri, 12 Apr 2024 09:54:16 -0400 Subject: [PATCH 102/194] feat(robot-server): add runtime parameter definitions to run summary (#14866) Adds the runtime parameter definitions to the run summary for both current and non current runs, accessible via the GET /runs and /runs/{run_id} endpoints. --- .../persistence/_migrations/v3_to_v4.py | 6 + .../robot_server/persistence/pydantic.py | 19 +- .../persistence/tables/schema_4.py | 7 + .../robot_server/runs/run_controller.py | 1 + .../robot_server/runs/run_data_manager.py | 20 +- robot-server/robot_server/runs/run_models.py | 20 +- robot-server/robot_server/runs/run_store.py | 41 +++- .../test_json_v6_protocol_run.tavern.yaml | 1 + .../test_json_v7_protocol_run.tavern.yaml | 1 + .../runs/test_protocol_run.tavern.yaml | 2 + ...t_run_queued_protocol_commands.tavern.yaml | 1 + ...t_run_with_run_time_parameters.tavern.yaml | 203 ++++++++++++++++++ robot-server/tests/persistence/test_tables.py | 1 + .../tests/runs/test_run_controller.py | 18 +- .../tests/runs/test_run_data_manager.py | 73 ++++++- robot-server/tests/runs/test_run_store.py | 109 +++++++++- 16 files changed, 509 insertions(+), 14 deletions(-) create mode 100644 robot-server/tests/integration/http_api/runs/test_run_with_run_time_parameters.tavern.yaml diff --git a/robot-server/robot_server/persistence/_migrations/v3_to_v4.py b/robot-server/robot_server/persistence/_migrations/v3_to_v4.py index 8b4445aaec3..b67d11d34ec 100644 --- a/robot-server/robot_server/persistence/_migrations/v3_to_v4.py +++ b/robot-server/robot_server/persistence/_migrations/v3_to_v4.py @@ -3,6 +3,7 @@ Summary of changes from schema 3: - Adds a new "run_time_parameter_values_and_defaults" column to analysis table +- Adds a new "run_time_parameters" column to run table """ from pathlib import Path @@ -50,3 +51,8 @@ def add_column( schema_4.analysis_table.name, schema_4.analysis_table.c.run_time_parameter_values_and_defaults, ) + add_column( + dest_engine, + schema_4.run_table.name, + schema_4.run_table.c.run_time_parameters, + ) diff --git a/robot-server/robot_server/persistence/pydantic.py b/robot-server/robot_server/persistence/pydantic.py index c3486394ad4..c56312ec166 100644 --- a/robot-server/robot_server/persistence/pydantic.py +++ b/robot-server/robot_server/persistence/pydantic.py @@ -1,7 +1,8 @@ """Store Pydantic objects in the SQL database.""" -from typing import Type, TypeVar -from pydantic import BaseModel, parse_raw_as +import json +from typing import Type, TypeVar, List, Sequence +from pydantic import BaseModel, parse_raw_as, parse_obj_as _BaseModelT = TypeVar("_BaseModelT", bound=BaseModel) @@ -17,6 +18,16 @@ def pydantic_to_json(obj: BaseModel) -> str: ) -def json_to_pydantic(model: Type[_BaseModelT], json: str) -> _BaseModelT: +def pydantic_list_to_json(obj_list: Sequence[BaseModel]) -> str: + """Serialize a list of Pydantic objects for storing in the SQL database.""" + return json.dumps([obj.dict(by_alias=True, exclude_none=True) for obj in obj_list]) + + +def json_to_pydantic(model: Type[_BaseModelT], json_str: str) -> _BaseModelT: """Parse a Pydantic object stored in the SQL database.""" - return parse_raw_as(model, json) + return parse_raw_as(model, json_str) + + +def json_to_pydantic_list(model: Type[_BaseModelT], json_str: str) -> List[_BaseModelT]: + """Parse a list of Pydantic objects stored in the SQL database.""" + return [parse_obj_as(model, obj_dict) for obj_dict in json.loads(json_str)] diff --git a/robot-server/robot_server/persistence/tables/schema_4.py b/robot-server/robot_server/persistence/tables/schema_4.py index 47d29d3d8f3..d1662bf7adc 100644 --- a/robot-server/robot_server/persistence/tables/schema_4.py +++ b/robot-server/robot_server/persistence/tables/schema_4.py @@ -85,6 +85,13 @@ sqlalchemy.Column("engine_status", sqlalchemy.String, nullable=True), # column added in schema v1 sqlalchemy.Column("_updated_at", UTCDateTime, nullable=True), + # column added in schema v4 + sqlalchemy.Column( + "run_time_parameters", + # Stores a JSON string. See RunStore. + sqlalchemy.String, + nullable=True, + ), ) action_table = sqlalchemy.Table( diff --git a/robot-server/robot_server/runs/run_controller.py b/robot-server/robot_server/runs/run_controller.py index 782754c1da6..923c9cfa64e 100644 --- a/robot-server/robot_server/runs/run_controller.py +++ b/robot-server/robot_server/runs/run_controller.py @@ -106,4 +106,5 @@ async def _run_protocol_and_insert_result( run_id=self._run_id, summary=result.state_summary, commands=result.commands, + run_time_parameters=result.parameters, ) diff --git a/robot-server/robot_server/runs/run_data_manager.py b/robot-server/robot_server/runs/run_data_manager.py index 570537a135c..154a1584823 100644 --- a/robot-server/robot_server/runs/run_data_manager.py +++ b/robot-server/robot_server/runs/run_data_manager.py @@ -22,13 +22,14 @@ from .run_store import RunResource, RunStore, BadRunResource, BadStateSummary from .run_models import Run, BadRun, RunDataError -from opentrons.protocol_engine.types import DeckConfigurationType +from opentrons.protocol_engine.types import DeckConfigurationType, RunTimeParameter def _build_run( run_resource: Union[RunResource, BadRunResource], state_summary: Union[StateSummary, BadStateSummary], current: bool, + run_time_parameters: List[RunTimeParameter], ) -> Union[Run, BadRun]: # TODO(mc, 2022-05-16): improve persistence strategy # such that this default summary object is not needed @@ -49,6 +50,7 @@ def _build_run( completedAt=state_summary.completedAt, startedAt=state_summary.startedAt, liquids=state_summary.liquids, + runTimeParameters=run_time_parameters, ) errors: List[EnumeratedError] = [] @@ -102,6 +104,7 @@ def _build_run( completedAt=state.completedAt, startedAt=state.startedAt, liquids=state.liquids, + runTimeParameters=run_time_parameters, ) @@ -172,6 +175,7 @@ async def create( run_id=prev_run_id, summary=prev_run_result.state_summary, commands=prev_run_result.commands, + run_time_parameters=prev_run_result.parameters, ) state_summary = await self._engine_store.create( run_id=run_id, @@ -196,6 +200,7 @@ async def create( run_resource=run_resource, state_summary=state_summary, current=True, + run_time_parameters=[], ) def get(self, run_id: str) -> Union[Run, BadRun]: @@ -215,9 +220,10 @@ def get(self, run_id: str) -> Union[Run, BadRun]: """ run_resource = self._run_store.get(run_id=run_id) state_summary = self._get_state_summary(run_id=run_id) + parameters = self._get_run_time_parameters(run_id=run_id) current = run_id == self._engine_store.current_run_id - return _build_run(run_resource, state_summary, current) + return _build_run(run_resource, state_summary, current, parameters) def get_run_loaded_labware_definitions( self, run_id: str @@ -260,6 +266,7 @@ def get_all(self, length: Optional[int]) -> List[Union[Run, BadRun]]: run_resource=run_resource, state_summary=self._get_state_summary(run_resource.run_id), current=run_resource.run_id == self._engine_store.current_run_id, + run_time_parameters=self._get_run_time_parameters(run_resource.run_id), ) for run_resource in self._run_store.get_all(length) ] @@ -310,15 +317,18 @@ async def update(self, run_id: str, current: Optional[bool]) -> Union[Run, BadRu run_id=run_id, summary=state_summary, commands=commands, + run_time_parameters=parameters, ) else: state_summary = self._engine_store.engine.state_view.get_summary() + parameters = self._engine_store.runner.run_time_parameters run_resource = self._run_store.get(run_id=run_id) return _build_run( run_resource=run_resource, state_summary=state_summary, current=next_current, + run_time_parameters=parameters, ) def get_commands_slice( @@ -385,3 +395,9 @@ def _get_state_summary(self, run_id: str) -> Union[StateSummary, BadStateSummary def _get_good_state_summary(self, run_id: str) -> Optional[StateSummary]: summary = self._get_state_summary(run_id) return summary if isinstance(summary, StateSummary) else None + + def _get_run_time_parameters(self, run_id: str) -> List[RunTimeParameter]: + if run_id == self._engine_store.current_run_id: + return self._engine_store.runner.run_time_parameters + else: + return self._run_store.get_run_time_parameters(run_id=run_id) diff --git a/robot-server/robot_server/runs/run_models.py b/robot-server/robot_server/runs/run_models.py index 7da6e0b0a5d..c93049bfef4 100644 --- a/robot-server/robot_server/runs/run_models.py +++ b/robot-server/robot_server/runs/run_models.py @@ -18,7 +18,7 @@ Liquid, CommandNote, ) -from opentrons.protocol_engine.types import RunTimeParamValuesType +from opentrons.protocol_engine.types import RunTimeParameter, RunTimeParamValuesType from opentrons_shared_data.errors import GeneralError from robot_server.service.json_api import ResourceModel from robot_server.errors.error_responses import ErrorDetails @@ -121,6 +121,15 @@ class Run(ResourceModel): ..., description="Labware offsets to apply as labware are loaded.", ) + runTimeParameters: List[RunTimeParameter] = Field( + default_factory=list, + description=( + "Run time parameters used during the run." + " These are the parameters that are defined in the protocol, with values" + " specified either in the run creation request or default values from the protocol" + " if none are specified in the request." + ), + ) protocolId: Optional[str] = Field( None, description=( @@ -185,6 +194,15 @@ class BadRun(ResourceModel): ..., description="Labware offsets to apply as labware are loaded.", ) + runTimeParameters: List[RunTimeParameter] = Field( + default_factory=list, + description=( + "Run time parameters used during the run." + " These are the parameters that are defined in the protocol, with values" + " specified either in the run creation request or default values from the protocol" + " if none are specified in the request." + ), + ) protocolId: Optional[str] = Field( None, description=( diff --git a/robot-server/robot_server/runs/run_store.py b/robot-server/robot_server/runs/run_store.py index 5aa6dbae96b..b86ec8e19ea 100644 --- a/robot-server/robot_server/runs/run_store.py +++ b/robot-server/robot_server/runs/run_store.py @@ -12,6 +12,7 @@ from opentrons.util.helpers import utc_now from opentrons.protocol_engine import StateSummary, CommandSlice from opentrons.protocol_engine.commands import Command +from opentrons.protocol_engine.types import RunTimeParameter from opentrons_shared_data.errors.exceptions import ( EnumeratedError, @@ -25,7 +26,12 @@ run_command_table, action_table, ) -from robot_server.persistence.pydantic import json_to_pydantic, pydantic_to_json +from robot_server.persistence.pydantic import ( + json_to_pydantic, + pydantic_to_json, + json_to_pydantic_list, + pydantic_list_to_json, +) from robot_server.protocols.protocol_store import ProtocolNotFoundError from .action_models import RunAction, RunActionType @@ -102,6 +108,7 @@ def update_run_state( run_id: str, summary: StateSummary, commands: List[Command], + run_time_parameters: List[RunTimeParameter], ) -> RunResource: """Update the run's state summary and commands list. @@ -109,6 +116,7 @@ def update_run_state( run_id: The run to update summary: The run's equipment and status summary. commands: The run's commands. + run_time_parameters: The run's run time parameters, if any. Returns: The run resource. @@ -124,6 +132,7 @@ def update_run_state( run_id=run_id, state_summary=summary, engine_status=summary.status, + run_time_parameters=run_time_parameters, ) ) ) @@ -346,6 +355,33 @@ def get_state_summary(self, run_id: str) -> Union[StateSummary, BadStateSummary] ) ) + @lru_cache(maxsize=_CACHE_ENTRIES) + def get_run_time_parameters(self, run_id: str) -> List[RunTimeParameter]: + """Get the archived run time parameters. + + This is a list of the run's parameter definitions (if any), + including the values used in the run itself, along with the default value, + constraints and associated names and descriptions. + """ + select_run_data = sqlalchemy.select(run_table.c.run_time_parameters).where( + run_table.c.id == run_id + ) + + with self._sql_engine.begin() as transaction: + row = transaction.execute(select_run_data).one() + + try: + return ( + json_to_pydantic_list(RunTimeParameter, row.run_time_parameters) # type: ignore[arg-type] + if row.run_time_parameters is not None + else [] + ) + except ValidationError: + log.warning( + f"Error retrieving run time parameters for {run_id}", exc_info=True + ) + return [] + def get_commands_slice( self, run_id: str, @@ -476,6 +512,7 @@ def _clear_caches(self) -> None: self.get_all.cache_clear() self.get_state_summary.cache_clear() self.get_command.cache_clear() + self.get_run_time_parameters.cache_clear() # The columns that must be present in a row passed to _convert_row_to_run(). @@ -552,9 +589,11 @@ def _convert_state_to_sql_values( run_id: str, state_summary: StateSummary, engine_status: str, + run_time_parameters: List[RunTimeParameter], ) -> Dict[str, object]: return { "state_summary": pydantic_to_json(state_summary), "engine_status": engine_status, "_updated_at": utc_now(), + "run_time_parameters": pydantic_list_to_json(run_time_parameters), } diff --git a/robot-server/tests/integration/http_api/runs/test_json_v6_protocol_run.tavern.yaml b/robot-server/tests/integration/http_api/runs/test_json_v6_protocol_run.tavern.yaml index 4ff631bf277..e7ac3483dd7 100644 --- a/robot-server/tests/integration/http_api/runs/test_json_v6_protocol_run.tavern.yaml +++ b/robot-server/tests/integration/http_api/runs/test_json_v6_protocol_run.tavern.yaml @@ -50,6 +50,7 @@ stages: displayName: Water description: Liquid H2O displayColor: '#7332a8' + runTimeParameters: [] protocolId: '{protocol_id}' - name: Execute a setup command diff --git a/robot-server/tests/integration/http_api/runs/test_json_v7_protocol_run.tavern.yaml b/robot-server/tests/integration/http_api/runs/test_json_v7_protocol_run.tavern.yaml index 317d339fbbf..bdc4ad4a66d 100644 --- a/robot-server/tests/integration/http_api/runs/test_json_v7_protocol_run.tavern.yaml +++ b/robot-server/tests/integration/http_api/runs/test_json_v7_protocol_run.tavern.yaml @@ -45,6 +45,7 @@ stages: definitionUri: opentrons/opentrons_1_trash_1100ml_fixed/1 location: !anydict labwareOffsets: [] + runTimeParameters: [] liquids: - id: waterId displayName: Water diff --git a/robot-server/tests/integration/http_api/runs/test_protocol_run.tavern.yaml b/robot-server/tests/integration/http_api/runs/test_protocol_run.tavern.yaml index 48dc570d6c9..67d1511a666 100644 --- a/robot-server/tests/integration/http_api/runs/test_protocol_run.tavern.yaml +++ b/robot-server/tests/integration/http_api/runs/test_protocol_run.tavern.yaml @@ -42,6 +42,7 @@ stages: definitionUri: opentrons/opentrons_1_trash_1100ml_fixed/1 location: !anydict labwareOffsets: [] + runTimeParameters: [] protocolId: '{protocol_id}' liquids: [] save: @@ -237,6 +238,7 @@ stages: createdAt: !re_search "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d+\\+\\d{2}:\\d{2}$" startedAt: !re_search "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d+\\+\\d{2}:\\d{2}$" liquids: [] + runTimeParameters: [] completedAt: !re_search "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d+\\+\\d{2}:\\d{2}$" errors: [] pipettes: [] diff --git a/robot-server/tests/integration/http_api/runs/test_run_queued_protocol_commands.tavern.yaml b/robot-server/tests/integration/http_api/runs/test_run_queued_protocol_commands.tavern.yaml index cc8cea69356..0d4a0010281 100644 --- a/robot-server/tests/integration/http_api/runs/test_run_queued_protocol_commands.tavern.yaml +++ b/robot-server/tests/integration/http_api/runs/test_run_queued_protocol_commands.tavern.yaml @@ -94,6 +94,7 @@ stages: labware: [] labwareOffsets: [] liquids: [] + runTimeParameters: [] modules: [] pipettes: [] status: 'idle' diff --git a/robot-server/tests/integration/http_api/runs/test_run_with_run_time_parameters.tavern.yaml b/robot-server/tests/integration/http_api/runs/test_run_with_run_time_parameters.tavern.yaml new file mode 100644 index 00000000000..d7f075b18cb --- /dev/null +++ b/robot-server/tests/integration/http_api/runs/test_run_with_run_time_parameters.tavern.yaml @@ -0,0 +1,203 @@ +test_name: Test the run endpoints with run time parameters + +marks: + - usefixtures: + - ot2_server_base_url + +stages: + - name: Upload a protocol + request: + url: '{ot2_server_base_url}/protocols' + method: POST + files: + files: 'tests/integration/protocols/basic_transfer_with_run_time_parameters.py' + response: + status_code: 201 + save: + json: + protocol_id: data.id + + - name: Create run from protocol + request: + url: '{ot2_server_base_url}/runs' + method: POST + json: + data: + protocolId: '{protocol_id}' + runTimeParameterValues: + sample_count: 4 + volume: 10.23 + dry_run: True + pipette: flex_8channel_50 + response: + status_code: 201 + save: + json: + run_id: data.id + json: + data: + id: !anystr + ok: True + createdAt: !anystr + status: idle + current: True + actions: [] + errors: [] + pipettes: [] + modules: [] + labware: [] + labwareOffsets: [] + runTimeParameters: [] + liquids: [] + protocolId: '{protocol_id}' + + - name: Play the run + request: + url: '{ot2_server_base_url}/runs/{run_id}/actions' + method: POST + json: + data: + actionType: play + response: + status_code: 201 + json: + data: + id: !anystr + actionType: play + createdAt: !anystr + + - name: Wait for the protocol to complete + max_retries: 10 + delay_after: 0.1 + request: + url: '{ot2_server_base_url}/runs/{run_id}' + method: GET + response: + status_code: 200 + strict: + - json:off + json: + data: + status: succeeded + + - name: Verify the run contains the set run time parameters + request: + url: '{ot2_server_base_url}/runs/{run_id}' + method: GET + response: + status_code: 200 + strict: + - json:off + json: + data: + id: !anystr + ok: True + createdAt: !anystr + status: succeeded + current: True + runTimeParameters: + - displayName: Sample count + variableName: sample_count + type: int + default: 6.0 + min: 1.0 + max: 12.0 + value: 4.0 + description: How many samples to process. + - displayName: Pipette volume + variableName: volume + type: float + default: 20.1 + choices: + - displayName: Low Volume + value: 10.23 + - displayName: Medium Volume + value: 20.1 + - displayName: High Volume + value: 50.5 + value: 10.23 + description: How many microliters to pipette of each sample. + - displayName: Dry Run + variableName: dry_run + type: bool + default: false + value: true + description: Skip aspirate and dispense steps. + - displayName: Pipette Name + variableName: pipette + type: str + choices: + - displayName: Single channel 50µL + value: flex_1channel_50 + - displayName: Eight Channel 50µL + value: flex_8channel_50 + default: flex_1channel_50 + value: flex_8channel_50 + description: What pipette to use during the protocol. + protocolId: '{protocol_id}' + + - name: Mark the run as not-current + request: + url: '{ot2_server_base_url}/runs/{run_id}' + method: PATCH + json: + data: + current: False + response: + status_code: 200 + + - name: Verify the archived run still contains the set run time parameters + request: + url: '{ot2_server_base_url}/runs/{run_id}' + method: GET + response: + status_code: 200 + strict: + - json:off + json: + data: + id: !anystr + ok: True + createdAt: !anystr + status: succeeded + current: False + runTimeParameters: + - displayName: Sample count + variableName: sample_count + type: int + default: 6.0 + min: 1.0 + max: 12.0 + value: 4.0 + description: How many samples to process. + - displayName: Pipette volume + variableName: volume + type: float + default: 20.1 + choices: + - displayName: Low Volume + value: 10.23 + - displayName: Medium Volume + value: 20.1 + - displayName: High Volume + value: 50.5 + value: 10.23 + description: How many microliters to pipette of each sample. + - displayName: Dry Run + variableName: dry_run + type: bool + default: false + value: true + description: Skip aspirate and dispense steps. + - displayName: Pipette Name + variableName: pipette + type: str + choices: + - displayName: Single channel 50µL + value: flex_1channel_50 + - displayName: Eight Channel 50µL + value: flex_8channel_50 + default: flex_1channel_50 + value: flex_8channel_50 + description: What pipette to use during the protocol. + protocolId: '{protocol_id}' diff --git a/robot-server/tests/persistence/test_tables.py b/robot-server/tests/persistence/test_tables.py index eaa2824ce75..5f3c45adcaa 100644 --- a/robot-server/tests/persistence/test_tables.py +++ b/robot-server/tests/persistence/test_tables.py @@ -56,6 +56,7 @@ state_summary VARCHAR, engine_status VARCHAR, _updated_at DATETIME, + run_time_parameters VARCHAR, PRIMARY KEY (id), FOREIGN KEY(protocol_id) REFERENCES protocol (id) ) diff --git a/robot-server/tests/runs/test_run_controller.py b/robot-server/tests/runs/test_run_controller.py index 5bf5778c486..a844cdcc6d5 100644 --- a/robot-server/tests/runs/test_run_controller.py +++ b/robot-server/tests/runs/test_run_controller.py @@ -11,6 +11,7 @@ commands as pe_commands, errors as pe_errors, ) +from opentrons.protocol_engine.types import RunTimeParameter, BooleanParameter from opentrons.protocol_runner import RunResult, JsonRunner, PythonAndLegacyRunner from robot_server.service.task_runner import TaskRunner @@ -60,6 +61,19 @@ def engine_state_summary() -> StateSummary: ) +@pytest.fixture() +def run_time_parameters() -> List[RunTimeParameter]: + """Get a RunTimeParameter list.""" + return [ + BooleanParameter( + displayName="Display Name", + variableName="variable_name", + value=False, + default=True, + ) + ] + + @pytest.fixture def protocol_commands() -> List[pe_commands.Command]: """Get a StateSummary value object.""" @@ -122,6 +136,7 @@ async def test_create_play_action_to_start( mock_run_store: RunStore, mock_task_runner: TaskRunner, engine_state_summary: StateSummary, + run_time_parameters: List[RunTimeParameter], protocol_commands: List[pe_commands.Command], run_id: str, subject: RunController, @@ -153,7 +168,7 @@ async def test_create_play_action_to_start( RunResult( commands=protocol_commands, state_summary=engine_state_summary, - parameters=[], + parameters=run_time_parameters, ) ) @@ -164,6 +179,7 @@ async def test_create_play_action_to_start( run_id=run_id, summary=engine_state_summary, commands=protocol_commands, + run_time_parameters=run_time_parameters, ), times=1, ) diff --git a/robot-server/tests/runs/test_run_data_manager.py b/robot-server/tests/runs/test_run_data_manager.py index ba4ceec8799..547ec0a7b74 100644 --- a/robot-server/tests/runs/test_run_data_manager.py +++ b/robot-server/tests/runs/test_run_data_manager.py @@ -1,5 +1,5 @@ """Tests for RunDataManager.""" -from typing import Optional +from typing import Optional, List import pytest from datetime import datetime @@ -85,6 +85,19 @@ def engine_state_summary() -> StateSummary: ) +@pytest.fixture() +def run_time_parameters() -> List[pe_types.RunTimeParameter]: + """Get a RunTimeParameter list.""" + return [ + pe_types.BooleanParameter( + displayName="Display Name", + variableName="variable_name", + value=False, + default=True, + ) + ] + + @pytest.fixture def run_resource() -> RunResource: """Get a StateSummary value object.""" @@ -299,6 +312,7 @@ async def test_get_current_run( mock_run_store: RunStore, subject: RunDataManager, engine_state_summary: StateSummary, + run_time_parameters: List[pe_types.RunTimeParameter], run_resource: RunResource, ) -> None: """It should get the current run from the engine.""" @@ -309,6 +323,9 @@ async def test_get_current_run( decoy.when(mock_engine_store.engine.state_view.get_summary()).then_return( engine_state_summary ) + decoy.when(mock_engine_store.runner.run_time_parameters).then_return( + run_time_parameters + ) result = subject.get(run_id=run_id) @@ -325,6 +342,7 @@ async def test_get_current_run( pipettes=engine_state_summary.pipettes, modules=engine_state_summary.modules, liquids=engine_state_summary.liquids, + runTimeParameters=run_time_parameters, ) assert subject.current_run_id == run_id @@ -335,6 +353,7 @@ async def test_get_historical_run( mock_run_store: RunStore, subject: RunDataManager, engine_state_summary: StateSummary, + run_time_parameters: List[pe_types.RunTimeParameter], run_resource: RunResource, ) -> None: """It should get a historical run from the store.""" @@ -344,6 +363,9 @@ async def test_get_historical_run( decoy.when(mock_run_store.get_state_summary(run_id=run_id)).then_return( engine_state_summary ) + decoy.when(mock_run_store.get_run_time_parameters(run_id=run_id)).then_return( + run_time_parameters + ) decoy.when(mock_engine_store.current_run_id).then_return("some other id") result = subject.get(run_id=run_id) @@ -361,6 +383,7 @@ async def test_get_historical_run( pipettes=engine_state_summary.pipettes, modules=engine_state_summary.modules, liquids=engine_state_summary.liquids, + runTimeParameters=run_time_parameters, ) @@ -370,6 +393,7 @@ async def test_get_historical_run_no_data( mock_run_store: RunStore, subject: RunDataManager, run_resource: RunResource, + run_time_parameters: List[pe_types.RunTimeParameter], ) -> None: """It should get a historical run from the store.""" run_id = "hello world" @@ -380,6 +404,9 @@ async def test_get_historical_run_no_data( decoy.when(mock_run_store.get_state_summary(run_id=run_id)).then_return( BadStateSummary(dataError=state_exc) ) + decoy.when(mock_run_store.get_run_time_parameters(run_id=run_id)).then_return( + run_time_parameters + ) decoy.when(mock_engine_store.current_run_id).then_return("some other id") result = subject.get(run_id=run_id) @@ -398,6 +425,7 @@ async def test_get_historical_run_no_data( pipettes=[], modules=[], liquids=[], + runTimeParameters=run_time_parameters, ) @@ -417,6 +445,14 @@ async def test_get_all_runs( modules=[LoadedModule.construct(id="current-module-id")], # type: ignore[call-arg] liquids=[Liquid(id="some-liquid-id", displayName="liquid", description="desc")], ) + current_run_time_parameters: List[pe_types.RunTimeParameter] = [ + pe_types.BooleanParameter( + displayName="Current Bool", + variableName="current bool", + value=False, + default=True, + ) + ] historical_run_data = StateSummary( status=EngineStatus.STOPPED, @@ -427,6 +463,14 @@ async def test_get_all_runs( modules=[LoadedModule.construct(id="old-module-id")], # type: ignore[call-arg] liquids=[], ) + historical_run_time_parameters: List[pe_types.RunTimeParameter] = [ + pe_types.BooleanParameter( + displayName="Old Bool", + variableName="Old bool", + value=True, + default=False, + ) + ] current_run_resource = RunResource( ok=True, @@ -448,9 +492,15 @@ async def test_get_all_runs( decoy.when(mock_engine_store.engine.state_view.get_summary()).then_return( current_run_data ) + decoy.when(mock_engine_store.runner.run_time_parameters).then_return( + current_run_time_parameters + ) decoy.when(mock_run_store.get_state_summary("historical-run")).then_return( historical_run_data ) + decoy.when(mock_run_store.get_run_time_parameters("historical-run")).then_return( + historical_run_time_parameters + ) decoy.when(mock_run_store.get_all(length=20)).then_return( [historical_run_resource, current_run_resource] ) @@ -471,6 +521,7 @@ async def test_get_all_runs( pipettes=historical_run_data.pipettes, modules=historical_run_data.modules, liquids=historical_run_data.liquids, + runTimeParameters=historical_run_time_parameters, ), Run( current=True, @@ -485,6 +536,7 @@ async def test_get_all_runs( pipettes=current_run_data.pipettes, modules=current_run_data.modules, liquids=current_run_data.liquids, + runTimeParameters=current_run_time_parameters, ), ] @@ -526,6 +578,7 @@ async def test_delete_historical_run( async def test_update_current( decoy: Decoy, engine_state_summary: StateSummary, + run_time_parameters: List[pe_types.RunTimeParameter], run_resource: RunResource, run_command: commands.Command, mock_engine_store: EngineStore, @@ -537,7 +590,9 @@ async def test_update_current( decoy.when(mock_engine_store.current_run_id).then_return(run_id) decoy.when(await mock_engine_store.clear()).then_return( RunResult( - commands=[run_command], state_summary=engine_state_summary, parameters=[] + commands=[run_command], + state_summary=engine_state_summary, + parameters=run_time_parameters, ) ) @@ -546,6 +601,7 @@ async def test_update_current( run_id=run_id, summary=engine_state_summary, commands=[run_command], + run_time_parameters=run_time_parameters, ) ).then_return(run_resource) @@ -564,6 +620,7 @@ async def test_update_current( pipettes=engine_state_summary.pipettes, modules=engine_state_summary.modules, liquids=engine_state_summary.liquids, + runTimeParameters=run_time_parameters, ) @@ -571,6 +628,7 @@ async def test_update_current( async def test_update_current_noop( decoy: Decoy, engine_state_summary: StateSummary, + run_time_parameters: List[pe_types.RunTimeParameter], run_resource: RunResource, run_command: commands.Command, mock_engine_store: EngineStore, @@ -584,6 +642,9 @@ async def test_update_current_noop( decoy.when(mock_engine_store.engine.state_view.get_summary()).then_return( engine_state_summary ) + decoy.when(mock_engine_store.runner.run_time_parameters).then_return( + run_time_parameters + ) decoy.when(mock_run_store.get(run_id=run_id)).then_return(run_resource) result = await subject.update(run_id=run_id, current=current) @@ -594,6 +655,7 @@ async def test_update_current_noop( run_id=run_id, summary=matchers.Anything(), commands=matchers.Anything(), + run_time_parameters=matchers.Anything(), ), times=0, ) @@ -611,6 +673,7 @@ async def test_update_current_noop( pipettes=engine_state_summary.pipettes, modules=engine_state_summary.modules, liquids=engine_state_summary.liquids, + runTimeParameters=run_time_parameters, ) @@ -634,6 +697,7 @@ async def test_update_current_not_allowed( async def test_create_archives_existing( decoy: Decoy, engine_state_summary: StateSummary, + run_time_parameters: List[pe_types.RunTimeParameter], run_resource: RunResource, run_command: commands.Command, mock_engine_store: EngineStore, @@ -647,7 +711,9 @@ async def test_create_archives_existing( decoy.when(mock_engine_store.current_run_id).then_return(run_id_old) decoy.when(await mock_engine_store.clear()).then_return( RunResult( - commands=[run_command], state_summary=engine_state_summary, parameters=[] + commands=[run_command], + state_summary=engine_state_summary, + parameters=run_time_parameters, ) ) @@ -685,6 +751,7 @@ async def test_create_archives_existing( run_id=run_id_old, summary=engine_state_summary, commands=[run_command], + run_time_parameters=run_time_parameters, ) ) diff --git a/robot-server/tests/runs/test_run_store.py b/robot-server/tests/runs/test_run_store.py index 31cabbe56bd..c6108cf5407 100644 --- a/robot-server/tests/runs/test_run_store.py +++ b/robot-server/tests/runs/test_run_store.py @@ -120,6 +120,41 @@ def state_summary() -> StateSummary: ) +@pytest.fixture() +def run_time_parameters() -> List[pe_types.RunTimeParameter]: + """Get a RunTimeParameter list.""" + return [ + pe_types.BooleanParameter( + displayName="Display Name 1", + variableName="variable_name_1", + value=False, + default=True, + ), + pe_types.NumberParameter( + displayName="Display Name 2", + variableName="variable_name_2", + type="int", + min=123.0, + max=456.0, + value=333.0, + default=222.0, + ), + pe_types.EnumParameter( + displayName="Display Name 3", + variableName="variable_name_3", + type="str", + choices=[ + pe_types.EnumChoice( + displayName="Choice Name", + value="cool choice", + ) + ], + default="cooler choice", + value="coolest choice", + ), + ] + + @pytest.fixture def invalid_state_summary() -> StateSummary: """Should fail pydantic validation.""" @@ -164,6 +199,7 @@ def test_update_run_state( subject: RunStore, state_summary: StateSummary, protocol_commands: List[pe_commands.Command], + run_time_parameters: List[pe_types.RunTimeParameter], mock_runs_publisher: mock.Mock, ) -> None: """It should be able to update a run state to the store.""" @@ -184,8 +220,10 @@ def test_update_run_state( run_id="run-id", summary=state_summary, commands=protocol_commands, + run_time_parameters=run_time_parameters, ) run_summary_result = subject.get_state_summary(run_id="run-id") + parameters_result = subject.get_run_time_parameters(run_id="run-id") commands_result = subject.get_commands_slice( run_id="run-id", length=len(protocol_commands), @@ -200,6 +238,7 @@ def test_update_run_state( actions=[action], ) assert run_summary_result == state_summary + assert parameters_result == run_time_parameters assert commands_result.commands == protocol_commands mock_runs_publisher.publish_runs_advise_refetch.assert_called_once_with( run_id="run-id" @@ -217,6 +256,7 @@ def test_update_state_run_not_found( run_id="run-not-found", summary=state_summary, commands=protocol_commands, + run_time_parameters=[], ) @@ -436,7 +476,9 @@ def test_get_state_summary( protocol_id=None, created_at=datetime(year=2021, month=1, day=1, tzinfo=timezone.utc), ) - subject.update_run_state(run_id="run-id", summary=state_summary, commands=[]) + subject.update_run_state( + run_id="run-id", summary=state_summary, commands=[], run_time_parameters=[] + ) result = subject.get_state_summary(run_id="run-id") assert result == state_summary mock_runs_publisher.publish_runs_advise_refetch.assert_called_once_with( @@ -454,7 +496,10 @@ def test_get_state_summary_failure( created_at=datetime(year=2021, month=1, day=1, tzinfo=timezone.utc), ) subject.update_run_state( - run_id="run-id", summary=invalid_state_summary, commands=[] + run_id="run-id", + summary=invalid_state_summary, + commands=[], + run_time_parameters=[], ) result = subject.get_state_summary(run_id="run-id") assert isinstance(result, BadStateSummary) @@ -473,6 +518,62 @@ def test_get_state_summary_none(subject: RunStore) -> None: assert result.dataError.code == ErrorCodes.INVALID_STORED_DATA +def test_get_run_time_parameters( + subject: RunStore, + state_summary: StateSummary, + run_time_parameters: List[pe_types.RunTimeParameter], +) -> None: + """It should be able to get store run time parameters.""" + subject.insert( + run_id="run-id", + protocol_id=None, + created_at=datetime(year=2021, month=1, day=1, tzinfo=timezone.utc), + ) + subject.update_run_state( + run_id="run-id", + summary=state_summary, + commands=[], + run_time_parameters=run_time_parameters, + ) + result = subject.get_run_time_parameters(run_id="run-id") + assert result == run_time_parameters + + +def test_get_run_time_parameters_invalid( + subject: RunStore, + state_summary: StateSummary, +) -> None: + """It should return an empty list if there invalid parameters.""" + bad_parameters = [pe_types.BooleanParameter.construct(foo="bar")] # type: ignore[call-arg] + subject.insert( + run_id="run-id", + protocol_id=None, + created_at=datetime(year=2021, month=1, day=1, tzinfo=timezone.utc), + ) + subject.update_run_state( + run_id="run-id", + summary=state_summary, + commands=[], + run_time_parameters=bad_parameters, # type: ignore[arg-type] + ) + result = subject.get_run_time_parameters(run_id="run-id") + assert result == [] + + +def test_get_run_time_parameters_none( + subject: RunStore, + state_summary: StateSummary, +) -> None: + """It should return an empty list if there are no run time parameters associated.""" + subject.insert( + run_id="run-id", + protocol_id=None, + created_at=datetime(year=2021, month=1, day=1, tzinfo=timezone.utc), + ) + result = subject.get_run_time_parameters(run_id="run-id") + assert result == [] + + def test_has_run_id(subject: RunStore) -> None: """It should tell us if a given ID is in the store.""" subject.insert( @@ -503,6 +604,7 @@ def test_get_command( run_id="run-id", summary=state_summary, commands=protocol_commands, + run_time_parameters=[], ) result = subject.get_command(run_id="run-id", command_id="pause-2") @@ -532,6 +634,7 @@ def test_get_command_raise_exception( run_id="run-id", summary=state_summary, commands=protocol_commands, + run_time_parameters=[], ) with pytest.raises(expected_exception): subject.get_command(run_id=input_run_id, command_id=input_command_id) @@ -552,6 +655,7 @@ def test_get_command_slice( run_id="run-id", summary=state_summary, commands=protocol_commands, + run_time_parameters=[], ) result = subject.get_commands_slice( run_id="run-id", cursor=0, length=len(protocol_commands) @@ -598,6 +702,7 @@ def test_get_commands_slice_clamping( run_id="run-id", summary=state_summary, commands=protocol_commands, + run_time_parameters=[], ) result = subject.get_commands_slice( run_id="run-id", cursor=input_cursor, length=input_length From 80222d9b5c0846c585231957da234a61e97980c2 Mon Sep 17 00:00:00 2001 From: Jamey Huffnagle Date: Fri, 12 Apr 2024 10:13:52 -0400 Subject: [PATCH 103/194] feat(app): add generic run paused splash screen (#14873) Closes EXEC-387 --- .../localization/en/error_recovery.json | 3 + app/src/assets/localization/en/index.ts | 2 + .../RunningProtocol/RunPausedSplash.tsx | 94 +++++++ .../__tests__/RunPausedSplash.test.tsx | 51 ++++ .../__tests__/RunningProtocol.test.tsx | 18 ++ app/src/pages/RunningProtocol/index.tsx | 237 ++++++++++-------- 6 files changed, 302 insertions(+), 103 deletions(-) create mode 100644 app/src/assets/localization/en/error_recovery.json create mode 100644 app/src/organisms/OnDeviceDisplay/RunningProtocol/RunPausedSplash.tsx create mode 100644 app/src/organisms/OnDeviceDisplay/RunningProtocol/__tests__/RunPausedSplash.test.tsx diff --git a/app/src/assets/localization/en/error_recovery.json b/app/src/assets/localization/en/error_recovery.json new file mode 100644 index 00000000000..7531853df16 --- /dev/null +++ b/app/src/assets/localization/en/error_recovery.json @@ -0,0 +1,3 @@ +{ + "run_paused": "Run paused" +} diff --git a/app/src/assets/localization/en/index.ts b/app/src/assets/localization/en/index.ts index c7256b1d415..8c865445056 100644 --- a/app/src/assets/localization/en/index.ts +++ b/app/src/assets/localization/en/index.ts @@ -29,6 +29,7 @@ import robot_controls from './robot_controls.json' import robot_info from './robot_info.json' import run_details from './run_details.json' import top_navigation from './top_navigation.json' +import error_recovery from './error_recovery.json' export const en = { shared, @@ -62,4 +63,5 @@ export const en = { robot_info, run_details, top_navigation, + error_recovery, } diff --git a/app/src/organisms/OnDeviceDisplay/RunningProtocol/RunPausedSplash.tsx b/app/src/organisms/OnDeviceDisplay/RunningProtocol/RunPausedSplash.tsx new file mode 100644 index 00000000000..529e5b6653f --- /dev/null +++ b/app/src/organisms/OnDeviceDisplay/RunningProtocol/RunPausedSplash.tsx @@ -0,0 +1,94 @@ +import * as React from 'react' +import styled from 'styled-components' +import { useTranslation } from 'react-i18next' + +import { + Flex, + Btn, + Icon, + JUSTIFY_CENTER, + ALIGN_CENTER, + SPACING, + COLORS, + DIRECTION_COLUMN, + POSITION_ABSOLUTE, + TYPOGRAPHY, + OVERFLOW_WRAP_BREAK_WORD, + DISPLAY_FLEX, +} from '@opentrons/components' + +interface RunPausedSplashProps { + onClose: () => void + errorType?: string + protocolName?: string +} + +export function RunPausedSplash({ + onClose, + errorType, + protocolName, +}: RunPausedSplashProps): JSX.Element { + const { t } = useTranslation('error_recovery') + + let subText: string | null + switch (errorType) { + default: + subText = protocolName ?? null + } + + return ( + + + + + {t('run_paused')} + + + {subText} + + + + ) +} + +const SplashHeader = styled.h1` + font-weight: ${TYPOGRAPHY.fontWeightBold}; + text-align: ${TYPOGRAPHY.textAlignLeft}; + font-size: ${TYPOGRAPHY.fontSize80}; + line-height: ${TYPOGRAPHY.lineHeight96}; + color: ${COLORS.white}; +` +const SplashBody = styled.h4` + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 4; + overflow: hidden; + overflow-wrap: ${OVERFLOW_WRAP_BREAK_WORD}; + font-weight: ${TYPOGRAPHY.fontWeightSemiBold}; + text-align: ${TYPOGRAPHY.textAlignCenter}; + text-transform: ${TYPOGRAPHY.textTransformCapitalize}; + font-size: ${TYPOGRAPHY.fontSize32}; + line-height: ${TYPOGRAPHY.lineHeight42}; + color: ${COLORS.white}; +` + +const SplashFrame = styled(Flex)` + width: 100%; + height: 100%; + flex-direction: ${DIRECTION_COLUMN}; + justify-content: ${JUSTIFY_CENTER}; + align-items: ${ALIGN_CENTER}; + grid-gap: ${SPACING.spacing40}; +` diff --git a/app/src/organisms/OnDeviceDisplay/RunningProtocol/__tests__/RunPausedSplash.test.tsx b/app/src/organisms/OnDeviceDisplay/RunningProtocol/__tests__/RunPausedSplash.test.tsx new file mode 100644 index 00000000000..6a7061346a4 --- /dev/null +++ b/app/src/organisms/OnDeviceDisplay/RunningProtocol/__tests__/RunPausedSplash.test.tsx @@ -0,0 +1,51 @@ +import * as React from 'react' +import { MemoryRouter } from 'react-router-dom' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { fireEvent, screen } from '@testing-library/react' +import { renderWithProviders } from '../../../../__testing-utils__' +import { i18n } from '../../../../i18n' + +import { COLORS } from '@opentrons/components' + +import { RunPausedSplash } from '../RunPausedSplash' + +const render = (props: React.ComponentProps) => { + return renderWithProviders( + + + , + { + i18nInstance: i18n, + } + ) +} + +const MOCK_PROTOCOL_NAME = 'MOCK_PROTOCOL' + +describe('ConfirmCancelRunModal', () => { + let props: React.ComponentProps + const mockOnClose = vi.fn() + + beforeEach(() => { + props = { + onClose: mockOnClose, + protocolName: MOCK_PROTOCOL_NAME, + errorType: '', + } + }) + + afterEach(() => { + vi.restoreAllMocks() + }) + + it('should render a generic paused screen if there is no errorType', () => { + render(props) + expect(screen.getByText('Run paused')).toBeInTheDocument() + expect(screen.getByText(MOCK_PROTOCOL_NAME)) + expect(screen.getByRole('button')).toHaveStyle({ + 'background-color': COLORS.grey50, + }) + fireEvent.click(screen.getByRole('button')) + expect(mockOnClose).toHaveBeenCalled() + }) +}) diff --git a/app/src/pages/RunningProtocol/__tests__/RunningProtocol.test.tsx b/app/src/pages/RunningProtocol/__tests__/RunningProtocol.test.tsx index 32f87a8047c..2b43991a88f 100644 --- a/app/src/pages/RunningProtocol/__tests__/RunningProtocol.test.tsx +++ b/app/src/pages/RunningProtocol/__tests__/RunningProtocol.test.tsx @@ -7,6 +7,7 @@ import { RUN_STATUS_BLOCKED_BY_OPEN_DOOR, RUN_STATUS_IDLE, RUN_STATUS_STOP_REQUESTED, + RUN_STATUS_AWAITING_RECOVERY, } from '@opentrons/api-client' import { useAllCommandsQuery, @@ -30,12 +31,14 @@ import { getLocalRobot } from '../../../redux/discovery' import { CancelingRunModal } from '../../../organisms/OnDeviceDisplay/RunningProtocol/CancelingRunModal' import { useTrackProtocolRunEvent } from '../../../organisms/Devices/hooks' import { useMostRecentCompletedAnalysis } from '../../../organisms/LabwarePositionCheck/useMostRecentCompletedAnalysis' +import { RunPausedSplash } from '../../../organisms/OnDeviceDisplay/RunningProtocol/RunPausedSplash' import { OpenDoorAlertModal } from '../../../organisms/OpenDoorAlertModal' import { RunningProtocol } from '..' import { useNotifyLastRunCommandKey, useNotifyRunQuery, } from '../../../resources/runs' +import { useFeatureFlag } from '../../../redux/config' import type { UseQueryResult } from 'react-query' import type { ProtocolAnalyses } from '@opentrons/api-client' @@ -47,12 +50,15 @@ vi.mock('../../../organisms/RunTimeControl/hooks') vi.mock( '../../../organisms/LabwarePositionCheck/useMostRecentCompletedAnalysis' ) +vi.mock('../../../organisms/OnDeviceDisplay/RunningProtocol/RunPausedSplash') vi.mock('../../../organisms/RunTimeControl/hooks') vi.mock('../../../organisms/OnDeviceDisplay/RunningProtocol') vi.mock('../../../redux/discovery') vi.mock('../../../organisms/OnDeviceDisplay/RunningProtocol/CancelingRunModal') vi.mock('../../../organisms/OpenDoorAlertModal') vi.mock('../../../resources/runs') +vi.mock('../../../redux/config') + const RUN_ID = 'run_id' const ROBOT_NAME = 'otie' const PROTOCOL_ID = 'protocol_id' @@ -85,6 +91,7 @@ describe('RunningProtocol', () => { data: { id: RUN_ID, protocolId: PROTOCOL_ID, + errors: [], }, }, } as any) @@ -133,6 +140,9 @@ describe('RunningProtocol', () => { vi.mocked(useNotifyLastRunCommandKey).mockReturnValue({ data: {}, } as any) + when(vi.mocked(useFeatureFlag)) + .calledWith('enableRunNotes') + .thenReturn(true) }) afterEach(() => { @@ -166,6 +176,14 @@ describe('RunningProtocol', () => { expect(vi.mocked(OpenDoorAlertModal)).toHaveBeenCalled() }) + it(`should display a Run Paused splash screen if the run status is "${RUN_STATUS_AWAITING_RECOVERY}"`, () => { + when(vi.mocked(useRunStatus)) + .calledWith(RUN_ID, { refetchInterval: 5000 }) + .thenReturn(RUN_STATUS_AWAITING_RECOVERY) + render(`/runs/${RUN_ID}/run`) + expect(vi.mocked(RunPausedSplash)).toHaveBeenCalled() + }) + // ToDo (kj:04/04/2023) need to figure out the way to simulate swipe it.todo('should render RunningProtocolCommandList when swiping left') // const [{ getByText }] = render(`/runs/${RUN_ID}/run`) diff --git a/app/src/pages/RunningProtocol/index.tsx b/app/src/pages/RunningProtocol/index.tsx index 2fc56806679..3ebe3b1c0ab 100644 --- a/app/src/pages/RunningProtocol/index.tsx +++ b/app/src/pages/RunningProtocol/index.tsx @@ -25,8 +25,10 @@ import { import { RUN_STATUS_STOP_REQUESTED, RUN_STATUS_BLOCKED_BY_OPEN_DOOR, + RUN_STATUS_AWAITING_RECOVERY, } from '@opentrons/api-client' +import { useFeatureFlag } from '../../redux/config' import { StepMeter } from '../../atoms/StepMeter' import { useMostRecentCompletedAnalysis } from '../../organisms/LabwarePositionCheck/useMostRecentCompletedAnalysis' import { @@ -51,6 +53,7 @@ import { } from '../../organisms/Devices/hooks' import { CancelingRunModal } from '../../organisms/OnDeviceDisplay/RunningProtocol/CancelingRunModal' import { ConfirmCancelRunModal } from '../../organisms/OnDeviceDisplay/RunningProtocol/ConfirmCancelRunModal' +import { RunPausedSplash } from '../../organisms/OnDeviceDisplay/RunningProtocol/RunPausedSplash' import { getLocalRobot } from '../../redux/discovery' import { OpenDoorAlertModal } from '../../organisms/OpenDoorAlertModal' @@ -102,6 +105,7 @@ export function RunningProtocol(): JSX.Element { const runStatus = useRunStatus(runId, { refetchInterval: RUN_STATUS_REFETCH_INTERVAL, }) + const [enableSplash, setEnableSplash] = React.useState(true) const { startedAt, stoppedAt, completedAt } = useRunTimestamps(runId) const { data: runRecord } = useNotifyRunQuery(runId, { staleTime: Infinity }) const protocolId = runRecord?.data.protocolId ?? null @@ -117,6 +121,10 @@ export function RunningProtocol(): JSX.Element { const { trackProtocolRunEvent } = useTrackProtocolRunEvent(runId, robotName) const robotAnalyticsData = useRobotAnalyticsData(robotName) const robotType = useRobotType(robotName) + const errorType = runRecord?.data.errors[0]?.errorType + + const enableRunNotes = useFeatureFlag('enableRunNotes') + React.useEffect(() => { if ( currentOption === 'CurrentRunningProtocolCommand' && @@ -160,114 +168,137 @@ export function RunningProtocol(): JSX.Element { return ( <> - {runStatus === RUN_STATUS_BLOCKED_BY_OPEN_DOOR ? ( - - ) : null} - {runStatus === RUN_STATUS_STOP_REQUESTED ? : null} - - {robotSideAnalysis != null ? ( - - ) : null} - {showConfirmCancelRunModal ? ( - - ) : null} - {interventionModalCommandKey != null && - runRecord?.data != null && - lastRunCommand != null && - isInterventionCommand(lastRunCommand) ? ( - - ) : null} - - {robotSideAnalysis != null ? ( - currentOption === 'CurrentRunningProtocolCommand' ? ( - - (lastAnimatedCommand.current = newCommandKey) + {enableSplash && + runStatus === RUN_STATUS_AWAITING_RECOVERY && + enableRunNotes ? ( + setEnableSplash(false)} + errorType={errorType} + protocolName={protocolName} + /> + ) : ( + <> + {runStatus === RUN_STATUS_BLOCKED_BY_OPEN_DOOR ? ( + + ) : null} + {runStatus === RUN_STATUS_STOP_REQUESTED ? ( + + ) : null} + + {robotSideAnalysis != null ? ( + - ) : ( - <> - + ) : null} + {interventionModalCommandKey != null && + runRecord?.data != null && + lastRunCommand != null && + isInterventionCommand(lastRunCommand) ? ( + + ) : null} + + {robotSideAnalysis != null ? ( + currentOption === 'CurrentRunningProtocolCommand' ? ( + + (lastAnimatedCommand.current = newCommandKey) + } + /> + ) : ( + <> + + + + ) + ) : ( + + )} + + - - - ) - ) : ( - - )} - - - + + - - + + )} ) } From fd6a5b2a03e221f2f1c1d3df9cf0ce056ec0e53f Mon Sep 17 00:00:00 2001 From: Jamey Huffnagle Date: Fri, 12 Apr 2024 10:22:00 -0400 Subject: [PATCH 104/194] fix(app): fix hepa/uv firmware copy (#14881) Closes RQA-2561 --- api-client/src/subsystems/types.ts | 1 + app/src/assets/localization/en/firmware_update.json | 1 + .../__tests__/UpdateInProgressModal.test.tsx | 9 ++++++++- 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/api-client/src/subsystems/types.ts b/api-client/src/subsystems/types.ts index 14f45324f62..564d59b21b2 100644 --- a/api-client/src/subsystems/types.ts +++ b/api-client/src/subsystems/types.ts @@ -6,6 +6,7 @@ export type Subsystem = | 'pipette_right' | 'gripper' | 'rear_panel' + | 'hepa_uv' type UpdateStatus = 'queued' | 'updating' | 'done' export interface SubsystemUpdateProgressData { diff --git a/app/src/assets/localization/en/firmware_update.json b/app/src/assets/localization/en/firmware_update.json index 8abe122d914..0540963084b 100644 --- a/app/src/assets/localization/en/firmware_update.json +++ b/app/src/assets/localization/en/firmware_update.json @@ -5,6 +5,7 @@ "gantry_y": "Gantry Y", "gripper": "Gripper", "head": "Head", + "hepa_uv": "HEPA/UV Module", "pipette_left": "pipette", "pipette_right": "pipette", "ready_to_use": "Your {{instrument}} is ready to use!", diff --git a/app/src/organisms/FirmwareUpdateModal/__tests__/UpdateInProgressModal.test.tsx b/app/src/organisms/FirmwareUpdateModal/__tests__/UpdateInProgressModal.test.tsx index 818b8ce341e..c08bef6b4ea 100644 --- a/app/src/organisms/FirmwareUpdateModal/__tests__/UpdateInProgressModal.test.tsx +++ b/app/src/organisms/FirmwareUpdateModal/__tests__/UpdateInProgressModal.test.tsx @@ -18,8 +18,15 @@ describe('UpdateInProgressModal', () => { subsystem: 'pipette_right', } }) - it('renders text', () => { + it('renders pipette text', () => { const { getByText } = render(props) getByText('Updating pipette firmware...') }) + it('renders Hepa/UV text', () => { + props = { + subsystem: 'hepa_uv', + } + const { getByText } = render(props) + getByText('Updating HEPA/UV Module firmware...') + }) }) From 15782add75b00d8aa26584e5d6bbce1ed2576df7 Mon Sep 17 00:00:00 2001 From: Max Marrone Date: Fri, 12 Apr 2024 10:23:14 -0400 Subject: [PATCH 105/194] refactor(protocol-engine): Rename stop() and pause() -> request_stop() and request_pause() (#14879) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Overview This fixes something that keeps confusing me as I work on EXEC-382. Various things state that `ProtocolEngine.stop()` takes effect immediately—meaning, to me, that the robot's motion is stopped immediately, the protocol exits immediately, and the HTTP run is marked as `stopped` immediately. This does not seem true. It merely puts the run into a `stop-requested` state, which only later settles into a `stopped` state. This PR adjusts some docstrings and renames `stop()` to ~~`stop_soon()`~~ `request_stop()`. ~~The name `stop_soon()` is inspired by asyncio and anyio's `call_soon()`.~~ `pause()` has the same caveat, so it's renamed to `request_pause()` for consistency. # Test plan None needed. # Review requests * ~~Taking for granted, for a moment, that the `ProtocolEngine` interface has to work like this: is `stop_soon()` a good name? Maybe `request_stop()` would be better?~~ Done. * ~~`pause()` has the same caveat. Do we want to rename that too, for consistency?~~ Done. # Risk assessment No risk. --- .../protocol_engine/actions/actions.py | 5 +---- .../protocol_engine/protocol_engine.py | 21 +++++++++++++------ .../protocol_runner/protocol_runner.py | 4 ++-- .../protocol_engine/test_protocol_engine.py | 6 +++--- .../protocol_runner/test_protocol_runner.py | 4 ++-- 5 files changed, 23 insertions(+), 17 deletions(-) diff --git a/api/src/opentrons/protocol_engine/actions/actions.py b/api/src/opentrons/protocol_engine/actions/actions.py index ee36e76f7de..2d46f614ec3 100644 --- a/api/src/opentrons/protocol_engine/actions/actions.py +++ b/api/src/opentrons/protocol_engine/actions/actions.py @@ -55,10 +55,7 @@ class PauseAction: @dataclass(frozen=True) class StopAction: - """Stop the current engine execution. - - After a StopAction, the engine status will be marked as stopped. - """ + """Request engine execution to stop soon.""" from_estop: bool = False diff --git a/api/src/opentrons/protocol_engine/protocol_engine.py b/api/src/opentrons/protocol_engine/protocol_engine.py index bd995f4339a..7389078343d 100644 --- a/api/src/opentrons/protocol_engine/protocol_engine.py +++ b/api/src/opentrons/protocol_engine/protocol_engine.py @@ -159,8 +159,12 @@ def play(self, deck_configuration: Optional[DeckConfigurationType] = None) -> No else: self._hardware_api.resume(HardwarePauseType.PAUSE) - def pause(self) -> None: - """Pause executing commands in the queue.""" + def request_pause(self) -> None: + """Make command execution pause soon. + + This will try to pause in the middle of the ongoing command, if there is one. + Otherwise, whenever the next command begins, the pause will happen then. + """ action = self._state_store.commands.validate_action_allowed( PauseAction(source=PauseSource.CLIENT) ) @@ -371,12 +375,17 @@ def estop( else: _log.info("estop pressed before protocol was started, taking no action.") - async def stop(self) -> None: - """Stop execution immediately, halting all motion and cancelling future commands. + async def request_stop(self) -> None: + """Make command execution stop soon. + + This will try to interrupt the ongoing command, if there is one. Future commands + are canceled. However, by the time this method returns, things may not have + settled by the time this method returns; the last command may still be + running. - After an engine has been `stop`'ed, it cannot be restarted. + After a stop has been requested, the engine cannot be restarted. - After a `stop`, you must still call `finish` to give the engine a chance + After a stop request, you must still call `finish` to give the engine a chance to clean up resources and propagate errors. """ action = self._state_store.commands.validate_action_allowed(StopAction()) diff --git a/api/src/opentrons/protocol_runner/protocol_runner.py b/api/src/opentrons/protocol_runner/protocol_runner.py index a1e88969615..9c097bbba2d 100644 --- a/api/src/opentrons/protocol_runner/protocol_runner.py +++ b/api/src/opentrons/protocol_runner/protocol_runner.py @@ -101,12 +101,12 @@ def play(self, deck_configuration: Optional[DeckConfigurationType] = None) -> No def pause(self) -> None: """Pause the run.""" - self._protocol_engine.pause() + self._protocol_engine.request_pause() async def stop(self) -> None: """Stop (cancel) the run.""" if self.was_started(): - await self._protocol_engine.stop() + await self._protocol_engine.request_stop() else: await self._protocol_engine.finish( drop_tips_after_run=False, diff --git a/api/tests/opentrons/protocol_engine/test_protocol_engine.py b/api/tests/opentrons/protocol_engine/test_protocol_engine.py index dd96b8d968a..959c9172b9e 100644 --- a/api/tests/opentrons/protocol_engine/test_protocol_engine.py +++ b/api/tests/opentrons/protocol_engine/test_protocol_engine.py @@ -515,7 +515,7 @@ def test_pause( state_store.commands.validate_action_allowed(expected_action), ).then_return(expected_action) - subject.pause() + subject.request_pause() decoy.verify( action_dispatcher.dispatch(expected_action), @@ -810,7 +810,7 @@ async def test_stop( state_store.commands.validate_action_allowed(expected_action), ).then_return(expected_action) - await subject.stop() + await subject.request_stop() decoy.verify( action_dispatcher.dispatch(expected_action), @@ -836,7 +836,7 @@ async def test_stop_for_legacy_core_protocols( decoy.when(hardware_api.is_movement_execution_taskified()).then_return(True) - await subject.stop() + await subject.request_stop() decoy.verify( action_dispatcher.dispatch(expected_action), diff --git a/api/tests/opentrons/protocol_runner/test_protocol_runner.py b/api/tests/opentrons/protocol_runner/test_protocol_runner.py index 5497e9e12ab..68e215bf3dd 100644 --- a/api/tests/opentrons/protocol_runner/test_protocol_runner.py +++ b/api/tests/opentrons/protocol_runner/test_protocol_runner.py @@ -238,7 +238,7 @@ def test_pause( """It should pause a protocol run with pause.""" subject.pause() - decoy.verify(protocol_engine.pause(), times=1) + decoy.verify(protocol_engine.request_pause(), times=1) @pytest.mark.parametrize( @@ -261,7 +261,7 @@ async def test_stop( subject.play() await subject.stop() - decoy.verify(await protocol_engine.stop(), times=1) + decoy.verify(await protocol_engine.request_stop(), times=1) @pytest.mark.parametrize( From aba93f180a0750d69ff6667abae3f86026ae62c2 Mon Sep 17 00:00:00 2001 From: koji Date: Fri, 12 Apr 2024 11:06:27 -0400 Subject: [PATCH 106/194] feat(opentron-ai-client): add Side Panel component (#14886) * feat(opentron-ai-client): add Side Panel component --- .storybook/main.js | 1 + .storybook/preview.jsx | 2 +- components/src/styles/flexbox.ts | 1 + opentrons-ai-client/README.md | 5 +- .../src/assets/images/opentrons_logo.svg | 51 +++++++++ .../localization/en/protocol_generator.json | 10 +- opentrons-ai-client/src/main.tsx | 9 +- .../molecules/SidePanel/SidePanel.stories.tsx | 21 ++++ .../SidePanel/__tests__/SidePanel.test.tsx | 48 ++++++++ .../src/molecules/SidePanel/index.tsx | 103 ++++++++++++++++++ opentrons-ai-client/src/molecules/index.ts | 1 + 11 files changed, 242 insertions(+), 10 deletions(-) create mode 100644 opentrons-ai-client/src/assets/images/opentrons_logo.svg create mode 100644 opentrons-ai-client/src/molecules/SidePanel/SidePanel.stories.tsx create mode 100644 opentrons-ai-client/src/molecules/SidePanel/__tests__/SidePanel.test.tsx create mode 100644 opentrons-ai-client/src/molecules/SidePanel/index.tsx create mode 100644 opentrons-ai-client/src/molecules/index.ts diff --git a/.storybook/main.js b/.storybook/main.js index e9fc91cdf48..985486d5d4e 100644 --- a/.storybook/main.js +++ b/.storybook/main.js @@ -2,6 +2,7 @@ module.exports = { stories: [ '../components/**/*.stories.@(js|jsx|ts|tsx)', '../app/**/*.stories.@(js|jsx|ts|tsx)', + '../opentrons-ai-client/**/*.stories.@(js|jsx|ts|tsx)', ], addons: [ diff --git a/.storybook/preview.jsx b/.storybook/preview.jsx index d8537e57827..32864c9abcb 100644 --- a/.storybook/preview.jsx +++ b/.storybook/preview.jsx @@ -20,7 +20,7 @@ export const parameters = { options: { storySort: { method: 'alphabetical', - order: ['Design Tokens', 'Library', 'App', 'ODD'], + order: ['Design Tokens', 'Library', 'App', 'ODD', 'AI'], }, }, } diff --git a/components/src/styles/flexbox.ts b/components/src/styles/flexbox.ts index bc588372e96..2c36936b200 100644 --- a/components/src/styles/flexbox.ts +++ b/components/src/styles/flexbox.ts @@ -1,6 +1,7 @@ export const FLEX_NONE = 'none' export const FLEX_AUTO = 'auto' export const FLEX_MIN_CONTENT = 'min-content' +export const FLEX_MAX_CONTENT = 'max-content' export const ALIGN_NORMAL = 'normal' export const ALIGN_START = 'start' diff --git a/opentrons-ai-client/README.md b/opentrons-ai-client/README.md index c2ff2908418..d4c80c2bb23 100644 --- a/opentrons-ai-client/README.md +++ b/opentrons-ai-client/README.md @@ -2,8 +2,6 @@ [![JavaScript Style Guide][style-guide-badge]][style-guide] -[Download][] | [Support][] - ## Overview The Opentrons AI application helps you to create a protocol with natural language. @@ -31,7 +29,7 @@ The UI stack is built using: Some important directories: -- `opentrons-ai-server` — Opentrons AI application's server +- [opentrons-ai-server][] — Opentrons AI application's server ## Copy management @@ -62,3 +60,4 @@ TBD [babel]: https://babeljs.io/ [vite]: https://vitejs.dev/ [bundle-analyzer]: https://github.com/webpack-contrib/webpack-bundle-analyzer +[opentrons-ai-server]: https://github.com/Opentrons/opentrons/tree/edge/opentrons-ai-server diff --git a/opentrons-ai-client/src/assets/images/opentrons_logo.svg b/opentrons-ai-client/src/assets/images/opentrons_logo.svg new file mode 100644 index 00000000000..b183d161e81 --- /dev/null +++ b/opentrons-ai-client/src/assets/images/opentrons_logo.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/opentrons-ai-client/src/assets/localization/en/protocol_generator.json b/opentrons-ai-client/src/assets/localization/en/protocol_generator.json index c8ac35504bb..f19455ad47e 100644 --- a/opentrons-ai-client/src/assets/localization/en/protocol_generator.json +++ b/opentrons-ai-client/src/assets/localization/en/protocol_generator.json @@ -2,20 +2,22 @@ "api": "API: An API level is 2.15", "application": "Application: Your protocol's name, describing what it does.", "commands": "Commands: List the protocol's steps, specifying quantities in microliters and giving exact source and destination locations.", + "got_feedback": "Got feedback? We love to hear it.", "make_sure_your_prompt": "Make sure your prompt includes the following:", "metadata": "Metadata: Three pieces of information.", "modules": "Modules: Thermocycler or Temperature Module.", "opentronsai_asks_you": "OpentronsAI asks you to provide it!", "ot2_pipettes": "OT-2 pipettes: Include volume, number of channels, and generation.", - "prc_flex": "PRC (Flex)", + "prc_flex": "PCR (Flex)", "prc": "PCR", "reagent_transfer_flex": "Reagent Transfer (Flex)", "reagent_transfer": "Reagent Transfer", "robot": "Robot: OT-2.", - "sidebar_body": "Write a prompt in natural language to generate a Reagent Transfer or a PCR protocol for the OT-2 or Opentrons Flex using the Opentrons Python Protocol API.", - "sidebar_header": "Use natural language to generate protocols with OpentronsAI powered by OpenAI", - "stuck": "Stuck? Try these example prompts to get started.", + "share_your_thoughts": "Share your thoughts here", + "side_panel_body": "Write a prompt in natural language to generate a Reagent Transfer or a PCR protocol for the OT-2 or Opentrons Flex using the Opentrons Python Protocol API.", + "side_panel_header": "Use natural language to generate protocols with OpentronsAI powered by OpenAI", "tipracks_and_labware": "Tip racks and labware: Use names from the Opentrons Labware Library.", + "try_example_prompts": "Stuck? Try these example prompts to get started.", "type_your_prompt": "Type your prompt...", "well_allocations": "Well allocations: Describe where liquids should go in labware.", "what_if_you": "What if you don’t provide all of those pieces of information?", diff --git a/opentrons-ai-client/src/main.tsx b/opentrons-ai-client/src/main.tsx index 466bd35e081..bf46623695e 100644 --- a/opentrons-ai-client/src/main.tsx +++ b/opentrons-ai-client/src/main.tsx @@ -1,12 +1,17 @@ import React from 'react' import ReactDOM from 'react-dom/client' +import { I18nextProvider } from 'react-i18next' + +import { i18n } from './i18n' import { App } from './App' const rootElement = document.getElementById('root') -if (rootElement) { +if (rootElement != null) { ReactDOM.createRoot(rootElement).render( - + + + ) } else { diff --git a/opentrons-ai-client/src/molecules/SidePanel/SidePanel.stories.tsx b/opentrons-ai-client/src/molecules/SidePanel/SidePanel.stories.tsx new file mode 100644 index 00000000000..1c1d30b7548 --- /dev/null +++ b/opentrons-ai-client/src/molecules/SidePanel/SidePanel.stories.tsx @@ -0,0 +1,21 @@ +import React from 'react' +import { I18nextProvider } from 'react-i18next' +import { i18n } from '../../i18n' +import { SidePanel as SidePanelComponent } from './index' + +import type { Meta, StoryObj } from '@storybook/react' + +const meta: Meta = { + title: 'AI/molecules/SidePanel', + component: SidePanelComponent, + decorators: [ + Story => ( + + + + ), + ], +} +export default meta +type Story = StoryObj +export const SidePanel: Story = {} diff --git a/opentrons-ai-client/src/molecules/SidePanel/__tests__/SidePanel.test.tsx b/opentrons-ai-client/src/molecules/SidePanel/__tests__/SidePanel.test.tsx new file mode 100644 index 00000000000..56cb50f73fc --- /dev/null +++ b/opentrons-ai-client/src/molecules/SidePanel/__tests__/SidePanel.test.tsx @@ -0,0 +1,48 @@ +import React from 'react' +import { screen } from '@testing-library/react' +import { describe, it, expect } from 'vitest' + +import { renderWithProviders } from '../../../__testing-utils__' +import { i18n } from '../../../i18n' + +import { SidePanel } from '../index' + +const LOGO_FILE_NAME = + '/opentrons-ai-client/src/assets/images/opentrons_logo.svg' + +const FEEDBACK_FORM_LINK = 'https://opentrons-ai-beta.paperform.co/' + +const render = (): ReturnType => { + return renderWithProviders(, { + i18nInstance: i18n, + }) +} + +describe('SidePanel', () => { + it('should render logo and text', () => { + render() + const image = screen.getByRole('img') + expect(image.getAttribute('src')).toEqual(LOGO_FILE_NAME) + screen.getByText( + 'Use natural language to generate protocols with OpentronsAI powered by OpenAI' + ) + screen.getByText( + 'Write a prompt in natural language to generate a Reagent Transfer or a PCR protocol for the OT-2 or Opentrons Flex using the Opentrons Python Protocol API.' + ) + screen.getByText('Stuck? Try these example prompts to get started.') + screen.getByText('Got feedback? We love to hear it.') + const link = screen.getByRole('link', { + name: 'Share your thoughts here', + }) + expect(link).toHaveAttribute('href', FEEDBACK_FORM_LINK) + }) + + it('should render buttons', () => { + render() + screen.getByRole('button', { name: 'PCR' }) + screen.getByRole('button', { name: 'PCR (Flex)' }) + screen.getByRole('button', { name: 'Reagent Transfer' }) + screen.getByRole('button', { name: 'Reagent Transfer (Flex)' }) + }) + it.todo('should call a mock function when clicking a button') +}) diff --git a/opentrons-ai-client/src/molecules/SidePanel/index.tsx b/opentrons-ai-client/src/molecules/SidePanel/index.tsx new file mode 100644 index 00000000000..536c0709a8b --- /dev/null +++ b/opentrons-ai-client/src/molecules/SidePanel/index.tsx @@ -0,0 +1,103 @@ +import React from 'react' +import styled, { css } from 'styled-components' +import { useTranslation } from 'react-i18next' +import { + COLORS, + DIRECTION_COLUMN, + Flex, + Link, + PrimaryButton, + SPACING, + StyledText, + TYPOGRAPHY, + WRAP, +} from '@opentrons/components' +import LOGO_PATH from '../../assets/images/opentrons_logo.svg' + +const IMAGE_ALT = 'Opentrons logo' +const FEEDBACK_FORM_LINK = 'https://opentrons-ai-beta.paperform.co/' +export function SidePanel(): JSX.Element { + const { t } = useTranslation('protocol_generator') + return ( + + {/* logo */} + + {IMAGE_ALT} + + + {/* body text */} + + + {t('side_panel_header')} + + {t('side_panel_body')} + + + {/* buttons */} + + + {t('try_example_prompts')} + + + + {/* ToDo(kk:04/11/2024) add a button component */} + {t('reagent_transfer')} + {t('reagent_transfer_flex')} + {t('prc')} + {t('prc_flex')} + + + + + {t('got_feedback')} + + + {t('share_your_thoughts')} + + + + ) +} + +const HEADER_TEXT_STYLE = css` + font-size: ${TYPOGRAPHY.fontSize32}; + line-height: ${TYPOGRAPHY.lineHeight42}; + font-weight: ${TYPOGRAPHY.fontWeightBold}; + color: ${COLORS.white}; +` +const BODY_TEXT_STYLE = css` + font-size: ${TYPOGRAPHY.fontSize20}; + line-height: ${TYPOGRAPHY.lineHeight24}; + font-weight: ${TYPOGRAPHY.fontWeightRegular}; + color: ${COLORS.white}; +` +const BUTTON_GUIDE_TEXT_STYLE = css` + font-size: ${TYPOGRAPHY.fontSize20}; + line-height: ${TYPOGRAPHY.lineHeight24}; + font-weight: ${TYPOGRAPHY.fontWeightSemiBold}; + color: ${COLORS.white}; +` + +const PromptButton = styled(PrimaryButton)` + border-radius: 2rem; + white-space: nowrap; +` + +const FeedbackLink = styled(Link)` + font-size: ${TYPOGRAPHY.fontSize20}; + line-height: ${TYPOGRAPHY.lineHeight24}; + font-weight: ${TYPOGRAPHY.fontWeightBold}; + color: ${COLORS.white}; + text-decoration: ${TYPOGRAPHY.textDecorationUnderline}; +` diff --git a/opentrons-ai-client/src/molecules/index.ts b/opentrons-ai-client/src/molecules/index.ts new file mode 100644 index 00000000000..80fcd68f91a --- /dev/null +++ b/opentrons-ai-client/src/molecules/index.ts @@ -0,0 +1 @@ +export * from './SidePanel' From b0fb14f139c59cefa8c1e3d9319279a3f5ca6fb4 Mon Sep 17 00:00:00 2001 From: koji Date: Fri, 12 Apr 2024 12:34:59 -0400 Subject: [PATCH 107/194] fix(app): update software keyboard ref type (#14860) * fix(app): update software keyboard ref type --- app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/index.tsx | 3 ++- app/src/atoms/SoftwareKeyboard/FullKeyboard/index.tsx | 3 ++- app/src/atoms/SoftwareKeyboard/IndividualKey/index.tsx | 4 ++-- app/src/atoms/SoftwareKeyboard/NumericalKeyboard/index.tsx | 4 +++- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/index.tsx b/app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/index.tsx index 5698e49f1e6..dccad085c08 100644 --- a/app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/index.tsx +++ b/app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/index.tsx @@ -1,6 +1,7 @@ import * as React from 'react' import Keyboard from 'react-simple-keyboard' import { alphanumericKeyboardLayout, customDisplay } from '../constants' +import type { KeyboardReactInterface } from 'react-simple-keyboard' import '../index.css' import './index.css' @@ -8,7 +9,7 @@ import './index.css' // TODO (kk:04/05/2024) add debug to make debugging easy interface AlphanumericKeyboardProps { onChange: (input: string) => void - keyboardRef: React.MutableRefObject + keyboardRef: React.MutableRefObject debug?: boolean } diff --git a/app/src/atoms/SoftwareKeyboard/FullKeyboard/index.tsx b/app/src/atoms/SoftwareKeyboard/FullKeyboard/index.tsx index 69c5c748d3a..663efdd9c24 100644 --- a/app/src/atoms/SoftwareKeyboard/FullKeyboard/index.tsx +++ b/app/src/atoms/SoftwareKeyboard/FullKeyboard/index.tsx @@ -1,6 +1,7 @@ import * as React from 'react' import { KeyboardReact as Keyboard } from 'react-simple-keyboard' import { customDisplay, fullKeyboardLayout } from '../constants' +import type { KeyboardReactInterface } from 'react-simple-keyboard' import '../index.css' import './index.css' @@ -8,7 +9,7 @@ import './index.css' // TODO (kk:04/05/2024) add debug to make debugging easy interface FullKeyboardProps { onChange: (input: string) => void - keyboardRef: React.MutableRefObject + keyboardRef: React.MutableRefObject debug?: boolean } diff --git a/app/src/atoms/SoftwareKeyboard/IndividualKey/index.tsx b/app/src/atoms/SoftwareKeyboard/IndividualKey/index.tsx index 9ff8c278423..310008cddc8 100644 --- a/app/src/atoms/SoftwareKeyboard/IndividualKey/index.tsx +++ b/app/src/atoms/SoftwareKeyboard/IndividualKey/index.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { KeyboardReact as Keyboard } from 'react-simple-keyboard' - +import type { KeyboardReactInterface } from 'react-simple-keyboard' import '../index.css' import './index.css' @@ -11,7 +11,7 @@ const customDisplay = { // TODO (kk:04/05/2024) add debug to make debugging easy interface IndividualKeyProps { onChange: (input: string) => void - keyboardRef: React.MutableRefObject + keyboardRef: React.MutableRefObject keyText: string debug?: boolean } diff --git a/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/index.tsx b/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/index.tsx index 9065bcce44f..8c41120d536 100644 --- a/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/index.tsx +++ b/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/index.tsx @@ -1,13 +1,15 @@ import * as React from 'react' import { KeyboardReact as Keyboard } from 'react-simple-keyboard' import { numericalKeyboardLayout, numericalCustom } from '../constants' + +import type { KeyboardReactInterface } from 'react-simple-keyboard' import '../index.css' import './index.css' // Note (kk:04/05/2024) add debug to make debugging easy interface NumericalKeyboardProps { onChange: (input: string) => void - keyboardRef: React.MutableRefObject + keyboardRef: React.MutableRefObject isDecimal?: boolean hasHyphen?: boolean debug?: boolean From 485bf0e3f258e66f242a6c27adacf6b074e912bc Mon Sep 17 00:00:00 2001 From: Nick Diehl <47604184+ncdiehl11@users.noreply.github.com> Date: Fri, 12 Apr 2024 13:19:11 -0400 Subject: [PATCH 108/194] feat(app, api-client, react-api-client): add api-client method for protocol reanalysis (#14878) closes AUTH-118 --- .../src/protocols/createProtocolAnalysis.ts | 28 ++++++ api-client/src/protocols/index.ts | 1 + .../ProtocolSetupParameters.test.tsx | 16 ++-- .../ProtocolSetupParameters/index.tsx | 25 +++++- app/src/pages/ProtocolDetails/index.tsx | 5 +- app/src/pages/ProtocolSetup/index.tsx | 26 +++--- ...useCreateProtocolAnalysisMutation.test.tsx | 77 +++++++++++++++++ react-api-client/src/protocols/index.ts | 1 + .../useCreateProtocolAnalysisMutation.ts | 86 +++++++++++++++++++ 9 files changed, 241 insertions(+), 24 deletions(-) create mode 100644 api-client/src/protocols/createProtocolAnalysis.ts create mode 100644 react-api-client/src/protocols/__tests__/useCreateProtocolAnalysisMutation.test.tsx create mode 100644 react-api-client/src/protocols/useCreateProtocolAnalysisMutation.ts diff --git a/api-client/src/protocols/createProtocolAnalysis.ts b/api-client/src/protocols/createProtocolAnalysis.ts new file mode 100644 index 00000000000..81ab83c11af --- /dev/null +++ b/api-client/src/protocols/createProtocolAnalysis.ts @@ -0,0 +1,28 @@ +import { POST, request } from '../request' + +import type { ProtocolAnalysisSummary } from '@opentrons/shared-data' +import type { ResponsePromise } from '../request' +import type { HostConfig } from '../types' +import type { RunTimeParameterCreateData } from '../runs' + +interface CreateProtocolAnalysisData { + runTimeParameterValues: RunTimeParameterCreateData + forceReAnalyze: boolean +} + +export function createProtocolAnalysis( + config: HostConfig, + protocolKey: string, + runTimeParameterValues?: RunTimeParameterCreateData, + forceReAnalyze?: boolean +): ResponsePromise { + const data = { + runTimeParameterValues: runTimeParameterValues ?? {}, + forceReAnalyze: forceReAnalyze ?? false, + } + const response = request< + ProtocolAnalysisSummary[], + { data: CreateProtocolAnalysisData } + >(POST, `/protocols/${protocolKey}/analyses`, { data }, config) + return response +} diff --git a/api-client/src/protocols/index.ts b/api-client/src/protocols/index.ts index 6febd0795cf..f035fa000e1 100644 --- a/api-client/src/protocols/index.ts +++ b/api-client/src/protocols/index.ts @@ -3,6 +3,7 @@ export { getProtocolAnalyses } from './getProtocolAnalyses' export { getProtocolAnalysisAsDocument } from './getProtocolAnalysisAsDocument' export { deleteProtocol } from './deleteProtocol' export { createProtocol } from './createProtocol' +export { createProtocolAnalysis } from './createProtocolAnalysis' export { getProtocols } from './getProtocols' export { getProtocolIds } from './getProtocolIds' diff --git a/app/src/organisms/ProtocolSetupParameters/__tests__/ProtocolSetupParameters.test.tsx b/app/src/organisms/ProtocolSetupParameters/__tests__/ProtocolSetupParameters.test.tsx index 1dc55314d59..4871eeaa379 100644 --- a/app/src/organisms/ProtocolSetupParameters/__tests__/ProtocolSetupParameters.test.tsx +++ b/app/src/organisms/ProtocolSetupParameters/__tests__/ProtocolSetupParameters.test.tsx @@ -2,7 +2,11 @@ import * as React from 'react' import { when } from 'vitest-when' import { it, describe, beforeEach, vi, expect } from 'vitest' import { fireEvent, screen } from '@testing-library/react' -import { useCreateRunMutation, useHost } from '@opentrons/react-api-client' +import { + useCreateProtocolAnalysisMutation, + useCreateRunMutation, + useHost, +} from '@opentrons/react-api-client' import { i18n } from '../../../i18n' import { renderWithProviders } from '../../../__testing-utils__' import { ProtocolSetupParameters } from '..' @@ -24,6 +28,7 @@ vi.mock('react-router-dom', async importOriginal => { } }) const MOCK_HOST_CONFIG: HostConfig = { hostname: 'MOCK_HOST' } +const mockCreateProtocolAnalysis = vi.fn() const mockCreateRun = vi.fn() const render = ( props: React.ComponentProps @@ -43,6 +48,9 @@ describe('ProtocolSetupParameters', () => { } vi.mocked(ChooseEnum).mockReturnValue(
    mock ChooseEnum
    ) vi.mocked(useHost).mockReturnValue(MOCK_HOST_CONFIG) + when(vi.mocked(useCreateProtocolAnalysisMutation)) + .calledWith(expect.anything(), expect.anything()) + .thenReturn({ createProtocolAnalysis: mockCreateProtocolAnalysis } as any) when(vi.mocked(useCreateRunMutation)) .calledWith(expect.anything()) .thenReturn({ createRun: mockCreateRun } as any) @@ -62,10 +70,9 @@ describe('ProtocolSetupParameters', () => { }) it('renders the other setting when boolean param is selected', () => { render(props) - screen.getByText('Off') - expect(screen.getAllByText('On')).toHaveLength(3) + expect(screen.getAllByText('On')).toHaveLength(2) fireEvent.click(screen.getByText('Dry Run')) - expect(screen.getAllByText('On')).toHaveLength(4) + expect(screen.getAllByText('On')).toHaveLength(3) }) it('renders the back icon and calls useHistory', () => { render(props) @@ -88,6 +95,5 @@ describe('ProtocolSetupParameters', () => { const title = screen.getByText('Reset parameter values?') fireEvent.click(screen.getByRole('button', { name: 'Go back' })) expect(title).not.toBeInTheDocument() - // TODO(jr, 3/19/24): wire up the confirm button }) }) diff --git a/app/src/organisms/ProtocolSetupParameters/index.tsx b/app/src/organisms/ProtocolSetupParameters/index.tsx index ac1f3fd700f..5dae07260f6 100644 --- a/app/src/organisms/ProtocolSetupParameters/index.tsx +++ b/app/src/organisms/ProtocolSetupParameters/index.tsx @@ -1,7 +1,11 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' import { useHistory } from 'react-router-dom' -import { useCreateRunMutation, useHost } from '@opentrons/react-api-client' +import { + useCreateProtocolAnalysisMutation, + useCreateRunMutation, + useHost, +} from '@opentrons/react-api-client' import { useQueryClient } from 'react-query' import { ALIGN_CENTER, @@ -51,7 +55,12 @@ export function ProtocolSetupParameters({ const [ runTimeParametersOverrides, setRunTimeParametersOverrides, - ] = React.useState(runTimeParameters) + ] = React.useState( + // present defaults rather than last-set value + runTimeParameters.map(param => { + return { ...param, value: param.default } + }) + ) const updateParameters = ( value: boolean | string | number, @@ -85,6 +94,14 @@ export function ProtocolSetupParameters({ } } + const runTimeParameterValues = getRunTimeParameterValuesForRun( + runTimeParametersOverrides + ) + const { createProtocolAnalysis } = useCreateProtocolAnalysisMutation( + protocolId, + host + ) + const { createRun, isLoading } = useCreateRunMutation({ onSuccess: data => { queryClient @@ -96,6 +113,10 @@ export function ProtocolSetupParameters({ }) const handleConfirmValues = (): void => { setStartSetup(true) + createProtocolAnalysis({ + protocolKey: protocolId, + runTimeParameterValues: runTimeParameterValues, + }) createRun({ protocolId, labwareOffsets, diff --git a/app/src/pages/ProtocolDetails/index.tsx b/app/src/pages/ProtocolDetails/index.tsx index 0503c0eae54..850fd0a8016 100644 --- a/app/src/pages/ProtocolDetails/index.tsx +++ b/app/src/pages/ProtocolDetails/index.tsx @@ -346,13 +346,12 @@ export function ProtocolDetails(): JSX.Element | null { let pinnedProtocolIds = useSelector(getPinnedProtocolIds) ?? [] const pinned = pinnedProtocolIds.includes(protocolId) - const { data: protocolData } = useProtocolQuery(protocolId) const { data: mostRecentAnalysis, } = useProtocolAnalysisAsDocumentQuery( protocolId, - last(protocolData?.data.analysisSummaries)?.id ?? null, - { enabled: protocolData != null } + last(protocolRecord?.data.analysisSummaries)?.id ?? null, + { enabled: protocolRecord != null } ) const shouldApplyOffsets = useSelector(getApplyHistoricOffsets) diff --git a/app/src/pages/ProtocolSetup/index.tsx b/app/src/pages/ProtocolSetup/index.tsx index 97499316f27..14b871f839c 100644 --- a/app/src/pages/ProtocolSetup/index.tsx +++ b/app/src/pages/ProtocolSetup/index.tsx @@ -69,7 +69,6 @@ import { getProtocolUsesGripper, } from '../../organisms/ProtocolSetupInstruments/utils' import { - useProtocolHasRunTimeParameters, useRunControls, useRunStatus, } from '../../organisms/RunTimeControl/hooks' @@ -257,9 +256,6 @@ function PrepareToRun({ const { t, i18n } = useTranslation(['protocol_setup', 'shared']) const history = useHistory() const { makeSnackbar } = useToaster() - const hasRunTimeParameters = useProtocolHasRunTimeParameters(runId) - console.log(hasRunTimeParameters) - // Watch for scrolling to toggle dropshadow const scrollRef = React.useRef(null) const [isScrolled, setIsScrolled] = React.useState(false) const observer = new IntersectionObserver(([entry]) => { @@ -366,6 +362,12 @@ function PrepareToRun({ }) const moduleCalibrationStatus = useModuleCalibrationStatus(robotName, runId) + const runTimeParameters = mostRecentAnalysis?.runTimeParameters ?? [] + const hasRunTimeParameters = runTimeParameters.length > 0 + const hasCustomRunTimeParameters = runTimeParameters.some( + parameter => parameter.value !== parameter.default + ) + const [ showConfirmCancelModal, setShowConfirmCancelModal, @@ -623,11 +625,11 @@ function PrepareToRun({ doorStatus?.data.status === 'open' && doorStatus?.data.doorRequiredClosedForProtocol - // TODO(Jr, 3/20/24): wire up custom values - const hasCustomValues = false - const parametersDetail = hasCustomValues - ? t('custom_values') - : t('default_values') + const parametersDetail = hasRunTimeParameters + ? hasCustomRunTimeParameters + ? t('custom_values') + : t('default_values') + : t('no_parameters_specified') return ( <> @@ -733,11 +735,7 @@ function PrepareToRun({ setSetupScreen('view only parameters')} title={t('parameters')} - detail={t( - hasRunTimeParameters - ? parametersDetail - : t('no_parameters_specified') - )} + detail={parametersDetail} subDetail={null} status="general" disabled={!hasRunTimeParameters} diff --git a/react-api-client/src/protocols/__tests__/useCreateProtocolAnalysisMutation.test.tsx b/react-api-client/src/protocols/__tests__/useCreateProtocolAnalysisMutation.test.tsx new file mode 100644 index 00000000000..e04c020fb1d --- /dev/null +++ b/react-api-client/src/protocols/__tests__/useCreateProtocolAnalysisMutation.test.tsx @@ -0,0 +1,77 @@ +import * as React from 'react' +import { describe, it, expect, beforeEach, vi } from 'vitest' +import { QueryClient, QueryClientProvider } from 'react-query' +import { act, renderHook, waitFor } from '@testing-library/react' +import { createProtocolAnalysis } from '@opentrons/api-client' +import { useHost } from '../../api' +import { useCreateProtocolAnalysisMutation } from '..' +import type { HostConfig, Response } from '@opentrons/api-client' +import type { ProtocolAnalysisSummary } from '@opentrons/shared-data' + +vi.mock('@opentrons/api-client') +vi.mock('../../api/useHost') + +const HOST_CONFIG: HostConfig = { hostname: 'localhost' } +const ANALYSIS_SUMMARY_RESPONSE = [ + { id: 'fakeAnalysis1', status: 'completed' }, + { id: 'fakeAnalysis2', status: 'pending' }, +] as ProtocolAnalysisSummary[] + +describe('useCreateProtocolAnalysisMutation hook', () => { + let wrapper: React.FunctionComponent<{ children: React.ReactNode }> + + beforeEach(() => { + const queryClient = new QueryClient() + const clientProvider: React.FunctionComponent<{ + children: React.ReactNode + }> = ({ children }) => ( + {children} + ) + wrapper = clientProvider + }) + + it('should return no data when calling createProtocolAnalysis if the request fails', async () => { + vi.mocked(useHost).mockReturnValue(HOST_CONFIG) + vi.mocked(createProtocolAnalysis).mockRejectedValue('oh no') + + const { result } = renderHook( + () => useCreateProtocolAnalysisMutation('fake-protocol-key'), + { + wrapper, + } + ) + + expect(result.current.data).toBeUndefined() + result.current.createProtocolAnalysis({ + protocolKey: 'fake-protocol-key', + runTimeParameterValues: {}, + }) + await waitFor(() => { + expect(result.current.data).toBeUndefined() + }) + }) + + it('should create an array of ProtocolAnalysisSummaries when calling the createProtocolAnalysis callback', async () => { + vi.mocked(useHost).mockReturnValue(HOST_CONFIG) + vi.mocked(createProtocolAnalysis).mockResolvedValue({ + data: ANALYSIS_SUMMARY_RESPONSE, + } as Response) + + const { result } = renderHook( + () => useCreateProtocolAnalysisMutation('fake-protocol-key'), + { + wrapper, + } + ) + act(() => + result.current.createProtocolAnalysis({ + protocolKey: 'fake-protocol-key', + runTimeParameterValues: {}, + }) + ) + + await waitFor(() => { + expect(result.current.data).toEqual(ANALYSIS_SUMMARY_RESPONSE) + }) + }) +}) diff --git a/react-api-client/src/protocols/index.ts b/react-api-client/src/protocols/index.ts index ddf7c3eeaac..561dee01e8b 100644 --- a/react-api-client/src/protocols/index.ts +++ b/react-api-client/src/protocols/index.ts @@ -4,4 +4,5 @@ export { useProtocolQuery } from './useProtocolQuery' export { useProtocolAnalysesQuery } from './useProtocolAnalysesQuery' export { useProtocolAnalysisAsDocumentQuery } from './useProtocolAnalysisAsDocumentQuery' export { useCreateProtocolMutation } from './useCreateProtocolMutation' +export { useCreateProtocolAnalysisMutation } from './useCreateProtocolAnalysisMutation' export { useDeleteProtocolMutation } from './useDeleteProtocolMutation' diff --git a/react-api-client/src/protocols/useCreateProtocolAnalysisMutation.ts b/react-api-client/src/protocols/useCreateProtocolAnalysisMutation.ts new file mode 100644 index 00000000000..f8ba6e10586 --- /dev/null +++ b/react-api-client/src/protocols/useCreateProtocolAnalysisMutation.ts @@ -0,0 +1,86 @@ +import { createProtocolAnalysis } from '@opentrons/api-client' +import { useMutation, useQueryClient } from 'react-query' +import { useHost } from '../api' +import type { + ErrorResponse, + HostConfig, + RunTimeParameterCreateData, +} from '@opentrons/api-client' +import type { ProtocolAnalysisSummary } from '@opentrons/shared-data' +import type { AxiosError } from 'axios' +import type { + UseMutationResult, + UseMutationOptions, + UseMutateFunction, +} from 'react-query' + +export interface CreateProtocolAnalysisVariables { + protocolKey: string + runTimeParameterValues?: RunTimeParameterCreateData + forceReAnalyze?: boolean +} +export type UseCreateProtocolMutationResult = UseMutationResult< + ProtocolAnalysisSummary[], + AxiosError, + CreateProtocolAnalysisVariables +> & { + createProtocolAnalysis: UseMutateFunction< + ProtocolAnalysisSummary[], + AxiosError, + CreateProtocolAnalysisVariables + > +} + +export type UseCreateProtocolAnalysisMutationOptions = UseMutationOptions< + ProtocolAnalysisSummary[], + AxiosError, + CreateProtocolAnalysisVariables +> + +export function useCreateProtocolAnalysisMutation( + protocolId: string | null, + hostOverride?: HostConfig | null, + options: UseCreateProtocolAnalysisMutationOptions | undefined = {} +): UseCreateProtocolMutationResult { + const contextHost = useHost() + const host = + hostOverride != null ? { ...contextHost, ...hostOverride } : contextHost + const queryClient = useQueryClient() + + const mutation = useMutation< + ProtocolAnalysisSummary[], + AxiosError, + CreateProtocolAnalysisVariables + >( + [host, 'protocols', protocolId, 'analyses'], + ({ protocolKey, runTimeParameterValues, forceReAnalyze }) => + createProtocolAnalysis( + host as HostConfig, + protocolKey, + runTimeParameterValues, + forceReAnalyze + ) + .then(response => { + queryClient + .invalidateQueries([host, 'protocols', protocolId, 'analyses']) + .then(() => + queryClient.setQueryData( + [host, 'protocols', protocolId, 'analyses'], + response.data + ) + ) + .catch((e: Error) => { + throw e + }) + return response.data + }) + .catch((e: Error) => { + throw e + }), + options + ) + return { + ...mutation, + createProtocolAnalysis: mutation.mutate, + } +} From 437e074171f2bddd3609be2dcaf80901372f1726 Mon Sep 17 00:00:00 2001 From: Brayan Almonte Date: Fri, 12 Apr 2024 15:28:18 -0400 Subject: [PATCH 109/194] feat(system-server,robot-server,api): add ability to enable OEM Mode via advanced settings. (#14832) --- api/src/opentrons/config/advanced_settings.py | 4 + api/src/opentrons/config/feature_flags.py | 4 + robot-server/Pipfile | 1 + robot-server/Pipfile.lock | 378 ++++++++++++++++++ .../service/legacy/routers/settings.py | 37 +- system-server/settings_schema.json | 13 + .../system_server/settings/__init__.py | 8 +- .../system_server/settings/settings.py | 50 ++- .../system_server/system/oem_mode/__init__.py | 5 + .../system/oem_mode/dependencies.py | 21 + .../system_server/system/oem_mode/models.py | 9 + .../system_server/system/oem_mode/router.py | 37 ++ system-server/system_server/system/router.py | 3 + .../integration/test_oem_mode.tavern.yaml | 37 ++ 14 files changed, 582 insertions(+), 25 deletions(-) create mode 100644 system-server/system_server/system/oem_mode/__init__.py create mode 100644 system-server/system_server/system/oem_mode/dependencies.py create mode 100644 system-server/system_server/system/oem_mode/models.py create mode 100644 system-server/system_server/system/oem_mode/router.py create mode 100644 system-server/tests/integration/test_oem_mode.tavern.yaml diff --git a/api/src/opentrons/config/advanced_settings.py b/api/src/opentrons/config/advanced_settings.py index f4c75701901..4d83d8ed1af 100644 --- a/api/src/opentrons/config/advanced_settings.py +++ b/api/src/opentrons/config/advanced_settings.py @@ -238,6 +238,10 @@ class Setting(NamedTuple): title="Enable OEM Mode", description="This setting anonymizes Opentrons branding in the ODD app.", robot_type=[RobotTypeEnum.FLEX], + show_if=( + "enableOEMMode", + True, + ), internal_only=True, ), SettingDefinition( diff --git a/api/src/opentrons/config/feature_flags.py b/api/src/opentrons/config/feature_flags.py index e9772a01ee8..65984dd7ab9 100644 --- a/api/src/opentrons/config/feature_flags.py +++ b/api/src/opentrons/config/feature_flags.py @@ -80,3 +80,7 @@ def enable_error_recovery_experiments() -> bool: def enable_performance_metrics(robot_type: RobotTypeEnum) -> bool: return advs.get_setting_with_env_overload("enablePerformanceMetrics", robot_type) + + +def oem_mode_enabled() -> bool: + return advs.get_setting_with_env_overload("enableOEMMode", RobotTypeEnum.FLEX) diff --git a/robot-server/Pipfile b/robot-server/Pipfile index e6c1b7ba794..9461d736de2 100755 --- a/robot-server/Pipfile +++ b/robot-server/Pipfile @@ -39,6 +39,7 @@ types-paho-mqtt = "==1.6.0.20240106" [packages] anyio = "==3.7.1" +aiohttp = "==3.8.1" # fastapi >=0.100.0 is intended for use with pydantic 2.x, and while it theoretically is # backwards compatible, best to be sure fastapi = "==0.99.1" diff --git a/robot-server/Pipfile.lock b/robot-server/Pipfile.lock index e97832aab95..6306e3dfb27 100644 --- a/robot-server/Pipfile.lock +++ b/robot-server/Pipfile.lock @@ -16,6 +16,85 @@ ] }, "default": { + "aiohttp": { + "hashes": [ + "sha256:01d7bdb774a9acc838e6b8f1d114f45303841b89b95984cbb7d80ea41172a9e3", + "sha256:03a6d5349c9ee8f79ab3ff3694d6ce1cfc3ced1c9d36200cb8f08ba06bd3b782", + "sha256:04d48b8ce6ab3cf2097b1855e1505181bdd05586ca275f2505514a6e274e8e75", + "sha256:0770e2806a30e744b4e21c9d73b7bee18a1cfa3c47991ee2e5a65b887c49d5cf", + "sha256:07b05cd3305e8a73112103c834e91cd27ce5b4bd07850c4b4dbd1877d3f45be7", + "sha256:086f92daf51a032d062ec5f58af5ca6a44d082c35299c96376a41cbb33034675", + "sha256:099ebd2c37ac74cce10a3527d2b49af80243e2a4fa39e7bce41617fbc35fa3c1", + "sha256:0c7ebbbde809ff4e970824b2b6cb7e4222be6b95a296e46c03cf050878fc1785", + "sha256:102e487eeb82afac440581e5d7f8f44560b36cf0bdd11abc51a46c1cd88914d4", + "sha256:11691cf4dc5b94236ccc609b70fec991234e7ef8d4c02dd0c9668d1e486f5abf", + "sha256:11a67c0d562e07067c4e86bffc1553f2cf5b664d6111c894671b2b8712f3aba5", + "sha256:12de6add4038df8f72fac606dff775791a60f113a725c960f2bab01d8b8e6b15", + "sha256:13487abd2f761d4be7c8ff9080de2671e53fff69711d46de703c310c4c9317ca", + "sha256:15b09b06dae900777833fe7fc4b4aa426556ce95847a3e8d7548e2d19e34edb8", + "sha256:1c182cb873bc91b411e184dab7a2b664d4fea2743df0e4d57402f7f3fa644bac", + "sha256:1ed0b6477896559f17b9eaeb6d38e07f7f9ffe40b9f0f9627ae8b9926ae260a8", + "sha256:28d490af82bc6b7ce53ff31337a18a10498303fe66f701ab65ef27e143c3b0ef", + "sha256:2e5d962cf7e1d426aa0e528a7e198658cdc8aa4fe87f781d039ad75dcd52c516", + "sha256:2ed076098b171573161eb146afcb9129b5ff63308960aeca4b676d9d3c35e700", + "sha256:2f2f69dca064926e79997f45b2f34e202b320fd3782f17a91941f7eb85502ee2", + "sha256:31560d268ff62143e92423ef183680b9829b1b482c011713ae941997921eebc8", + "sha256:31d1e1c0dbf19ebccbfd62eff461518dcb1e307b195e93bba60c965a4dcf1ba0", + "sha256:37951ad2f4a6df6506750a23f7cbabad24c73c65f23f72e95897bb2cecbae676", + "sha256:3af642b43ce56c24d063325dd2cf20ee012d2b9ba4c3c008755a301aaea720ad", + "sha256:44db35a9e15d6fe5c40d74952e803b1d96e964f683b5a78c3cc64eb177878155", + "sha256:473d93d4450880fe278696549f2e7aed8cd23708c3c1997981464475f32137db", + "sha256:477c3ea0ba410b2b56b7efb072c36fa91b1e6fc331761798fa3f28bb224830dd", + "sha256:4a4a4e30bf1edcad13fb0804300557aedd07a92cabc74382fdd0ba6ca2661091", + "sha256:4aed991a28ea3ce320dc8ce655875e1e00a11bdd29fe9444dd4f88c30d558602", + "sha256:51467000f3647d519272392f484126aa716f747859794ac9924a7aafa86cd411", + "sha256:55c3d1072704d27401c92339144d199d9de7b52627f724a949fc7d5fc56d8b93", + "sha256:589c72667a5febd36f1315aa6e5f56dd4aa4862df295cb51c769d16142ddd7cd", + "sha256:5bfde62d1d2641a1f5173b8c8c2d96ceb4854f54a44c23102e2ccc7e02f003ec", + "sha256:5c23b1ad869653bc818e972b7a3a79852d0e494e9ab7e1a701a3decc49c20d51", + "sha256:61bfc23df345d8c9716d03717c2ed5e27374e0fe6f659ea64edcd27b4b044cf7", + "sha256:6ae828d3a003f03ae31915c31fa684b9890ea44c9c989056fea96e3d12a9fa17", + "sha256:6c7cefb4b0640703eb1069835c02486669312bf2f12b48a748e0a7756d0de33d", + "sha256:6d69f36d445c45cda7b3b26afef2fc34ef5ac0cdc75584a87ef307ee3c8c6d00", + "sha256:6f0d5f33feb5f69ddd57a4a4bd3d56c719a141080b445cbf18f238973c5c9923", + "sha256:6f8b01295e26c68b3a1b90efb7a89029110d3a4139270b24fda961893216c440", + "sha256:713ac174a629d39b7c6a3aa757b337599798da4c1157114a314e4e391cd28e32", + "sha256:718626a174e7e467f0558954f94af117b7d4695d48eb980146016afa4b580b2e", + "sha256:7187a76598bdb895af0adbd2fb7474d7f6025d170bc0a1130242da817ce9e7d1", + "sha256:71927042ed6365a09a98a6377501af5c9f0a4d38083652bcd2281a06a5976724", + "sha256:7d08744e9bae2ca9c382581f7dce1273fe3c9bae94ff572c3626e8da5b193c6a", + "sha256:7dadf3c307b31e0e61689cbf9e06be7a867c563d5a63ce9dca578f956609abf8", + "sha256:81e3d8c34c623ca4e36c46524a3530e99c0bc95ed068fd6e9b55cb721d408fb2", + "sha256:844a9b460871ee0a0b0b68a64890dae9c415e513db0f4a7e3cab41a0f2fedf33", + "sha256:8b7ef7cbd4fec9a1e811a5de813311ed4f7ac7d93e0fda233c9b3e1428f7dd7b", + "sha256:97ef77eb6b044134c0b3a96e16abcb05ecce892965a2124c566af0fd60f717e2", + "sha256:99b5eeae8e019e7aad8af8bb314fb908dd2e028b3cdaad87ec05095394cce632", + "sha256:a25fa703a527158aaf10dafd956f7d42ac6d30ec80e9a70846253dd13e2f067b", + "sha256:a2f635ce61a89c5732537a7896b6319a8fcfa23ba09bec36e1b1ac0ab31270d2", + "sha256:a79004bb58748f31ae1cbe9fa891054baaa46fb106c2dc7af9f8e3304dc30316", + "sha256:a996d01ca39b8dfe77440f3cd600825d05841088fd6bc0144cc6c2ec14cc5f74", + "sha256:b0e20cddbd676ab8a64c774fefa0ad787cc506afd844de95da56060348021e96", + "sha256:b6613280ccedf24354406caf785db748bebbddcf31408b20c0b48cb86af76866", + "sha256:b9d00268fcb9f66fbcc7cd9fe423741d90c75ee029a1d15c09b22d23253c0a44", + "sha256:bb01ba6b0d3f6c68b89fce7305080145d4877ad3acaed424bae4d4ee75faa950", + "sha256:c2aef4703f1f2ddc6df17519885dbfa3514929149d3ff900b73f45998f2532fa", + "sha256:c34dc4958b232ef6188c4318cb7b2c2d80521c9a56c52449f8f93ab7bc2a8a1c", + "sha256:c3630c3ef435c0a7c549ba170a0633a56e92629aeed0e707fec832dee313fb7a", + "sha256:c3d6a4d0619e09dcd61021debf7059955c2004fa29f48788a3dfaf9c9901a7cd", + "sha256:d15367ce87c8e9e09b0f989bfd72dc641bcd04ba091c68cd305312d00962addd", + "sha256:d2f9b69293c33aaa53d923032fe227feac867f81682f002ce33ffae978f0a9a9", + "sha256:e999f2d0e12eea01caeecb17b653f3713d758f6dcc770417cf29ef08d3931421", + "sha256:ea302f34477fda3f85560a06d9ebdc7fa41e82420e892fc50b577e35fc6a50b2", + "sha256:eaba923151d9deea315be1f3e2b31cc39a6d1d2f682f942905951f4e40200922", + "sha256:ef9612483cb35171d51d9173647eed5d0069eaa2ee812793a75373447d487aa4", + "sha256:f5315a2eb0239185af1bddb1abf472d877fede3cc8d143c6cddad37678293237", + "sha256:fa0ffcace9b3aa34d205d8130f7873fcfefcb6a4dd3dd705b0dab69af6712642", + "sha256:fc5471e1a54de15ef71c1bc6ebe80d4dc681ea600e68bfd1cbce40427f0b7578" + ], + "index": "pypi", + "markers": "python_version >= '3.6'", + "version": "==3.8.1" + }, "aionotify": { "hashes": [ "sha256:385e1becfaac2d9f4326673033d53912ef9565b6febdedbec593ee966df392c6", @@ -23,6 +102,14 @@ ], "version": "==0.2.0" }, + "aiosignal": { + "hashes": [ + "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc", + "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17" + ], + "markers": "python_version >= '3.7'", + "version": "==1.3.1" + }, "anyio": { "hashes": [ "sha256:44a3c9aba0f5defa43261a8b3efb97891f2bd7d804e0e1f56419befa1adfc780", @@ -32,6 +119,14 @@ "markers": "python_version >= '3.7'", "version": "==3.7.1" }, + "async-timeout": { + "hashes": [ + "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f", + "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028" + ], + "markers": "python_version >= '3.7'", + "version": "==4.0.3" + }, "attrs": { "hashes": [ "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30", @@ -40,6 +135,14 @@ "markers": "python_version >= '3.7'", "version": "==23.2.0" }, + "charset-normalizer": { + "hashes": [ + "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845", + "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f" + ], + "markers": "python_full_version >= '3.6.0'", + "version": "==2.1.1" + }, "click": { "hashes": [ "sha256:24e1a4a9ec5bf6299411369b208c1df2188d9eb8d916302fe6bf03faed227f1e", @@ -66,6 +169,89 @@ "markers": "python_version >= '3.7'", "version": "==0.99.1" }, + "frozenlist": { + "hashes": [ + "sha256:04ced3e6a46b4cfffe20f9ae482818e34eba9b5fb0ce4056e4cc9b6e212d09b7", + "sha256:0633c8d5337cb5c77acbccc6357ac49a1770b8c487e5b3505c57b949b4b82e98", + "sha256:068b63f23b17df8569b7fdca5517edef76171cf3897eb68beb01341131fbd2ad", + "sha256:0c250a29735d4f15321007fb02865f0e6b6a41a6b88f1f523ca1596ab5f50bd5", + "sha256:1979bc0aeb89b33b588c51c54ab0161791149f2461ea7c7c946d95d5f93b56ae", + "sha256:1a4471094e146b6790f61b98616ab8e44f72661879cc63fa1049d13ef711e71e", + "sha256:1b280e6507ea8a4fa0c0a7150b4e526a8d113989e28eaaef946cc77ffd7efc0a", + "sha256:1d0ce09d36d53bbbe566fe296965b23b961764c0bcf3ce2fa45f463745c04701", + "sha256:20b51fa3f588ff2fe658663db52a41a4f7aa6c04f6201449c6c7c476bd255c0d", + "sha256:23b2d7679b73fe0e5a4560b672a39f98dfc6f60df63823b0a9970525325b95f6", + "sha256:23b701e65c7b36e4bf15546a89279bd4d8675faabc287d06bbcfac7d3c33e1e6", + "sha256:2471c201b70d58a0f0c1f91261542a03d9a5e088ed3dc6c160d614c01649c106", + "sha256:27657df69e8801be6c3638054e202a135c7f299267f1a55ed3a598934f6c0d75", + "sha256:29acab3f66f0f24674b7dc4736477bcd4bc3ad4b896f5f45379a67bce8b96868", + "sha256:32453c1de775c889eb4e22f1197fe3bdfe457d16476ea407472b9442e6295f7a", + "sha256:3a670dc61eb0d0eb7080890c13de3066790f9049b47b0de04007090807c776b0", + "sha256:3e0153a805a98f5ada7e09826255ba99fb4f7524bb81bf6b47fb702666484ae1", + "sha256:410478a0c562d1a5bcc2f7ea448359fcb050ed48b3c6f6f4f18c313a9bdb1826", + "sha256:442acde1e068288a4ba7acfe05f5f343e19fac87bfc96d89eb886b0363e977ec", + "sha256:48f6a4533887e189dae092f1cf981f2e3885175f7a0f33c91fb5b7b682b6bab6", + "sha256:4f57dab5fe3407b6c0c1cc907ac98e8a189f9e418f3b6e54d65a718aaafe3950", + "sha256:4f9c515e7914626b2a2e1e311794b4c35720a0be87af52b79ff8e1429fc25f19", + "sha256:55fdc093b5a3cb41d420884cdaf37a1e74c3c37a31f46e66286d9145d2063bd0", + "sha256:5667ed53d68d91920defdf4035d1cdaa3c3121dc0b113255124bcfada1cfa1b8", + "sha256:590344787a90ae57d62511dd7c736ed56b428f04cd8c161fcc5e7232c130c69a", + "sha256:5a7d70357e7cee13f470c7883a063aae5fe209a493c57d86eb7f5a6f910fae09", + "sha256:5c3894db91f5a489fc8fa6a9991820f368f0b3cbdb9cd8849547ccfab3392d86", + "sha256:5c849d495bf5154cd8da18a9eb15db127d4dba2968d88831aff6f0331ea9bd4c", + "sha256:64536573d0a2cb6e625cf309984e2d873979709f2cf22839bf2d61790b448ad5", + "sha256:693945278a31f2086d9bf3df0fe8254bbeaef1fe71e1351c3bd730aa7d31c41b", + "sha256:6db4667b187a6742b33afbbaf05a7bc551ffcf1ced0000a571aedbb4aa42fc7b", + "sha256:6eb73fa5426ea69ee0e012fb59cdc76a15b1283d6e32e4f8dc4482ec67d1194d", + "sha256:722e1124aec435320ae01ee3ac7bec11a5d47f25d0ed6328f2273d287bc3abb0", + "sha256:7268252af60904bf52c26173cbadc3a071cece75f873705419c8681f24d3edea", + "sha256:74fb4bee6880b529a0c6560885fce4dc95936920f9f20f53d99a213f7bf66776", + "sha256:780d3a35680ced9ce682fbcf4cb9c2bad3136eeff760ab33707b71db84664e3a", + "sha256:82e8211d69a4f4bc360ea22cd6555f8e61a1bd211d1d5d39d3d228b48c83a897", + "sha256:89aa2c2eeb20957be2d950b85974b30a01a762f3308cd02bb15e1ad632e22dc7", + "sha256:8aefbba5f69d42246543407ed2461db31006b0f76c4e32dfd6f42215a2c41d09", + "sha256:96ec70beabbd3b10e8bfe52616a13561e58fe84c0101dd031dc78f250d5128b9", + "sha256:9750cc7fe1ae3b1611bb8cfc3f9ec11d532244235d75901fb6b8e42ce9229dfe", + "sha256:9acbb16f06fe7f52f441bb6f413ebae6c37baa6ef9edd49cdd567216da8600cd", + "sha256:9d3e0c25a2350080e9319724dede4f31f43a6c9779be48021a7f4ebde8b2d742", + "sha256:a06339f38e9ed3a64e4c4e43aec7f59084033647f908e4259d279a52d3757d09", + "sha256:a0cb6f11204443f27a1628b0e460f37fb30f624be6051d490fa7d7e26d4af3d0", + "sha256:a7496bfe1da7fb1a4e1cc23bb67c58fab69311cc7d32b5a99c2007b4b2a0e932", + "sha256:a828c57f00f729620a442881cc60e57cfcec6842ba38e1b19fd3e47ac0ff8dc1", + "sha256:a9b2de4cf0cdd5bd2dee4c4f63a653c61d2408055ab77b151c1957f221cabf2a", + "sha256:b46c8ae3a8f1f41a0d2ef350c0b6e65822d80772fe46b653ab6b6274f61d4a49", + "sha256:b7e3ed87d4138356775346e6845cccbe66cd9e207f3cd11d2f0b9fd13681359d", + "sha256:b7f2f9f912dca3934c1baec2e4585a674ef16fe00218d833856408c48d5beee7", + "sha256:ba60bb19387e13597fb059f32cd4d59445d7b18b69a745b8f8e5db0346f33480", + "sha256:beee944ae828747fd7cb216a70f120767fc9f4f00bacae8543c14a6831673f89", + "sha256:bfa4a17e17ce9abf47a74ae02f32d014c5e9404b6d9ac7f729e01562bbee601e", + "sha256:c037a86e8513059a2613aaba4d817bb90b9d9b6b69aace3ce9c877e8c8ed402b", + "sha256:c302220494f5c1ebeb0912ea782bcd5e2f8308037b3c7553fad0e48ebad6ad82", + "sha256:c6321c9efe29975232da3bd0af0ad216800a47e93d763ce64f291917a381b8eb", + "sha256:c757a9dd70d72b076d6f68efdbb9bc943665ae954dad2801b874c8c69e185068", + "sha256:c99169d4ff810155ca50b4da3b075cbde79752443117d89429595c2e8e37fed8", + "sha256:c9c92be9fd329ac801cc420e08452b70e7aeab94ea4233a4804f0915c14eba9b", + "sha256:cc7b01b3754ea68a62bd77ce6020afaffb44a590c2289089289363472d13aedb", + "sha256:db9e724bebd621d9beca794f2a4ff1d26eed5965b004a97f1f1685a173b869c2", + "sha256:dca69045298ce5c11fd539682cff879cc1e664c245d1c64da929813e54241d11", + "sha256:dd9b1baec094d91bf36ec729445f7769d0d0cf6b64d04d86e45baf89e2b9059b", + "sha256:e02a0e11cf6597299b9f3bbd3f93d79217cb90cfd1411aec33848b13f5c656cc", + "sha256:e6a20a581f9ce92d389a8c7d7c3dd47c81fd5d6e655c8dddf341e14aa48659d0", + "sha256:e7004be74cbb7d9f34553a5ce5fb08be14fb33bc86f332fb71cbe5216362a497", + "sha256:e774d53b1a477a67838a904131c4b0eef6b3d8a651f8b138b04f748fccfefe17", + "sha256:edb678da49d9f72c9f6c609fbe41a5dfb9a9282f9e6a2253d5a91e0fc382d7c0", + "sha256:f146e0911cb2f1da549fc58fc7bcd2b836a44b79ef871980d605ec392ff6b0d2", + "sha256:f56e2333dda1fe0f909e7cc59f021eba0d2307bc6f012a1ccf2beca6ba362439", + "sha256:f9a3ea26252bd92f570600098783d1371354d89d5f6b7dfd87359d669f2109b5", + "sha256:f9aa1878d1083b276b0196f2dfbe00c9b7e752475ed3b682025ff20c1c1f51ac", + "sha256:fb3c2db03683b5767dedb5769b8a40ebb47d6f7f45b1b3e3b4b51ec8ad9d9825", + "sha256:fbeb989b5cc29e8daf7f976b421c220f1b8c731cbf22b9130d8815418ea45887", + "sha256:fde5bd59ab5357e3853313127f4d3565fc7dad314a74d7b5d43c22c6a5ed2ced", + "sha256:fe1a06da377e3a1062ae5fe0926e12b84eceb8a50b350ddca72dc85015873f74" + ], + "markers": "python_version >= '3.8'", + "version": "==1.4.1" + }, "h11": { "hashes": [ "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", @@ -153,6 +339,102 @@ "markers": "platform_system != 'Windows'", "version": "==1.0.7" }, + "multidict": { + "hashes": [ + "sha256:01265f5e40f5a17f8241d52656ed27192be03bfa8764d88e8220141d1e4b3556", + "sha256:0275e35209c27a3f7951e1ce7aaf93ce0d163b28948444bec61dd7badc6d3f8c", + "sha256:04bde7a7b3de05732a4eb39c94574db1ec99abb56162d6c520ad26f83267de29", + "sha256:04da1bb8c8dbadf2a18a452639771951c662c5ad03aefe4884775454be322c9b", + "sha256:09a892e4a9fb47331da06948690ae38eaa2426de97b4ccbfafbdcbe5c8f37ff8", + "sha256:0d63c74e3d7ab26de115c49bffc92cc77ed23395303d496eae515d4204a625e7", + "sha256:107c0cdefe028703fb5dafe640a409cb146d44a6ae201e55b35a4af8e95457dd", + "sha256:141b43360bfd3bdd75f15ed811850763555a251e38b2405967f8e25fb43f7d40", + "sha256:14c2976aa9038c2629efa2c148022ed5eb4cb939e15ec7aace7ca932f48f9ba6", + "sha256:19fe01cea168585ba0f678cad6f58133db2aa14eccaf22f88e4a6dccadfad8b3", + "sha256:1d147090048129ce3c453f0292e7697d333db95e52616b3793922945804a433c", + "sha256:1d9ea7a7e779d7a3561aade7d596649fbecfa5c08a7674b11b423783217933f9", + "sha256:215ed703caf15f578dca76ee6f6b21b7603791ae090fbf1ef9d865571039ade5", + "sha256:21fd81c4ebdb4f214161be351eb5bcf385426bf023041da2fd9e60681f3cebae", + "sha256:220dd781e3f7af2c2c1053da9fa96d9cf3072ca58f057f4c5adaaa1cab8fc442", + "sha256:228b644ae063c10e7f324ab1ab6b548bdf6f8b47f3ec234fef1093bc2735e5f9", + "sha256:29bfeb0dff5cb5fdab2023a7a9947b3b4af63e9c47cae2a10ad58394b517fddc", + "sha256:2f4848aa3baa109e6ab81fe2006c77ed4d3cd1e0ac2c1fbddb7b1277c168788c", + "sha256:2faa5ae9376faba05f630d7e5e6be05be22913782b927b19d12b8145968a85ea", + "sha256:2ffc42c922dbfddb4a4c3b438eb056828719f07608af27d163191cb3e3aa6cc5", + "sha256:37b15024f864916b4951adb95d3a80c9431299080341ab9544ed148091b53f50", + "sha256:3cc2ad10255f903656017363cd59436f2111443a76f996584d1077e43ee51182", + "sha256:3d25f19500588cbc47dc19081d78131c32637c25804df8414463ec908631e453", + "sha256:403c0911cd5d5791605808b942c88a8155c2592e05332d2bf78f18697a5fa15e", + "sha256:411bf8515f3be9813d06004cac41ccf7d1cd46dfe233705933dd163b60e37600", + "sha256:425bf820055005bfc8aa9a0b99ccb52cc2f4070153e34b701acc98d201693733", + "sha256:435a0984199d81ca178b9ae2c26ec3d49692d20ee29bc4c11a2a8d4514c67eda", + "sha256:4a6a4f196f08c58c59e0b8ef8ec441d12aee4125a7d4f4fef000ccb22f8d7241", + "sha256:4cc0ef8b962ac7a5e62b9e826bd0cd5040e7d401bc45a6835910ed699037a461", + "sha256:51d035609b86722963404f711db441cf7134f1889107fb171a970c9701f92e1e", + "sha256:53689bb4e102200a4fafa9de9c7c3c212ab40a7ab2c8e474491914d2305f187e", + "sha256:55205d03e8a598cfc688c71ca8ea5f66447164efff8869517f175ea632c7cb7b", + "sha256:5c0631926c4f58e9a5ccce555ad7747d9a9f8b10619621f22f9635f069f6233e", + "sha256:5cb241881eefd96b46f89b1a056187ea8e9ba14ab88ba632e68d7a2ecb7aadf7", + "sha256:60d698e8179a42ec85172d12f50b1668254628425a6bd611aba022257cac1386", + "sha256:612d1156111ae11d14afaf3a0669ebf6c170dbb735e510a7438ffe2369a847fd", + "sha256:6214c5a5571802c33f80e6c84713b2c79e024995b9c5897f794b43e714daeec9", + "sha256:6939c95381e003f54cd4c5516740faba40cf5ad3eeff460c3ad1d3e0ea2549bf", + "sha256:69db76c09796b313331bb7048229e3bee7928eb62bab5e071e9f7fcc4879caee", + "sha256:6bf7a982604375a8d49b6cc1b781c1747f243d91b81035a9b43a2126c04766f5", + "sha256:766c8f7511df26d9f11cd3a8be623e59cca73d44643abab3f8c8c07620524e4a", + "sha256:76c0de87358b192de7ea9649beb392f107dcad9ad27276324c24c91774ca5271", + "sha256:76f067f5121dcecf0d63a67f29080b26c43c71a98b10c701b0677e4a065fbd54", + "sha256:7901c05ead4b3fb75113fb1dd33eb1253c6d3ee37ce93305acd9d38e0b5f21a4", + "sha256:79660376075cfd4b2c80f295528aa6beb2058fd289f4c9252f986751a4cd0496", + "sha256:79a6d2ba910adb2cbafc95dad936f8b9386e77c84c35bc0add315b856d7c3abb", + "sha256:7afcdd1fc07befad18ec4523a782cde4e93e0a2bf71239894b8d61ee578c1319", + "sha256:7be7047bd08accdb7487737631d25735c9a04327911de89ff1b26b81745bd4e3", + "sha256:7c6390cf87ff6234643428991b7359b5f59cc15155695deb4eda5c777d2b880f", + "sha256:7df704ca8cf4a073334e0427ae2345323613e4df18cc224f647f251e5e75a527", + "sha256:85f67aed7bb647f93e7520633d8f51d3cbc6ab96957c71272b286b2f30dc70ed", + "sha256:896ebdcf62683551312c30e20614305f53125750803b614e9e6ce74a96232604", + "sha256:92d16a3e275e38293623ebf639c471d3e03bb20b8ebb845237e0d3664914caef", + "sha256:99f60d34c048c5c2fabc766108c103612344c46e35d4ed9ae0673d33c8fb26e8", + "sha256:9fe7b0653ba3d9d65cbe7698cca585bf0f8c83dbbcc710db9c90f478e175f2d5", + "sha256:a3145cb08d8625b2d3fee1b2d596a8766352979c9bffe5d7833e0503d0f0b5e5", + "sha256:aeaf541ddbad8311a87dd695ed9642401131ea39ad7bc8cf3ef3967fd093b626", + "sha256:b55358304d7a73d7bdf5de62494aaf70bd33015831ffd98bc498b433dfe5b10c", + "sha256:b82cc8ace10ab5bd93235dfaab2021c70637005e1ac787031f4d1da63d493c1d", + "sha256:c0868d64af83169e4d4152ec612637a543f7a336e4a307b119e98042e852ad9c", + "sha256:c1c1496e73051918fcd4f58ff2e0f2f3066d1c76a0c6aeffd9b45d53243702cc", + "sha256:c9bf56195c6bbd293340ea82eafd0071cb3d450c703d2c93afb89f93b8386ccc", + "sha256:cbebcd5bcaf1eaf302617c114aa67569dd3f090dd0ce8ba9e35e9985b41ac35b", + "sha256:cd6c8fca38178e12c00418de737aef1261576bd1b6e8c6134d3e729a4e858b38", + "sha256:ceb3b7e6a0135e092de86110c5a74e46bda4bd4fbfeeb3a3bcec79c0f861e450", + "sha256:cf590b134eb70629e350691ecca88eac3e3b8b3c86992042fb82e3cb1830d5e1", + "sha256:d3eb1ceec286eba8220c26f3b0096cf189aea7057b6e7b7a2e60ed36b373b77f", + "sha256:d65f25da8e248202bd47445cec78e0025c0fe7582b23ec69c3b27a640dd7a8e3", + "sha256:d6f6d4f185481c9669b9447bf9d9cf3b95a0e9df9d169bbc17e363b7d5487755", + "sha256:d84a5c3a5f7ce6db1f999fb9438f686bc2e09d38143f2d93d8406ed2dd6b9226", + "sha256:d946b0a9eb8aaa590df1fe082cee553ceab173e6cb5b03239716338629c50c7a", + "sha256:dce1c6912ab9ff5f179eaf6efe7365c1f425ed690b03341911bf4939ef2f3046", + "sha256:de170c7b4fe6859beb8926e84f7d7d6c693dfe8e27372ce3b76f01c46e489fcf", + "sha256:e02021f87a5b6932fa6ce916ca004c4d441509d33bbdbeca70d05dff5e9d2479", + "sha256:e030047e85cbcedbfc073f71836d62dd5dadfbe7531cae27789ff66bc551bd5e", + "sha256:e0e79d91e71b9867c73323a3444724d496c037e578a0e1755ae159ba14f4f3d1", + "sha256:e4428b29611e989719874670fd152b6625500ad6c686d464e99f5aaeeaca175a", + "sha256:e4972624066095e52b569e02b5ca97dbd7a7ddd4294bf4e7247d52635630dd83", + "sha256:e7be68734bd8c9a513f2b0cfd508802d6609da068f40dc57d4e3494cefc92929", + "sha256:e8e94e6912639a02ce173341ff62cc1201232ab86b8a8fcc05572741a5dc7d93", + "sha256:ea1456df2a27c73ce51120fa2f519f1bea2f4a03a917f4a43c8707cf4cbbae1a", + "sha256:ebd8d160f91a764652d3e51ce0d2956b38efe37c9231cd82cfc0bed2e40b581c", + "sha256:eca2e9d0cc5a889850e9bbd68e98314ada174ff6ccd1129500103df7a94a7a44", + "sha256:edd08e6f2f1a390bf137080507e44ccc086353c8e98c657e666c017718561b89", + "sha256:f285e862d2f153a70586579c15c44656f888806ed0e5b56b64489afe4a2dbfba", + "sha256:f2a1dee728b52b33eebff5072817176c172050d44d67befd681609b4746e1c2e", + "sha256:f7e301075edaf50500f0b341543c41194d8df3ae5caf4702f2095f3ca73dd8da", + "sha256:fb616be3538599e797a2017cccca78e354c767165e8858ab5116813146041a24", + "sha256:fce28b3c8a81b6b36dfac9feb1de115bab619b3c13905b419ec71d03a3fc1423", + "sha256:fe5d7785250541f7f5019ab9cba2c71169dc7d74d0f45253f8313f436458a4ef" + ], + "markers": "python_version >= '3.7'", + "version": "==6.0.5" + }, "numpy": { "hashes": [ "sha256:07a8c89a04997625236c5ecb7afe35a02af3896c8aa01890a849913a2309c676", @@ -518,6 +800,102 @@ "markers": "python_full_version >= '3.7.0'", "version": "==1.2.0" }, + "yarl": { + "hashes": [ + "sha256:008d3e808d03ef28542372d01057fd09168419cdc8f848efe2804f894ae03e51", + "sha256:03caa9507d3d3c83bca08650678e25364e1843b484f19986a527630ca376ecce", + "sha256:07574b007ee20e5c375a8fe4a0789fad26db905f9813be0f9fef5a68080de559", + "sha256:09efe4615ada057ba2d30df871d2f668af661e971dfeedf0c159927d48bbeff0", + "sha256:0d2454f0aef65ea81037759be5ca9947539667eecebca092733b2eb43c965a81", + "sha256:0e9d124c191d5b881060a9e5060627694c3bdd1fe24c5eecc8d5d7d0eb6faabc", + "sha256:18580f672e44ce1238b82f7fb87d727c4a131f3a9d33a5e0e82b793362bf18b4", + "sha256:1f23e4fe1e8794f74b6027d7cf19dc25f8b63af1483d91d595d4a07eca1fb26c", + "sha256:206a55215e6d05dbc6c98ce598a59e6fbd0c493e2de4ea6cc2f4934d5a18d130", + "sha256:23d32a2594cb5d565d358a92e151315d1b2268bc10f4610d098f96b147370136", + "sha256:26a1dc6285e03f3cc9e839a2da83bcbf31dcb0d004c72d0730e755b33466c30e", + "sha256:29e0f83f37610f173eb7e7b5562dd71467993495e568e708d99e9d1944f561ec", + "sha256:2b134fd795e2322b7684155b7855cc99409d10b2e408056db2b93b51a52accc7", + "sha256:2d47552b6e52c3319fede1b60b3de120fe83bde9b7bddad11a69fb0af7db32f1", + "sha256:357495293086c5b6d34ca9616a43d329317feab7917518bc97a08f9e55648455", + "sha256:35a2b9396879ce32754bd457d31a51ff0a9d426fd9e0e3c33394bf4b9036b099", + "sha256:3777ce5536d17989c91696db1d459574e9a9bd37660ea7ee4d3344579bb6f129", + "sha256:3986b6f41ad22988e53d5778f91855dc0399b043fc8946d4f2e68af22ee9ff10", + "sha256:44d8ffbb9c06e5a7f529f38f53eda23e50d1ed33c6c869e01481d3fafa6b8142", + "sha256:49a180c2e0743d5d6e0b4d1a9e5f633c62eca3f8a86ba5dd3c471060e352ca98", + "sha256:4aa9741085f635934f3a2583e16fcf62ba835719a8b2b28fb2917bb0537c1dfa", + "sha256:4b21516d181cd77ebd06ce160ef8cc2a5e9ad35fb1c5930882baff5ac865eee7", + "sha256:4b3c1ffe10069f655ea2d731808e76e0f452fc6c749bea04781daf18e6039525", + "sha256:4c7d56b293cc071e82532f70adcbd8b61909eec973ae9d2d1f9b233f3d943f2c", + "sha256:4e9035df8d0880b2f1c7f5031f33f69e071dfe72ee9310cfc76f7b605958ceb9", + "sha256:54525ae423d7b7a8ee81ba189f131054defdb122cde31ff17477951464c1691c", + "sha256:549d19c84c55d11687ddbd47eeb348a89df9cb30e1993f1b128f4685cd0ebbf8", + "sha256:54beabb809ffcacbd9d28ac57b0db46e42a6e341a030293fb3185c409e626b8b", + "sha256:566db86717cf8080b99b58b083b773a908ae40f06681e87e589a976faf8246bf", + "sha256:5a2e2433eb9344a163aced6a5f6c9222c0786e5a9e9cac2c89f0b28433f56e23", + "sha256:5aef935237d60a51a62b86249839b51345f47564208c6ee615ed2a40878dccdd", + "sha256:604f31d97fa493083ea21bd9b92c419012531c4e17ea6da0f65cacdcf5d0bd27", + "sha256:63b20738b5aac74e239622d2fe30df4fca4942a86e31bf47a81a0e94c14df94f", + "sha256:686a0c2f85f83463272ddffd4deb5e591c98aac1897d65e92319f729c320eece", + "sha256:6a962e04b8f91f8c4e5917e518d17958e3bdee71fd1d8b88cdce74dd0ebbf434", + "sha256:6ad6d10ed9b67a382b45f29ea028f92d25bc0bc1daf6c5b801b90b5aa70fb9ec", + "sha256:6f5cb257bc2ec58f437da2b37a8cd48f666db96d47b8a3115c29f316313654ff", + "sha256:6fe79f998a4052d79e1c30eeb7d6c1c1056ad33300f682465e1b4e9b5a188b78", + "sha256:7855426dfbddac81896b6e533ebefc0af2f132d4a47340cee6d22cac7190022d", + "sha256:7d5aaac37d19b2904bb9dfe12cdb08c8443e7ba7d2852894ad448d4b8f442863", + "sha256:801e9264d19643548651b9db361ce3287176671fb0117f96b5ac0ee1c3530d53", + "sha256:81eb57278deb6098a5b62e88ad8281b2ba09f2f1147c4767522353eaa6260b31", + "sha256:824d6c50492add5da9374875ce72db7a0733b29c2394890aef23d533106e2b15", + "sha256:8397a3817d7dcdd14bb266283cd1d6fc7264a48c186b986f32e86d86d35fbac5", + "sha256:848cd2a1df56ddbffeb375535fb62c9d1645dde33ca4d51341378b3f5954429b", + "sha256:84fc30f71689d7fc9168b92788abc977dc8cefa806909565fc2951d02f6b7d57", + "sha256:8619d6915b3b0b34420cf9b2bb6d81ef59d984cb0fde7544e9ece32b4b3043c3", + "sha256:8a854227cf581330ffa2c4824d96e52ee621dd571078a252c25e3a3b3d94a1b1", + "sha256:8be9e837ea9113676e5754b43b940b50cce76d9ed7d2461df1af39a8ee674d9f", + "sha256:928cecb0ef9d5a7946eb6ff58417ad2fe9375762382f1bf5c55e61645f2c43ad", + "sha256:957b4774373cf6f709359e5c8c4a0af9f6d7875db657adb0feaf8d6cb3c3964c", + "sha256:992f18e0ea248ee03b5a6e8b3b4738850ae7dbb172cc41c966462801cbf62cf7", + "sha256:9fc5fc1eeb029757349ad26bbc5880557389a03fa6ada41703db5e068881e5f2", + "sha256:a00862fb23195b6b8322f7d781b0dc1d82cb3bcac346d1e38689370cc1cc398b", + "sha256:a3a6ed1d525bfb91b3fc9b690c5a21bb52de28c018530ad85093cc488bee2dd2", + "sha256:a6327976c7c2f4ee6816eff196e25385ccc02cb81427952414a64811037bbc8b", + "sha256:a7409f968456111140c1c95301cadf071bd30a81cbd7ab829169fb9e3d72eae9", + "sha256:a825ec844298c791fd28ed14ed1bffc56a98d15b8c58a20e0e08c1f5f2bea1be", + "sha256:a8c1df72eb746f4136fe9a2e72b0c9dc1da1cbd23b5372f94b5820ff8ae30e0e", + "sha256:a9bd00dc3bc395a662900f33f74feb3e757429e545d831eef5bb280252631984", + "sha256:aa102d6d280a5455ad6a0f9e6d769989638718e938a6a0a2ff3f4a7ff8c62cc4", + "sha256:aaaea1e536f98754a6e5c56091baa1b6ce2f2700cc4a00b0d49eca8dea471074", + "sha256:ad4d7a90a92e528aadf4965d685c17dacff3df282db1121136c382dc0b6014d2", + "sha256:b8477c1ee4bd47c57d49621a062121c3023609f7a13b8a46953eb6c9716ca392", + "sha256:ba6f52cbc7809cd8d74604cce9c14868306ae4aa0282016b641c661f981a6e91", + "sha256:bac8d525a8dbc2a1507ec731d2867025d11ceadcb4dd421423a5d42c56818541", + "sha256:bef596fdaa8f26e3d66af846bbe77057237cb6e8efff8cd7cc8dff9a62278bbf", + "sha256:c0ec0ed476f77db9fb29bca17f0a8fcc7bc97ad4c6c1d8959c507decb22e8572", + "sha256:c38c9ddb6103ceae4e4498f9c08fac9b590c5c71b0370f98714768e22ac6fa66", + "sha256:c7224cab95645c7ab53791022ae77a4509472613e839dab722a72abe5a684575", + "sha256:c74018551e31269d56fab81a728f683667e7c28c04e807ba08f8c9e3bba32f14", + "sha256:ca06675212f94e7a610e85ca36948bb8fc023e458dd6c63ef71abfd482481aa5", + "sha256:d1d2532b340b692880261c15aee4dc94dd22ca5d61b9db9a8a361953d36410b1", + "sha256:d25039a474c4c72a5ad4b52495056f843a7ff07b632c1b92ea9043a3d9950f6e", + "sha256:d5ff2c858f5f6a42c2a8e751100f237c5e869cbde669a724f2062d4c4ef93551", + "sha256:d7d7f7de27b8944f1fee2c26a88b4dabc2409d2fea7a9ed3df79b67277644e17", + "sha256:d7eeb6d22331e2fd42fce928a81c697c9ee2d51400bd1a28803965883e13cead", + "sha256:d8a1c6c0be645c745a081c192e747c5de06e944a0d21245f4cf7c05e457c36e0", + "sha256:d8b889777de69897406c9fb0b76cdf2fd0f31267861ae7501d93003d55f54fbe", + "sha256:d9e09c9d74f4566e905a0b8fa668c58109f7624db96a2171f21747abc7524234", + "sha256:db8e58b9d79200c76956cefd14d5c90af54416ff5353c5bfd7cbe58818e26ef0", + "sha256:ddb2a5c08a4eaaba605340fdee8fc08e406c56617566d9643ad8bf6852778fc7", + "sha256:e0381b4ce23ff92f8170080c97678040fc5b08da85e9e292292aba67fdac6c34", + "sha256:e23a6d84d9d1738dbc6e38167776107e63307dfc8ad108e580548d1f2c587f42", + "sha256:e516dc8baf7b380e6c1c26792610230f37147bb754d6426462ab115a02944385", + "sha256:ea65804b5dc88dacd4a40279af0cdadcfe74b3e5b4c897aa0d81cf86927fee78", + "sha256:ec61d826d80fc293ed46c9dd26995921e3a82146feacd952ef0757236fc137be", + "sha256:ee04010f26d5102399bd17f8df8bc38dc7ccd7701dc77f4a68c5b8d733406958", + "sha256:f3bc6af6e2b8f92eced34ef6a96ffb248e863af20ef4fde9448cc8c9b858b749", + "sha256:f7d6b36dd2e029b6bcb8a13cf19664c7b8e19ab3a58e0fefbb5b8461447ed5ec" + ], + "markers": "python_version >= '3.7'", + "version": "==1.9.4" + }, "zipp": { "hashes": [ "sha256:957cfda87797e389580cb8b9e3870841ca991e2125350677b2ca83a0e99390a3", diff --git a/robot-server/robot_server/service/legacy/routers/settings.py b/robot-server/robot_server/service/legacy/routers/settings.py index 16a732ff97f..5d79053d696 100644 --- a/robot-server/robot_server/service/legacy/routers/settings.py +++ b/robot-server/robot_server/service/legacy/routers/settings.py @@ -1,7 +1,7 @@ -from dataclasses import asdict +import aiohttp import logging +from dataclasses import asdict from typing import cast, Any, Dict, List, Optional, Union - from starlette import status from fastapi import APIRouter, Depends @@ -32,7 +32,6 @@ from robot_server.errors.error_responses import LegacyErrorResponse from robot_server.hardware import ( get_hardware, - get_robot_type, get_robot_type_enum, get_ot2_hardware, ) @@ -64,6 +63,17 @@ router = APIRouter() +# TODO: (ba, 2024-04-11): We should have a proper IPC mechanism to talk between +# the servers instead of one off endpoint calls like these. +async def set_oem_mode_request(enable): + """PUT request to set the OEM Mode for the system server.""" + async with aiohttp.ClientSession() as session: + async with session.put( + "http://127.0.0.1:31950/system/oem_mode/enable", json={"enable": enable} + ) as resp: + return resp.status + + @router.post( path="/settings", summary="Change a setting", @@ -78,10 +88,17 @@ async def post_settings( update: AdvancedSettingRequest, hardware: HardwareControlAPI = Depends(get_hardware), - robot_type: str = Depends(get_robot_type), + robot_type: RobotTypeEnum = Depends(get_robot_type_enum), ) -> AdvancedSettingsResponse: """Update advanced setting (feature flag)""" try: + # send request to system server if this is the enableOEMMode setting + if update.id == "enableOEMMode" and robot_type == RobotTypeEnum.FLEX: + resp = await set_oem_mode_request(update.value) + if resp != 200: + # TODO: raise correct error here + raise Exception(f"Something went wrong setting OEM Mode. err: {resp}") + await advanced_settings.set_adv_setting(update.id, update.value) hardware.hardware_feature_flags = HardwareFeatureFlags.build_from_ff() await hardware.set_status_bar_enabled(ff.status_bar_enabled()) @@ -104,21 +121,15 @@ async def post_settings( response_model_exclude_unset=True, ) async def get_settings( - robot_type: str = Depends(get_robot_type), + robot_type: RobotTypeEnum = Depends(get_robot_type_enum), ) -> AdvancedSettingsResponse: """Get advanced setting (feature flags)""" return _create_settings_response(robot_type) -def _create_settings_response(robot_type: str) -> AdvancedSettingsResponse: +def _create_settings_response(robot_type: RobotTypeEnum) -> AdvancedSettingsResponse: """Create the feature flag settings response object""" - # TODO lc(8-10-2023) We should convert the robot type function to return - # the enum value directly. - if robot_type == "OT-2 Standard": - robot_type_enum = RobotTypeEnum.OT2 - else: - robot_type_enum = RobotTypeEnum.FLEX - data = advanced_settings.get_all_adv_settings(robot_type_enum) + data = advanced_settings.get_all_adv_settings(robot_type) if advanced_settings.is_restart_required(): links = Links(restart="/server/restart") diff --git a/system-server/settings_schema.json b/system-server/settings_schema.json index c16b2c49621..7916f39dcf9 100644 --- a/system-server/settings_schema.json +++ b/system-server/settings_schema.json @@ -9,6 +9,19 @@ "default": "/var/lib/opentrons-system-server/", "env_names": ["ot_system_server_persistence_directory"], "type": "string" + }, + "oem_mode_enabled": { + "title": "OEM Mode Enabled", + "description": "A flag used to change the default splash screen on system startup. If this flag is disabled (default), the Opentrons loading video will be shown. If this flag is enabled but `oem_mode_splash_custom` is not set, then the default OEM Mode splash screen will be shown. If this flag is enabled and `oem_mode_splash_custom` is set to a PNG filepath, the custom splash screen will be shown.", + "default": false, + "env_names": ["ot_system_server_oem_mode_enabled"], + "type": "bool" + }, + "oem_mode_splash_custom": { + "description": "The filepath of the PNG image used as the custom splash screen. Read the description of the `oem_mode_enabled` flag to know how the splash screen changes when the flag is enabled/disabled.", + "default": null, + "env_names": ["ot_system_server_oem_mode_splash_custom"], + "type": "string" } }, "additionalProperties": false diff --git a/system-server/system_server/settings/__init__.py b/system-server/system_server/settings/__init__.py index b2db58a6389..feae773340f 100644 --- a/system-server/system_server/settings/__init__.py +++ b/system-server/system_server/settings/__init__.py @@ -1,6 +1,10 @@ """system_server.settings: Provides an interface to get server settings.""" -from .settings import get_settings, SystemServerSettings +from .settings import ( + save_settings, + get_settings, + SystemServerSettings, +) -__all__ = ["get_settings", "SystemServerSettings"] +__all__ = ["save_settings", "get_settings", "SystemServerSettings"] diff --git a/system-server/system_server/settings/settings.py b/system-server/system_server/settings/settings.py index a042b76b91d..32e34079ebd 100644 --- a/system-server/system_server/settings/settings.py +++ b/system-server/system_server/settings/settings.py @@ -1,27 +1,20 @@ """System server configuration options.""" import typing -import logging from functools import lru_cache from pydantic import BaseSettings, Field -from dotenv import load_dotenv - -log = logging.getLogger(__name__) +from dotenv import load_dotenv, set_key @lru_cache(maxsize=1) def get_settings() -> "SystemServerSettings": """Get the settings.""" - update_from_dotenv() - return SystemServerSettings() - - -def update_from_dotenv() -> None: - """Get the location of the settings file.""" env = Environment().dot_env_path if env: load_dotenv(env) + return SystemServerSettings() + class Environment(BaseSettings): """Environment related settings.""" @@ -56,7 +49,44 @@ class SystemServerSettings(BaseSettings): ), ) + oem_mode_enabled: typing.Optional[bool] = Field( + default=False, + description=( + "A flag used to change the default splash screen on system startup." + " If this flag is disabled (default), the Opentrons loading video will be shown." + " If this flag is enabled but `oem_mode_splash_custom` is not set," + " then the default OEM Mode splash screen will be shown." + " If this flag is enabled and `oem_mode_splash_custom` is set to a" + " PNG filepath, the custom splash screen will be shown." + ), + ) + + oem_mode_splash_custom: typing.Optional[str] = Field( + default=None, + description=( + "The filepath of the PNG image used as the custom splash screen." + " Read the description of the `oem_mode_enabled` flag to know how" + " the splash screen changes when the flag is enabled/disabled." + ), + ) + class Config: """Prefix configuration for environment variables.""" + env_file = Environment().dot_env_path env_prefix = "OT_SYSTEM_SERVER_" + + +def save_settings(settings: SystemServerSettings) -> bool: + """Save the settings to the dotenv file.""" + env_path = Environment().dot_env_path + env_path = env_path or f"{settings.persistence_directory}/system.env" + prefix = settings.Config.env_prefix + try: + for key, val in settings.dict().items(): + name = f"{prefix}{key}" + value = str(val) if val is not None else "" + set_key(env_path, name, value) + return True + except (IOError, ValueError): + return False diff --git a/system-server/system_server/system/oem_mode/__init__.py b/system-server/system_server/system/oem_mode/__init__.py new file mode 100644 index 00000000000..ddbd3555ebf --- /dev/null +++ b/system-server/system_server/system/oem_mode/__init__.py @@ -0,0 +1,5 @@ +"""OEM Mode endpoint.""" + +from .router import oem_mode_router + +__all__ = ["oem_mode_router"] diff --git a/system-server/system_server/system/oem_mode/dependencies.py b/system-server/system_server/system/oem_mode/dependencies.py new file mode 100644 index 00000000000..b59bb825024 --- /dev/null +++ b/system-server/system_server/system/oem_mode/dependencies.py @@ -0,0 +1,21 @@ +"""Dependencies for /system/register endpoints.""" +from system_server.jwt import Registrant +from fastapi import Query + + +def create_registrant( + subject: str = Query( + ..., description="Identifies the human intending to register with the robot" + ), + agent: str = Query(..., description="Identifies the app type making the request"), + agentId: str = Query( + ..., description="A unique identifier for the instance of the agent" + ), +) -> Registrant: + """Define a unique Registrant to create a registration token for. + + A registrant is defined by a set of unique identifiers that remain + persistent indefinitely for the same person using the same method of + access to the system. + """ + return Registrant(subject=subject, agent=agent, agent_id=agentId) diff --git a/system-server/system_server/system/oem_mode/models.py b/system-server/system_server/system/oem_mode/models.py new file mode 100644 index 00000000000..192e1ce4fa6 --- /dev/null +++ b/system-server/system_server/system/oem_mode/models.py @@ -0,0 +1,9 @@ +"""Models for /system/oem_mode.""" + +from pydantic import BaseModel, Field + + +class EnableOEMMode(BaseModel): + """Enable OEM Mode model.""" + + enable: bool = Field(..., description="Enable or Disable OEM Mode.") diff --git a/system-server/system_server/system/oem_mode/router.py b/system-server/system_server/system/oem_mode/router.py new file mode 100644 index 00000000000..c8c6d96240b --- /dev/null +++ b/system-server/system_server/system/oem_mode/router.py @@ -0,0 +1,37 @@ +"""Router for /system/register endpoint.""" + +from fastapi import APIRouter, Depends, status, Response +from .models import EnableOEMMode +from ...settings import SystemServerSettings, get_settings, save_settings + + +oem_mode_router = APIRouter() + + +@oem_mode_router.put( + "/system/oem_mode/enable", + summary="Enable or Disable OEM Mode for this robot.", + responses={ + status.HTTP_200_OK: {"message": "OEM Mode changed successfully."}, + status.HTTP_400_BAD_REQUEST: {"message": "OEM Mode did not changed."}, + status.HTTP_500_INTERNAL_SERVER_ERROR: { + "message": "OEM Mode unhandled exception." + }, + }, +) +async def enable_oem_mode_endpoint( + response: Response, + enableRequest: EnableOEMMode, + settings: SystemServerSettings = Depends(get_settings), +) -> Response: + """Router for /system/oem_mode/enable endpoint.""" + enable = enableRequest.enable + try: + settings.oem_mode_enabled = enable + success = save_settings(settings) + response.status_code = ( + status.HTTP_200_OK if success else status.HTTP_400_BAD_REQUEST + ) + except Exception: + response.status_code = status.HTTP_500_INTERNAL_SERVER_ERROR + return response diff --git a/system-server/system_server/system/router.py b/system-server/system_server/system/router.py index 0ae868c5f51..b138a1d0ed6 100644 --- a/system-server/system_server/system/router.py +++ b/system-server/system_server/system/router.py @@ -3,6 +3,7 @@ from .register.router import register_router from .authorize.router import authorize_router from .connected.router import connected_router +from .oem_mode.router import oem_mode_router system_router = APIRouter() @@ -11,3 +12,5 @@ system_router.include_router(router=authorize_router) system_router.include_router(router=connected_router) + +system_router.include_router(router=oem_mode_router) diff --git a/system-server/tests/integration/test_oem_mode.tavern.yaml b/system-server/tests/integration/test_oem_mode.tavern.yaml new file mode 100644 index 00000000000..399422c96b8 --- /dev/null +++ b/system-server/tests/integration/test_oem_mode.tavern.yaml @@ -0,0 +1,37 @@ +--- +test_name: PUT Enable OEM Mode +marks: + - usefixtures: + - run_server +stages: + - name: PUT first request + request: &enable_oem_mode_first + url: "{host:s}:{port:d}/system/oem_mode/enable" + json: + enable: true + method: PUT + headers: + content-type: application/json + response: + status_code: 200 + - name: PUT second request + request: &enable_oem_mode_second + url: "{host:s}:{port:d}/system/oem_mode/enable" + json: + enable: false + method: PUT + headers: + content-type: application/json + response: + status_code: 200 + - name: PUT third request + request: &enable_oem_mode_third + url: "{host:s}:{port:d}/system/oem_mode/enable" + json: + wrong_key: false + method: PUT + headers: + content-type: application/json + response: + status_code: 422 + From f206b14e79eeb13c1d2a6c088a11ab69b3ad3006 Mon Sep 17 00:00:00 2001 From: Brent Hagen Date: Fri, 12 Apr 2024 15:30:53 -0400 Subject: [PATCH 110/194] feat(api-client,app,react-api-client): implement ODD anonymous localization provider (#14741) implements a localization provider for the ODD app that substitutes anonymous translation values when the "enableOEMMode" robot setting is on. pushes the localization provider further down the DOM to be within the ODD api host provider for the robot settings request, and splits out a separate instance for the desktop app. refactors the `OnDeviceDisplayApp` component a bit to avoid rerenders of providers - currently the entire ODD app rerenders on every route change because of the scroll ref. refactors the initial loading screen out of the route tree and up the DOM to block localization provider render until the robot-server api is up adds api client and react api client functions for robot settings get requests. all translation keys that reference "opentrons" or "flex" are moved to the new `branded.json` and `anonymous.json` files. anonymous copy will be finalized in PLAT-243 closes PLAT-265 --- api-client/src/robot/getRobotSettings.ts | 11 ++ api-client/src/robot/index.ts | 5 + api-client/src/robot/types.ts | 15 +++ app/src/App/DesktopApp.tsx | 82 ++++++------ app/src/App/OnDeviceDisplayApp.tsx | 125 +++++++++++------- app/src/App/OnDeviceDisplayAppFallback.tsx | 6 +- .../App/__tests__/OnDeviceDisplayApp.test.tsx | 39 +++++- app/src/LocalizationProvider.tsx | 60 +++++++++ app/src/assets/localization/en/anonymous.json | 72 ++++++++++ .../assets/localization/en/app_settings.json | 28 +--- app/src/assets/localization/en/branded.json | 72 ++++++++++ .../localization/en/device_details.json | 4 - .../localization/en/device_settings.json | 23 +--- .../localization/en/devices_landing.json | 1 - .../localization/en/firmware_update.json | 1 - .../localization/en/gripper_wizard_flows.json | 7 - app/src/assets/localization/en/index.ts | 16 +-- .../localization/en/labware_landing.json | 2 - .../en/labware_position_check.json | 3 - .../localization/en/module_wizard_flows.json | 2 - .../en/more_network_and_system.json | 9 -- .../assets/localization/en/more_panel.json | 6 - .../localization/en/pipette_wizard_flows.json | 1 - .../localization/en/protocol_calibration.json | 22 --- .../assets/localization/en/protocol_info.json | 6 - .../assets/localization/en/protocol_list.json | 1 - .../localization/en/protocol_setup.json | 10 -- .../en/robot_advanced_settings.json | 18 --- .../localization/en/robot_calibration.json | 4 - .../localization/en/robot_connection.json | 32 ----- .../assets/localization/en/robot_info.json | 8 -- .../assets/localization/en/run_details.json | 3 - app/src/assets/localization/en/shared.json | 3 - app/src/i18n.ts | 78 +++++------ app/src/index.tsx | 6 +- .../AdvancedSettings/OverridePathToPython.tsx | 4 +- .../ShowLabwareOffsetSnippets.tsx | 4 +- app/src/organisms/Alerts/AlertsModal.tsx | 16 ++- .../AppSettings/ConnectRobotSlideout.tsx | 4 +- .../AppSettings/PreviousVersionModal.tsx | 8 +- .../AskForCalibrationBlockModal.tsx | 4 +- .../CalibrationPanels/ChooseTipRack.tsx | 8 +- .../AvailableRobotOption.tsx | 4 +- .../ConfigFormResetButton.tsx | 4 +- .../DeckFixtureSetupInstructionsModal.tsx | 4 +- .../ConnectionTroubleshootingModal.tsx | 2 +- .../Devices/ProtocolRun/RunFailedModal.tsx | 4 +- .../SetupLabware/SecureLabwareModal.tsx | 8 +- .../HowLPCWorksModal.tsx | 4 +- .../AdvancedTabSlideouts/DeviceResetModal.tsx | 4 +- .../AdvancedTab/RobotServerVersion.tsx | 4 +- .../AdvancedTab/SoftwareUpdateModal.tsx | 121 ----------------- .../AdvancedTab/UpdateRobotSoftware.tsx | 6 +- .../AdvancedTab/UseOlderProtocol.tsx | 4 +- .../__tests__/SoftwareUpdateModal.test.tsx | 99 -------------- .../RobotSettings/AdvancedTab/index.ts | 1 - .../ConnectNetwork/DisconnectModal.tsx | 4 +- .../RobotSettings/RobotSettingsPrivacy.tsx | 6 +- .../UpdateBuildroot/UpdateRobotModal.tsx | 2 +- .../EmergencyStop/EstopPressedModal.tsx | 6 +- .../UpdateResultsModal.tsx | 4 +- .../GripperCard/AboutGripperSlideout.tsx | 4 +- .../GripperWizardFlows/BeforeBeginning.tsx | 4 +- .../GripperWizardFlows/MountGripper.tsx | 4 +- .../organisms/GripperWizardFlows/Success.tsx | 12 +- .../GripperWizardFlows/UnmountGripper.tsx | 6 +- app/src/organisms/LabwareCard/index.tsx | 4 +- app/src/organisms/LabwareDetails/index.tsx | 4 +- app/src/organisms/ModuleCard/ErrorInfo.tsx | 4 +- .../organisms/ModuleCard/ModuleSetupModal.tsx | 4 +- .../ModuleWizardFlows/BeforeBeginning.tsx | 2 +- app/src/organisms/ModuleWizardFlows/index.tsx | 2 +- .../AlternativeSecurityTypeModal.tsx | 4 +- .../RunningProtocol/RunFailedModal.tsx | 4 +- .../PipetteWizardFlows/ProbeNotAttached.tsx | 10 +- .../organisms/PipetteWizardFlows/Results.tsx | 9 +- .../SetupInstructionsModal.tsx | 6 +- .../RobotSettingsDashboard/Privacy.tsx | 6 +- .../RobotSystemVersionModal.tsx | 2 +- .../organisms/TakeoverModal/TakeoverModal.tsx | 6 +- app/src/organisms/UpdateAppModal/index.tsx | 11 +- app/src/organisms/UpdateRobotBanner/index.tsx | 4 +- app/src/pages/AppSettings/GeneralSettings.tsx | 8 +- app/src/pages/AppSettings/PrivacySettings.tsx | 2 +- app/src/pages/ConnectViaUSB/index.tsx | 6 +- .../__tests__/InitialLoadingScreen.test.tsx | 18 ++- app/src/pages/InitialLoadingScreen/index.tsx | 35 ++--- app/src/pages/Labware/hooks.tsx | 4 +- app/src/pages/NetworkSetupMenu/index.tsx | 6 +- .../pages/ProtocolDashboard/NoProtocols.tsx | 4 +- .../pages/ProtocolDashboard/ProtocolCard.tsx | 6 +- .../RobotDashboard/AnalyticsOptInModal.tsx | 4 +- .../RobotSettingsList.tsx | 8 +- app/src/pages/Welcome/index.tsx | 4 +- app/src/redux/robot-settings/types.ts | 23 ++-- .../__tests__/useRobotSettingsQuery.test.tsx | 81 ++++++++++++ react-api-client/src/robot/index.ts | 1 + .../src/robot/useRobotSettingsQuery.ts | 21 +++ 98 files changed, 722 insertions(+), 741 deletions(-) create mode 100644 api-client/src/robot/getRobotSettings.ts create mode 100644 app/src/LocalizationProvider.tsx create mode 100644 app/src/assets/localization/en/anonymous.json create mode 100644 app/src/assets/localization/en/branded.json delete mode 100644 app/src/assets/localization/en/more_network_and_system.json delete mode 100644 app/src/assets/localization/en/more_panel.json delete mode 100644 app/src/assets/localization/en/protocol_calibration.json delete mode 100644 app/src/assets/localization/en/robot_advanced_settings.json delete mode 100644 app/src/assets/localization/en/robot_connection.json delete mode 100644 app/src/assets/localization/en/robot_info.json delete mode 100644 app/src/organisms/Devices/RobotSettings/AdvancedTab/SoftwareUpdateModal.tsx delete mode 100644 app/src/organisms/Devices/RobotSettings/AdvancedTab/__tests__/SoftwareUpdateModal.test.tsx create mode 100644 react-api-client/src/robot/__tests__/useRobotSettingsQuery.test.tsx create mode 100644 react-api-client/src/robot/useRobotSettingsQuery.ts diff --git a/api-client/src/robot/getRobotSettings.ts b/api-client/src/robot/getRobotSettings.ts new file mode 100644 index 00000000000..ffe0014fcb0 --- /dev/null +++ b/api-client/src/robot/getRobotSettings.ts @@ -0,0 +1,11 @@ +import { GET, request } from '../request' + +import type { ResponsePromise } from '../request' +import type { HostConfig } from '../types' +import type { RobotSettingsResponse } from './types' + +export function getRobotSettings( + config: HostConfig +): ResponsePromise { + return request(GET, '/settings', null, config) +} diff --git a/api-client/src/robot/index.ts b/api-client/src/robot/index.ts index 96ef28165b0..588a2f7a80e 100644 --- a/api-client/src/robot/index.ts +++ b/api-client/src/robot/index.ts @@ -3,11 +3,16 @@ export { getEstopStatus } from './getEstopStatus' export { acknowledgeEstopDisengage } from './acknowledgeEstopDisengage' export { getLights } from './getLights' export { setLights } from './setLights' +export { getRobotSettings } from './getRobotSettings' + export type { DoorStatus, EstopPhysicalStatus, EstopState, EstopStatus, Lights, + RobotSettings, + RobotSettingsField, + RobotSettingsResponse, SetLightsData, } from './types' diff --git a/api-client/src/robot/types.ts b/api-client/src/robot/types.ts index 00d887b9c4e..088d78fa5c8 100644 --- a/api-client/src/robot/types.ts +++ b/api-client/src/robot/types.ts @@ -27,3 +27,18 @@ export interface Lights { export interface SetLightsData { on: boolean } + +export interface RobotSettingsField { + id: string + title: string + description: string + value: boolean | null + restart_required?: boolean +} + +export type RobotSettings = RobotSettingsField[] + +export interface RobotSettingsResponse { + settings: RobotSettings + links?: { restart?: string } +} diff --git a/app/src/App/DesktopApp.tsx b/app/src/App/DesktopApp.tsx index f42ef7e0e80..ffa50727da1 100644 --- a/app/src/App/DesktopApp.tsx +++ b/app/src/App/DesktopApp.tsx @@ -1,6 +1,7 @@ import * as React from 'react' import { Redirect, Route, Switch, useRouteMatch } from 'react-router-dom' import { ErrorBoundary } from 'react-error-boundary' +import { I18nextProvider } from 'react-i18next' import { Box, @@ -11,6 +12,7 @@ import { import { ApiHostProvider } from '@opentrons/react-api-client' import NiceModal from '@ebay/nice-modal-react' +import { i18n } from '../i18n' import { Alerts } from '../organisms/Alerts' import { Breadcrumbs } from '../organisms/Breadcrumbs' import { ToasterOven } from '../organisms/ToasterOven' @@ -101,45 +103,47 @@ export const DesktopApp = (): JSX.Element => { return ( - - - - - - - - {desktopRoutes.map( - ({ Component, exact, path }: RouteProps) => { - return ( - - - - - - - - ) - } - )} - - - - - - - - + + + + + + + + + {desktopRoutes.map( + ({ Component, exact, path }: RouteProps) => { + return ( + + + + + + + + ) + } + )} + + + + + + + + + ) } diff --git a/app/src/App/OnDeviceDisplayApp.tsx b/app/src/App/OnDeviceDisplayApp.tsx index c9923e1aea3..835e005d256 100644 --- a/app/src/App/OnDeviceDisplayApp.tsx +++ b/app/src/App/OnDeviceDisplayApp.tsx @@ -16,6 +16,7 @@ import { ApiHostProvider } from '@opentrons/react-api-client' import NiceModal from '@ebay/nice-modal-react' import { SleepScreen } from '../atoms/SleepScreen' +import { OnDeviceLocalizationProvider } from '../LocalizationProvider' import { ToasterOven } from '../organisms/ToasterOven' import { MaintenanceRunTakeover } from '../organisms/TakeoverModal' import { FirmwareUpdateTakeover } from '../organisms/FirmwareUpdateModal/FirmwareUpdateTakeover' @@ -66,7 +67,6 @@ export const ON_DEVICE_DISPLAY_PATHS = [ '/emergency-stop', '/instruments', '/instruments/:mount', - '/loading', '/network-setup', '/network-setup/ethernet', '/network-setup/usb', @@ -97,8 +97,6 @@ function getPathComponent( return case '/instruments/:mount': return - case '/loading': - return case '/network-setup': return case '/network-setup/ethernet': @@ -151,12 +149,75 @@ export const OnDeviceDisplayApp = (): JSX.Element => { } const dispatch = useDispatch() const isIdle = useIdle(sleepTime, options) + + React.useEffect(() => { + if (isIdle) { + dispatch(updateBrightness(TURN_OFF_BACKLIGHT)) + } else { + dispatch( + updateConfigValue( + 'onDeviceDisplaySettings.brightness', + userSetBrightness + ) + ) + } + }, [dispatch, isIdle, userSetBrightness]) + + // TODO (sb:6/12/23) Create a notification manager to set up preference and order of takeover modals + return ( + + + + + + {isIdle ? ( + + ) : ( + <> + + + + + + + + + + + + )} + + + + + + + ) +} + +const getTargetPath = (unfinishedUnboxingFlowRoute: string | null): string => { + if (unfinishedUnboxingFlowRoute != null) { + return unfinishedUnboxingFlowRoute + } + + return '/dashboard' +} + +// split to a separate function because scrollRef rerenders on every route change +// this avoids rerendering parent providers as well +export function OnDeviceDisplayAppRoutes(): JSX.Element { const [currentNode, setCurrentNode] = React.useState(null) const scrollRef = React.useCallback((node: HTMLElement | null) => { setCurrentNode(node) }, []) const isScrolling = useScrolling(currentNode) + const { unfinishedUnboxingFlowRoute } = useSelector( + getOnDeviceDisplaySettings + ) + + const targetPath = getTargetPath(unfinishedUnboxingFlowRoute) + const TOUCH_SCREEN_STYLE = css` position: ${POSITION_RELATIVE}; width: 100%; @@ -176,54 +237,18 @@ export const OnDeviceDisplayApp = (): JSX.Element => { } ` - React.useEffect(() => { - if (isIdle) { - dispatch(updateBrightness(TURN_OFF_BACKLIGHT)) - } else { - dispatch( - updateConfigValue( - 'onDeviceDisplaySettings.brightness', - userSetBrightness - ) - ) - } - }, [dispatch, isIdle, userSetBrightness]) - - // TODO (sb:6/12/23) Create a notification manager to set up preference and order of takeover modals return ( - - - - {isIdle ? ( - - ) : ( - <> - - - - - - - - {ON_DEVICE_DISPLAY_PATHS.map(path => ( - - - - {getPathComponent(path)} - - - ))} - - - - - - - )} - - - - + + {ON_DEVICE_DISPLAY_PATHS.map(path => ( + + + + {getPathComponent(path)} + + + ))} + {targetPath != null && } + ) } diff --git a/app/src/App/OnDeviceDisplayAppFallback.tsx b/app/src/App/OnDeviceDisplayAppFallback.tsx index 6a345c1735e..0e48a31e565 100644 --- a/app/src/App/OnDeviceDisplayAppFallback.tsx +++ b/app/src/App/OnDeviceDisplayAppFallback.tsx @@ -27,7 +27,7 @@ import type { ModalHeaderBaseProps } from '../molecules/Modal/types' export function OnDeviceDisplayAppFallback({ error, }: FallbackProps): JSX.Element { - const { t } = useTranslation('app_settings') + const { t } = useTranslation(['app_settings', 'branded']) const trackEvent = useTrackEvent() const dispatch = useDispatch() const localRobot = useSelector(getLocalRobot) @@ -59,7 +59,9 @@ export function OnDeviceDisplayAppFallback({ alignItems={ALIGN_CENTER} justifyContent={JUSTIFY_CENTER} > - {t('error_boundary_description')} + + {t('branded:error_boundary_description')} + { + const actual = await vi.importActual('@opentrons/react-api-client') + return { + ...actual, + useRobotSettingsQuery: () => + (({ + data: { settings: [] }, + } as unknown) as UseQueryResult), + } +}) +vi.mock('../../LocalizationProvider') vi.mock('../../pages/Welcome') vi.mock('../../pages/NetworkSetupMenu') vi.mock('../../pages/ConnectViaEthernet') @@ -45,7 +60,6 @@ vi.mock('../../pages/InstrumentsDashboard') vi.mock('../../pages/RunningProtocol') vi.mock('../../pages/RunSummary') vi.mock('../../pages/NameRobot') -vi.mock('../../pages/InitialLoadingScreen') vi.mock('../../pages/EmergencyStop') vi.mock('../../pages/DeckConfiguration') vi.mock('../../redux/config') @@ -73,7 +87,7 @@ const render = (path = '/') => { describe('OnDeviceDisplayApp', () => { beforeEach(() => { vi.mocked(getOnDeviceDisplaySettings).mockReturnValue(mockSettings as any) - vi.mocked(getIsShellReady).mockReturnValue(false) + vi.mocked(getIsShellReady).mockReturnValue(true) vi.mocked(useCurrentRunRoute).mockReturnValue(null) vi.mocked(getLocalRobot).mockReturnValue(mockConnectedRobot) vi.mocked(useNotifyCurrentMaintenanceRun).mockReturnValue({ @@ -83,6 +97,12 @@ describe('OnDeviceDisplayApp', () => { }, }, } as any) + // TODO(bh, 2024-03-27): implement testing of branded and anonymous i18n, but for now pass through + vi.mocked( + OnDeviceLocalizationProvider + ).mockImplementation((props: OnDeviceLocalizationProviderProps) => ( + <>{props.children} + )) }) afterEach(() => { vi.resetAllMocks() @@ -140,9 +160,16 @@ describe('OnDeviceDisplayApp', () => { render('/runs/my-run-id/summary') expect(vi.mocked(RunSummary)).toHaveBeenCalled() }) - it('renders the loading screen on mount', () => { - render('/loading') - expect(vi.mocked(InitialLoadingScreen)).toHaveBeenCalled() + it('renders the localization provider and not the loading screen when app-shell is ready', () => { + render('/') + expect(vi.mocked(OnDeviceLocalizationProvider)).toHaveBeenCalled() + expect(screen.queryByLabelText('loading indicator')).toBeNull() + }) + it('renders the loading screen when app-shell is not ready', () => { + vi.mocked(getIsShellReady).mockReturnValue(false) + render('/') + screen.getByLabelText('loading indicator') + expect(vi.mocked(OnDeviceLocalizationProvider)).not.toHaveBeenCalled() }) it('renders EmergencyStop component from /emergency-stop', () => { render('/emergency-stop') diff --git a/app/src/LocalizationProvider.tsx b/app/src/LocalizationProvider.tsx new file mode 100644 index 00000000000..4b9e10a1f8d --- /dev/null +++ b/app/src/LocalizationProvider.tsx @@ -0,0 +1,60 @@ +import * as React from 'react' +import { I18nextProvider } from 'react-i18next' +import reduce from 'lodash/reduce' + +import { useRobotSettingsQuery } from '@opentrons/react-api-client' + +import { resources } from './assets/localization' +import { i18n, i18nCb, i18nConfig } from './i18n' + +import type { RobotSettingsField } from '@opentrons/api-client' + +export interface OnDeviceLocalizationProviderProps { + children?: React.ReactNode +} + +const BRANDED_RESOURCE = 'branded' +const ANONYMOUS_RESOURCE = 'anonymous' + +// TODO(bh, 2024-03-26): anonymization limited to ODD for now, may change in future OEM phases +export function OnDeviceLocalizationProvider( + props: OnDeviceLocalizationProviderProps +): JSX.Element | null { + const { settings } = useRobotSettingsQuery().data ?? {} + const oemModeSetting = (settings ?? []).find( + (setting: RobotSettingsField) => setting?.id === 'enableOEMMode' + ) + const isOEMMode = oemModeSetting?.value ?? false + + // iterate through language resources, nested files, substitute anonymous file for branded file for OEM mode + const anonResources = reduce( + resources, + (acc, resource, language) => { + const anonFiles = reduce( + resource, + (acc, file, fileName) => { + if (fileName === BRANDED_RESOURCE && isOEMMode) { + return acc + } else if (fileName === ANONYMOUS_RESOURCE) { + return isOEMMode ? { ...acc, [BRANDED_RESOURCE]: file } : acc + } else { + return { ...acc, [fileName]: file } + } + }, + {} + ) + return { ...acc, [language]: anonFiles } + }, + {} + ) + + const anonI18n = i18n.createInstance( + { + ...i18nConfig, + resources: anonResources, + }, + i18nCb + ) + + return {props.children} +} diff --git a/app/src/assets/localization/en/anonymous.json b/app/src/assets/localization/en/anonymous.json new file mode 100644 index 00000000000..ac288115d49 --- /dev/null +++ b/app/src/assets/localization/en/anonymous.json @@ -0,0 +1,72 @@ +{ + "a_robot_software_update_is_available": "A robot software update is required to run protocols with this version of the Opentrons App. Go to Robot", + "about_flex_gripper": "About Gripper", + "alternative_security_types_description": "The robot supports connecting to various enterprise access points. Connect via USB and finish setup in the robot's app.", + "calibration_block_description": "This block is a specially made tool that fits perfectly on your deck and helps with calibration.If you do not have a Calibration Block, please email support@opentrons.com so we can send you one. In your message, be sure to include your name, company or institution name, and shipping address. While you wait for the block to arrive, you can use the flat surface on the trash bin of your robot instead.", + "calibration_on_opentrons_tips_is_important": "It’s extremely important to perform this calibration using the Opentrons tips and tip racks specified above, as the robot determines accuracy based on the known measurements of these tips.", + "choose_what_data_to_share": "Choose what robot data to share.", + "computer_in_app_is_controlling_robot": "A computer with the Opentrons App is currently controlling this robot.", + "confirm_terminate": "This will immediately stop the activity begun on a computer. You, or another user, may lose progress or see an error in the Opentrons App.", + "connect_and_screw_in_gripper": "Connect and secure Gripper", + "connect_via_usb_description_3": "3. Launch the robot's desktop app on your computer to continue.", + "connection_description_usb": "Connect directly to a computer.", + "connection_lost_description": "The Opentrons App is unable to communicate with this robot right now. Double check the USB or Wifi connection to the robot, then try to reconnect.", + "contact_information": "Contact support for assistance.", + "contact_support_for_connection_help": "If none of these work, contact Opentrons Support for help (via the question mark link in this app, or by emailing {{support_email}}.)", + "deck_fixture_setup_modal_bottom_description": "For details on installing different fixture types, contact support.", + "delete_protocol_from_app": "Delete the protocol, make changes to address the error, and resend the protocol to this robot from the robot's app.", + "error_boundary_description": "You need to restart the touchscreen. Contact support for assistance.", + "estop_pressed_description": "First, safely clear the deck of any labware or spills. Then, twist the E-stop button clockwise. Finally, have the robot move the gantry to its home position.", + "find_your_robot": "Find your robot in the Opentrons App to install software updates.", + "firmware_update_download_logs": "Contact support for assistance.", + "general_error_message": "If you keep getting this message, try restarting your app and/or robot. If this does not resolve the issue please contact Opentrons Support.", + "gripper_still_attached": "Gripper still attached", + "gripper_successfully_attached_and_calibrated": "Gripper successfully attached and calibrated", + "gripper_successfully_calibrated": "Gripper successfully calibrated", + "gripper_successfully_detached": "Gripper successfully detached", + "gripper": "Gripper", + "ip_description_second": "Opentrons recommends working with your network administrator to assign a static IP address to the robot.", + "learn_uninstalling": "Learn more about uninstalling the Opentrons App", + "loosen_screws_and_detach": "Loosen screws and detach Gripper", + "modal_instructions": "For step-by-step instructions on setting up your module, consult the Quickstart Guide that came in its box. You can also click the link below or scan the QR code to visit the modules section of the Opentrons Help Center.", + "module_calibration_failed": "Module calibration was unsuccessful. Make sure the calibration adapter is fully seated on the module and try again. If you still have trouble, contact Opentrons Support.{{error}}", + "module_calibration_get_started": "To get started, remove labware from the deck and clean up the working area to make the calibration easier. Also gather the needed equipment shown to the right.The calibration adapter came with your module. The pipette probe came with your robot's pipette.", + "module_error_contact_support": "Try powering the module off and on again. If the error persists, contact Opentrons Support.", + "network_setup_menu_description": "You’ll use this connection to run software updates and load protocols onto your robot.", + "opentrons_app_successfully_updated": "The Opentrons App was successfully updated.", + "opentrons_app_update": "Opentrons App update", + "opentrons_app_update_available": "Opentrons App Update Available", + "opentrons_app_update_available_variation": "An Opentrons App update is available.", + "opentrons_app_will_use_interpreter": "If specified, the Opentrons App will use the Python interpreter at this path instead of the default bundled Python interpreter.", + "opentrons_cares_about_privacy": "We care about your privacy. We anonymize all data and only use it to improve our products.", + "opentrons_def": "Opentrons Definition", + "opentrons_labware_def": "Opentrons labware definition", + "opentrons_tip_racks_recommended": "Opentrons tip racks are highly recommended. Accuracy cannot be guaranteed with other tip racks.", + "opentrons_tip_rack_name": "opentrons", + "previous_releases": "View previous Opentrons releases", + "receive_alert": "Receive an alert when an Opentrons software update is available.", + "restore_description": "Opentrons does not recommend reverting to previous software versions, but you can access previous releases below. For best results, uninstall the existing app and remove its configuration files before installing the previous version.", + "robot_server_version_ot3_description": "The robot software includes the robot server and the touchscreen display interface.", + "robot_software_update_required": "A robot software update is required to run protocols with this version of the Opentrons App.", + "run_failed_modal_description_desktop": "Contact support for assistance.", + "secure_labware_explanation_magnetic_module": "Opentrons recommends ensuring your labware locks to the Magnetic Module by adjusting the black plate bracket on top of the module. Please note there are two sizes of plate brackets supplied with your module: standard and deep well. These brackets can be removed and swapped by unscrewing the modules thumb screw (the silver knob on the front).", + "secure_labware_explanation_thermocycler": "Opentrons recommends securing your labware to the Thermocycler module by closing its latch. Doing so ensures level and accurate plate placement for optimal results.", + "send_a_protocol_to_store": "Send a protocol to the robot to get started.", + "setup_instructions_description": "For step-by-step instructions on setting up your module, consult the Quickstart Guide that came in its box or scan the QR code to visit the modules section of the Opentrons Help Center.", + "share_app_analytics": "Share App Analytics with Opentrons", + "share_app_analytics_description": "Help Opentrons improve its products and services by automatically sending anonymous diagnostics and usage data.", + "share_display_usage_description": "Data on how you interact with the robot's touchscreen.", + "share_logs_with_opentrons": "Share Robot logs with Opentrons", + "share_logs_with_opentrons_description": "Help Opentrons improve its products and services by automatically sending anonymous robot logs. Opentrons uses these logs to troubleshoot robot issues and spot error trends.", + "show_labware_offset_snippets_description": "Only for users who need to apply Labware Offset data outside of the Opentrons App. When enabled, code snippets for Jupyter Notebook and SSH are available during protocol setup.", + "something_seems_wrong": "There may be a problem with your pipette. Exit setup and contact support for assistance.", + "these_are_advanced_settings": "These are advanced settings. Please do not attempt to adjust without assistance from support. Changing these settings may affect the lifespan of your pipette.These settings do not override any pipette settings defined in protocols.", + "update_requires_restarting_app": "Updating requires restarting the Opentrons App.", + "update_robot_software_description": "Bypass the Opentrons App auto-update process and update the robot software manually.", + "update_robot_software_link": "Launch Opentrons software update page", + "use_older_protocol_analysis_method_description": "Use an older, slower method of analyzing uploaded protocols. This changes how the OT-2 validates your protocol during the upload step, but does not affect how your protocol actually runs. Support might ask you to change this setting if you encounter problems with the newer, faster protocol analysis method.", + "versions_sync": "Learn more about keeping the Opentrons App and robot software in sync", + "want_to_help_out": "Want to help out Opentrons?", + "welcome_title": "Welcome!", + "why_use_lpc": "Labware Position Check is intended to correct for minor variances. Opentrons does not recommend using Labware Position Check to compensate for large positional adjustments. Needing to set large labware offsets could indicate a problem with robot calibration." +} diff --git a/app/src/assets/localization/en/app_settings.json b/app/src/assets/localization/en/app_settings.json index 18e3eef9e8a..389854a8b33 100644 --- a/app/src/assets/localization/en/app_settings.json +++ b/app/src/assets/localization/en/app_settings.json @@ -11,16 +11,12 @@ "additional_folder_location": "Additional Source Folder", "additional_labware_folder_title": "Additional Custom Labware Source Folder", "advanced": "Advanced", - "allow_sending_all_protocols_to_ot3": "Allow Sending All Protocols to Opentrons Flex", - "allow_sending_all_protocols_to_ot3_description": "Enable the \"Send to Opentrons Flex\" menu item for each imported protocol, even if protocol analysis fails or does not recognize it as designed for the Opentrons Flex.", - "analytics_description": "Help Opentrons improve its products and services by automatically sending anonymous diagnostics and usage data.", "app_changes": "App Changes in ", "app_settings": "App Settings", "bug_fixes": "Bug Fixes", "cal_block": "Always use calibration block to calibrate", "change_folder_button": "Change labware source folder", "channel": "Channel", - "choose_what_data_to_share": "Choose what data to share with Opentrons.", "clear_confirm": "Clear unavailable robots", "clear_robots_button": "Clear unavailable robots list", "clear_robots_description": "Clear the list of unavailable robots on the Devices page. This action cannot be undone.", @@ -35,7 +31,6 @@ "download_update": "Downloading update...", "enable_dev_tools": "Developer Tools", "enable_dev_tools_description": "Enabling this setting opens Developer Tools on app launch, enables additional logging and gives access to feature flags.", - "error_boundary_description": "You need to restart the touchscreen. Then download the robot logs from the Opentrons App and send them to support@opentrons.com for assistance.", "error_boundary_desktop_app_description": "You need to reload the app. Contact support with the following error message:", "error_boundary_title": "An unknown error has occurred", "feature_flags": "Feature Flags", @@ -46,20 +41,12 @@ "installing_update": "Installing update...", "ip_available": "Available", "ip_description_first": "Enter an IP address or hostname to connect to a robot.", - "ip_description_second": "Opentrons recommends working with your network administrator to assign a static IP address to the robot.", - "learn_uninstalling": "Learn more about uninstalling the Opentrons App", "manage_versions": "It is very important for the robot and app software to be on the same version. Manage the robot software versions via Robot Settings > Advanced.", "new_features": "New Features", "no_folder": "No additional source folder specified", "no_specified_folder": "No path specified", "no_unavail_robots_to_clear": "No unavailable robots to clear", "not_found": "Not Found", - "opentrons_app_successfully_updated": "The Opentrons App was successfully updated.", - "opentrons_app_update": "Opentrons App update", - "opentrons_app_update_available": "Opentrons App Update Available", - "opentrons_app_update_available_variation": "An Opentrons App update is available.", - "opentrons_app_will_use_interpreter": "If specified, the Opentrons App will use the Python interpreter at this path instead of the default bundled Python interpreter.", - "opentrons_cares_about_privacy": "Opentrons cares about your privacy. We anonymize all data and only use it to improve our products.", "opt_in": "Opt in", "opt_in_description": "Automatically send us anonymous diagnostics and usage data. We only use this information to improve our products.", "opt_out": "Opt out", @@ -68,29 +55,22 @@ "override_path_to_python": "Override Path to Python", "prevent_robot_caching": "Prevent Robot Caching", "prevent_robot_caching_description": "The app will immediately clear unavailable robots and will not remember unavailable robots while this is enabled. On networks with many robots, preventing caching may improve network performance at the expense of slower and less reliable robot discovery on app launch.", - "previous_releases": "View previous Opentrons releases", "privacy": "Privacy", "problem_during_update": "This update is taking longer than usual.", "prompt": "Always show the prompt to choose calibration block or trash bin", - "receive_alert": "Receive an alert when an Opentrons software update is available.", "release_notes": "Release notes", "reload_app": "Reload app", "remind_later": "Remind me later", "reset_to_default": "Reset to default", "restart_touchscreen": "Restart touchscreen", "restarting_app": "Download complete, restarting the app...", - "restore_description": "Opentrons does not recommend reverting to previous software versions, but you can access previous releases below. For best results, uninstall the existing app and remove its configuration files before installing the previous version.", "restore_previous": "See how to restore a previous software version", "searching": "Searching for 30s", "setup_connection": "Set up connection", - "share_app_analytics": "Share App Analytics with Opentrons", - "share_app_analytics_description": "Help Opentrons improve its products and services by automatically sending anonymous diagnostics and usage data.", "share_display_usage": "Share display usage", - "share_display_usage_description": "Data on how you interact with the touchscreen on Flex.", "share_robot_logs": "Share robot logs", "share_robot_logs_description": "Data on actions the robot does, like running protocols.", "show_labware_offset_snippets": "Show Labware Offset data code snippets", - "show_labware_offset_snippets_description": "Only for users who need to apply Labware Offset data outside of the Opentrons App. When enabled, code snippets for Jupyter Notebook and SSH are available during protocol setup.", "software_update_available": "Software Update Available", "software_version": "App Software Version", "successfully_deleted_unavail_robots": "Successfully deleted unavailable robots", @@ -104,7 +84,6 @@ "update_available": "Update available", "update_channel": "Update Channel", "update_description": "Stable receives the latest stable releases. Beta allows you to try out new in-progress features before they launch in Stable channel, but they have not completed testing yet.", - "update_requires_restarting": "Updating requires restarting the Opentrons App.", "usb_to_ethernet_adapter_description": "Description", "usb_to_ethernet_adapter_driver_version": "Driver Version", "usb_to_ethernet_adapter_info": "USB-to-Ethernet Adapter Information", @@ -116,11 +95,6 @@ "usb_to_ethernet_not_connected": "No USB-to-Ethernet adapter connected", "usb_to_ethernet_unknown_manufacturer": "Unknown Manufacturer", "usb_to_ethernet_unknown_product": "Unknown Adapter", - "versions_sync": "Learn more about keeping the Opentrons App and robot software in sync", - "view_change_log": "View Opentrons technical change log", - "view_issue_tracker": "View Opentrons issue tracker", - "view_release_notes": "View full Opentrons release notes", "view_software_update": "View software update", - "view_update": "View Update", - "want_to_help_out": "Want to help out Opentrons?" + "view_update": "View Update" } diff --git a/app/src/assets/localization/en/branded.json b/app/src/assets/localization/en/branded.json new file mode 100644 index 00000000000..c28f2d9b3cc --- /dev/null +++ b/app/src/assets/localization/en/branded.json @@ -0,0 +1,72 @@ +{ + "a_robot_software_update_is_available": "A robot software update is required to run protocols with this version of the Opentrons App. Go to Robot", + "about_flex_gripper": "About Flex Gripper", + "alternative_security_types_description": "The Opentrons App supports connecting Flex to various enterprise access points. Connect via USB and finish setup in the app.", + "calibration_block_description": "This block is a specially made tool that fits perfectly on your deck and helps with calibration.If you do not have a Calibration Block, please email support@opentrons.com so we can send you one. In your message, be sure to include your name, company or institution name, and shipping address. While you wait for the block to arrive, you can use the flat surface on the trash bin of your robot instead.", + "calibration_on_opentrons_tips_is_important": "It’s extremely important to perform this calibration using the Opentrons tips and tip racks specified above, as the robot determines accuracy based on the known measurements of these tips.", + "choose_what_data_to_share": "Choose what data to share with Opentrons.", + "computer_in_app_is_controlling_robot": "A computer with the Opentrons App is currently controlling this robot.", + "confirm_terminate": "This will immediately stop the activity begun on a computer. You, or another user, may lose progress or see an error in the Opentrons App.", + "connect_and_screw_in_gripper": "Connect and secure Flex Gripper", + "connect_via_usb_description_3": "3. Launch the Opentrons App on the computer to continue.", + "connection_description_usb": "Connect directly to a computer (running the Opentrons App).", + "connection_lost_description": "The Opentrons App is unable to communicate with this robot right now. Double check the USB or Wifi connection to the robot, then try to reconnect.", + "contact_information": "Download the robot logs from the Opentrons App and send it to support@opentrons.com for assistance.", + "contact_support_for_connection_help": "If none of these work, contact Opentrons Support for help (via the question mark link in this app, or by emailing {{support_email}}.)", + "deck_fixture_setup_modal_bottom_description": "For details on installing different fixture types, scan the QR code or search for “deck configuration” on support.opentrons.com", + "delete_protocol_from_app": "Delete the protocol, make changes to address the error, and resend the protocol to this robot from the Opentrons App.", + "error_boundary_description": "You need to restart the touchscreen. Then download the robot logs from the Opentrons App and send them to support@opentrons.com for assistance.", + "estop_pressed_description": "First, safely clear the deck of any labware or spills. Then, twist the E-stop button clockwise. Finally, have Flex move the gantry to its home position.", + "find_your_robot": "Find your robot in the Opentrons App to install software updates.", + "firmware_update_download_logs": "Download the robot logs from the Opentrons App and send them to support@opentrons.com for assistance.", + "general_error_message": "If you keep getting this message, try restarting your app and/or robot. If this does not resolve the issue please contact Opentrons Support.", + "gripper_still_attached": "Flex Gripper still attached", + "gripper_successfully_attached_and_calibrated": "Flex Gripper successfully attached and calibrated", + "gripper_successfully_calibrated": "Flex Gripper successfully calibrated", + "gripper_successfully_detached": "Flex Gripper successfully detached", + "gripper": "Flex Gripper", + "ip_description_second": "Opentrons recommends working with your network administrator to assign a static IP address to the robot.", + "learn_uninstalling": "Learn more about uninstalling the Opentrons App", + "loosen_screws_and_detach": "Loosen screws and detach Flex Gripper", + "modal_instructions": "For step-by-step instructions on setting up your module, consult the Quickstart Guide that came in its box. You can also click the link below or scan the QR code to visit the modules section of the Opentrons Help Center.", + "module_calibration_failed": "Module calibration was unsuccessful. Make sure the calibration adapter is fully seated on the module and try again. If you still have trouble, contact Opentrons Support.{{error}}", + "module_calibration_get_started": "To get started, remove labware from the deck and clean up the working area to make the calibration easier. Also gather the needed equipment shown to the right.The calibration adapter came with your module. The pipette probe came with your Flex pipette.", + "module_error_contact_support": "Try powering the module off and on again. If the error persists, contact Opentrons Support.", + "network_setup_menu_description": "You’ll use this connection to run software updates and load protocols onto your Opentrons Flex.", + "opentrons_app_successfully_updated": "The Opentrons App was successfully updated.", + "opentrons_app_update": "Opentrons App update", + "opentrons_app_update_available": "Opentrons App Update Available", + "opentrons_app_update_available_variation": "An Opentrons App update is available.", + "opentrons_app_will_use_interpreter": "If specified, the Opentrons App will use the Python interpreter at this path instead of the default bundled Python interpreter.", + "opentrons_cares_about_privacy": "Opentrons cares about your privacy. We anonymize all data and only use it to improve our products.", + "opentrons_def": "Opentrons Definition", + "opentrons_labware_def": "Opentrons labware definition", + "opentrons_tip_rack_name": "opentrons", + "opentrons_tip_racks_recommended": "Opentrons tip racks are highly recommended. Accuracy cannot be guaranteed with other tip racks.", + "previous_releases": "View previous Opentrons releases", + "receive_alert": "Receive an alert when an Opentrons software update is available.", + "restore_description": "Opentrons does not recommend reverting to previous software versions, but you can access previous releases below. For best results, uninstall the existing app and remove its configuration files before installing the previous version.", + "robot_server_version_ot3_description": "The Opentrons Flex software includes the robot server and the touchscreen display interface.", + "robot_software_update_required": "A robot software update is required to run protocols with this version of the Opentrons App.", + "run_failed_modal_description_desktop": "Download the run log and send it to support@opentrons.com for assistance.", + "secure_labware_explanation_magnetic_module": "Opentrons recommends ensuring your labware locks to the Magnetic Module by adjusting the black plate bracket on top of the module. Please note there are two sizes of plate brackets supplied with your module: standard and deep well. These brackets can be removed and swapped by unscrewing the modules thumb screw (the silver knob on the front).", + "secure_labware_explanation_thermocycler": "Opentrons recommends securing your labware to the Thermocycler module by closing its latch. Doing so ensures level and accurate plate placement for optimal results.", + "send_a_protocol_to_store": "Send a protocol from the Opentrons App to get started.", + "setup_instructions_description": "For step-by-step instructions on setting up your module, consult the Quickstart Guide that came in its box or scan the QR code to visit the modules section of the Opentrons Help Center.", + "share_app_analytics": "Share App Analytics with Opentrons", + "share_app_analytics_description": "Help Opentrons improve its products and services by automatically sending anonymous diagnostics and usage data.", + "share_display_usage_description": "Data on how you interact with the touchscreen on Flex.", + "share_logs_with_opentrons": "Share Robot logs with Opentrons", + "share_logs_with_opentrons_description": "Help Opentrons improve its products and services by automatically sending anonymous robot logs. Opentrons uses these logs to troubleshoot robot issues and spot error trends.", + "show_labware_offset_snippets_description": "Only for users who need to apply Labware Offset data outside of the Opentrons App. When enabled, code snippets for Jupyter Notebook and SSH are available during protocol setup.", + "something_seems_wrong": "There may be a problem with your pipette. Exit setup and contact Opentrons Support for assistance.", + "these_are_advanced_settings": "These are advanced settings. Please do not attempt to adjust without assistance from Opentrons Support. Changing these settings may affect the lifespan of your pipette.These settings do not override any pipette settings defined in protocols.", + "update_requires_restarting_app": "Updating requires restarting the Opentrons App.", + "update_robot_software_description": "Bypass the Opentrons App auto-update process and update the robot software manually.", + "update_robot_software_link": "Launch Opentrons software update page", + "use_older_protocol_analysis_method_description": "Use an older, slower method of analyzing uploaded protocols. This changes how the OT-2 validates your protocol during the upload step, but does not affect how your protocol actually runs. Opentrons Support might ask you to change this setting if you encounter problems with the newer, faster protocol analysis method.", + "versions_sync": "Learn more about keeping the Opentrons App and robot software in sync", + "want_to_help_out": "Want to help out Opentrons?", + "welcome_title": "Welcome to your Opentrons Flex!", + "why_use_lpc": "Labware Position Check is intended to correct for minor variances. Opentrons does not recommend using Labware Position Check to compensate for large positional adjustments. Needing to set large labware offsets could indicate a problem with robot calibration." +} diff --git a/app/src/assets/localization/en/device_details.json b/app/src/assets/localization/en/device_details.json index d217718af42..4520a016224 100644 --- a/app/src/assets/localization/en/device_details.json +++ b/app/src/assets/localization/en/device_details.json @@ -1,5 +1,4 @@ { - "about_flex_gripper": "About Flex Gripper", "about_gripper": "About gripper", "about_module": "About {{name}}", "about_pipette_name": "About {{name}} Pipette", @@ -40,7 +39,6 @@ "deck_configuration": "deck configuration", "deck_fixture_setup_instructions": "Deck fixture setup instructions", "deck_fixture_setup_modal_bottom_description_desktop": "For detailed instructions for different types of fixtures, scan the QR code or go to the link below.", - "deck_fixture_setup_modal_bottom_description": "For details on installing different fixture types, scan the QR code or search for “deck configuration” on support.opentrons.com", "deck_fixture_setup_modal_top_description": "First, unscrew and remove the deck slot where you'll install a fixture. Then put the fixture in place and attach it as needed.", "deck_slot": "deck slot {{slot}}", "delete_run": "Delete protocol run record", @@ -94,7 +92,6 @@ "module_calibration_required_update_pipette_FW": "Update pipette firmware before proceeding with required module calibration.", "module_calibration_required": "Module calibration required.", "module_controls": "Module Controls", - "module_error_contact_support": "Try powering the module off and on again. If the error persists, contact Opentrons Support.", "module_error": "Module error", "module_name_error": "{{moduleName}} error", "module_status_range": "Between {{min}} - {{max}} {{unit}}", @@ -177,7 +174,6 @@ "tempdeck_slideout_body": "Pre heat or cool your {{model}}. Enter a whole number between 4 °C and 96 °C.", "tempdeck_slideout_title": "Set Temperature for {{name}}", "temperature": "Temperature", - "these_are_advanced_settings": "These are advanced settings. Please do not attempt to adjust without assistance from Opentrons Support. Changing these settings may affect the lifespan of your pipette.These settings do not override any pipette settings defined in protocols.", "this_robot_will_restart_with_update": "This robot has to restart to update its software. Restarting will immediately stop the current run or calibration.Do you want to update now anyway?", "tip_pickup_drop": "Tip Pickup / Drop", "to_run_protocol_go_to_protocols_page": "To run a protocol on this robot, import a protocol on the Protocols page", diff --git a/app/src/assets/localization/en/device_settings.json b/app/src/assets/localization/en/device_settings.json index f92a6e30d69..c6bd00ad70d 100644 --- a/app/src/assets/localization/en/device_settings.json +++ b/app/src/assets/localization/en/device_settings.json @@ -6,7 +6,6 @@ "advanced": "Advanced", "alpha_description": "Warning: alpha releases are feature-complete but may contain significant bugs.", "alternative_security_types": "Alternative security types", - "alternative_security_types_description": "The Opentrons App supports connecting Flex to various enterprise access points. Connect via USB and finish setup in the app.", "app_change_in": "App Changes in {{version}}", "apply_historic_offsets": "Apply Labware Offsets", "are_you_sure_you_want_to_disconnect": "Are you sure you want to disconnect from {{ssid}}?", @@ -59,16 +58,13 @@ "connect_via": "Connect via {{type}}", "connect_via_usb_description_1": "1. Connect the USB A-to-B cable to the robot’s USB-B port.", "connect_via_usb_description_2": "2. Connect the cable to an open USB port on your computer.", - "connect_via_usb_description_3": "3. Launch the Opentrons App on the computer to continue.", "connected": "Connected", "connected_network": "Connected Network", "connected_to_ssid": "Connected to {{ssid}}", "connected_via": "Connected via {{networkInterface}}", "connecting_to": "Connecting to {{ssid}}...", "connection_description_ethernet": "Connect to your lab's wired network.", - "connection_description_usb": "Connect directly to a computer (running the Opentrons App).", "connection_description_wifi": "Find a network in your lab or enter your own.", - "connection_lost_description": "The Opentrons App is unable to communicate with this robot right now. Double check the USB or Wifi connection to the robot, then try to reconnect.", "connection_to_robot_lost": "Connection to robot lost", "deck_calibration_description": "Calibrating the deck is required for new robots or after you relocate your robot. Recalibrating the deck will require you to also recalibrate pipette offsets.", "deck_calibration_missing": "Deck calibration missing", @@ -119,7 +115,6 @@ "estop_missing": "E-stop missing", "estop_missing_description": "Your E-stop could be damaged or detached. {{robotName}} lost its connection to the E-stop, so it canceled the protocol. Connect a functioning E-stop to continue.", "estop_pressed": "E-stop pressed", - "estop_pressed_description": "First, safely clear the deck of any labware or spills. Then, twist the E-stop button clockwise. Finally, have Flex move the gantry to its home position.", "ethernet": "Ethernet", "ethernet_connection_description": "Connect an Ethernet cable to the back of the robot and a network switch or hub.", "exit": "exit", @@ -129,7 +124,6 @@ "factory_resets_cannot_be_undone": "Factory resets cannot be undone.", "failed_to_connect_to_ssid": "Failed to connect to {{ssid}}", "feature_flags": "Feature Flags", - "find_your_robot": "Find your robot in the Opentrons App to install software updates.", "finish_setup": "Finish setup", "firmware_version": "Firmware Version", "fully_calibrate_before_checking_health": "Fully calibrate your robot before checking calibration health", @@ -174,7 +168,6 @@ "need_another_security_type": "Need another security type?", "network_name": "Network Name", "network_settings": "Network Settings", - "network_setup_menu_description": "You’ll use this connection to run software updates and load protocols onto your Opentrons Flex.", "networking": "Networking", "never": "Never", "new_features": "New Features", @@ -240,10 +233,8 @@ "robot_operating_update_available": "Robot Operating System Update Available", "robot_serial_number": "Robot Serial Number", "robot_server_version": "Robot Server Version", - "robot_server_version_ot3_description": "The Opentrons Flex software includes the robot server and the touchscreen display interface.", "robot_settings": "Robot Settings", "robot_settings_advanced_unknown": "Unknown", - "robot_software_update_required": "A robot software update is required to run protocols with this version of the Opentrons App.", "robot_successfully_connected": "Robot successfully connected to {{networkName}}.", "robot_system_version": "Robot System Version", "robot_system_version_available": "Robot System Version {{releaseVersion}} available", @@ -261,10 +252,6 @@ "select_authentication_method": "Select authentication method for your selected network.", "sending_software": "Sending software...", "serial": "Serial", - "share_logs_with_opentrons": "Share Robot logs with Opentrons", - "share_logs_with_opentrons_description": "Help Opentrons improve its products and services by automatically sending anonymous robot logs. Opentrons uses these logs to troubleshoot robot issues and spot error trends.", - "share_logs_with_opentrons_description_short": "Share anonymous robot logs with Opentrons.", - "share_logs_with_opentrons_short": "Share Robot logs", "short_trash_bin": "Short trash bin", "short_trash_bin_description": "For pre-2019 robots with trash bins that are 55mm tall (instead of 77mm default)", "show": "Show", @@ -278,7 +265,6 @@ "successfully_connected": "Successfully connected!", "successfully_connected_to_network": "Successfully connected to {{ssid}}!", "supported_protocol_api_versions": "Supported Protocol API Versions", - "switch_to_usb_description": "If your network uses a different authentication method, connect to the Opentrons App and finish Wi-Fi setup there.", "text_size": "Text Size", "text_size_description": "Text on all screens will adjust to the size you choose below.", "tip_length_calibrations_history": "See all Tip Length Calibration history", @@ -296,27 +282,20 @@ "update_found": "Update found!", "update_robot_now": "Update robot now", "update_robot_software": "Update robot software manually with a local file (.zip)", - "update_robot_software_description": "Bypass the Opentrons App auto-update process and update the robot software manually.", - "update_robot_software_link": "Launch Opentrons software update page", "updating": "Updating", - "update_requires_restarting": "Updating the robot software requires restarting the robot", + "update_requires_restarting_robot": "Updating the robot software requires restarting the robot", "usage_settings": "Usage Settings", "usb": "USB", "usb_to_ethernet_description": "Looking for USB-to-Ethernet Adapter info?", "use_older_aspirate": "Use older aspirate behavior", "use_older_aspirate_description": "Aspirate with the less accurate volumetric calibrations that were used before version 3.7.0. Use this if you need consistency with pre-v3.7.0 results. This only affects GEN1 P10S, P10M, P50M, and P300S pipettes.", "use_older_protocol_analysis_method": "Use older protocol analysis method", - "use_older_protocol_analysis_method_description": "Use an older, slower method of analyzing uploaded protocols. This changes how the OT-2 validates your protocol during the upload step, but does not affect how your protocol actually runs. Opentrons Support might ask you to change this setting if you encounter problems with the newer, faster protocol analysis method.", "validating_software": "Validating software...", "view_details": "View details", "view_latest_release_notes_at": "View latest release notes at {{url}}", "view_network_details": "View network details", - "view_opentrons_issue_tracker": "View Opentrons issue tracker", - "view_opentrons_release_notes": "View full Opentrons release notes", - "view_opentrons_technical_change_log": "View Opentrons technical change log", "view_update": "View update", "welcome_description": "Quickly run protocols and check on your robot's status right on your lab bench.", - "welcome_title": "Welcome to your Opentrons Flex!", "wifi": "Wi-Fi", "wired_ip": "Wired IP", "wired_mac_address": "Wired MAC Address", diff --git a/app/src/assets/localization/en/devices_landing.json b/app/src/assets/localization/en/devices_landing.json index 60a25974fec..b0a3307ace1 100644 --- a/app/src/assets/localization/en/devices_landing.json +++ b/app/src/assets/localization/en/devices_landing.json @@ -4,7 +4,6 @@ "check_same_network": "Check that the computer and robot are on the same network", "connect_to_network": "Connect to network", "connection_troubleshooting_intro": "If you’re having trouble with the robot’s connection, try these troubleshooting tasks. First, double check that the robot is powered on.", - "contact_support_for_connection_help": "If none of these work, contact Opentrons Support for help (via the question mark link in this app, or by emailing {{support_email}}.)", "deck_configuration": "Deck configuration", "devices": "Devices", "disconnect_from_network": "Disconnect from network", diff --git a/app/src/assets/localization/en/firmware_update.json b/app/src/assets/localization/en/firmware_update.json index 0540963084b..5d543032e12 100644 --- a/app/src/assets/localization/en/firmware_update.json +++ b/app/src/assets/localization/en/firmware_update.json @@ -1,5 +1,4 @@ { - "download_logs": "Download the robot logs from the Opentrons App and send them to support@opentrons.com for assistance.", "firmware_out_of_date": "The firmware for {{mount}} {{instrument}} is out of date. You need to update it before running protocols that use this instrument.", "gantry_x": "Gantry X", "gantry_y": "Gantry Y", diff --git a/app/src/assets/localization/en/gripper_wizard_flows.json b/app/src/assets/localization/en/gripper_wizard_flows.json index a868cdb474e..ff98d8e07f0 100644 --- a/app/src/assets/localization/en/gripper_wizard_flows.json +++ b/app/src/assets/localization/en/gripper_wizard_flows.json @@ -8,7 +8,6 @@ "calibration_pin": "Calibration Pin", "calibration_pin_touching": "The calibration pin will touch the calibration square in slot {{slot}} to determine its exact position.", "complete_calibration": "Complete calibration", - "connect_and_screw_in_gripper": "Connect and secure Flex Gripper", "continue": "Continue", "continue_calibration": "Continue calibration", "detach_gripper": "Detach Gripper", @@ -17,17 +16,11 @@ "get_started": "Get started", "gripper_calibration": "Gripper Calibration", "gripper_recalibration": "Gripper Recalibration", - "gripper_still_attached": "Flex Gripper still attached", - "gripper_successfully_attached_and_calibrated": "Flex Gripper successfully attached and calibrated", "gripper_successfully_attached": "Gripper successfully attached", - "gripper_successfully_calibrated": "Flex Gripper successfully calibrated", - "gripper_successfully_detached": "Flex Gripper successfully detached", - "gripper": "Flex Gripper", "hex_screwdriver": "2.5 mm Hex Screwdriver", "hold_gripper_and_loosen_screws": "Hold the gripper in place and loosen the top gripper screw first. After that move onto the bottom screw. (The screws are captive and will not come apart from the gripper.) Then carefully remove the gripper.", "insert_pin_into_front_jaw": "Insert calibration pin in front jaw", "insert_pin_into_rear_jaw": "Insert calibration pin in rear jaw", - "loosen_screws_and_detach": "Loosen screws and detach Flex Gripper", "move_gantry_to_front": "Move gantry to front", "move_pin_from_front_to_rear_jaw": "Remove the calibration pin from the front jaw and attach it to the rear jaw.", "move_pin_from_rear_jaw_to_storage": "Take the calibration pin from the rear gripper jaw and return it to its storage location.", diff --git a/app/src/assets/localization/en/index.ts b/app/src/assets/localization/en/index.ts index 8c865445056..c74aab09de5 100644 --- a/app/src/assets/localization/en/index.ts +++ b/app/src/assets/localization/en/index.ts @@ -1,5 +1,7 @@ import shared from './shared.json' +import anonymous from './anonymous.json' import app_settings from './app_settings.json' +import branded from './branded.json' import change_pipette from './change_pipette.json' import protocol_command_text from './protocol_command_text.json' import device_details from './device_details.json' @@ -14,26 +16,22 @@ import labware_details from './labware_details.json' import labware_landing from './labware_landing.json' import labware_position_check from './labware_position_check.json' import module_wizard_flows from './module_wizard_flows.json' -import more_network_and_system from './more_network_and_system.json' -import more_panel from './more_panel.json' import pipette_wizard_flows from './pipette_wizard_flows.json' -import protocol_calibration from './protocol_calibration.json' import protocol_details from './protocol_details.json' import protocol_info from './protocol_info.json' import protocol_list from './protocol_list.json' import protocol_setup from './protocol_setup.json' -import robot_advanced_settings from './robot_advanced_settings.json' import robot_calibration from './robot_calibration.json' -import robot_connection from './robot_connection.json' import robot_controls from './robot_controls.json' -import robot_info from './robot_info.json' import run_details from './run_details.json' import top_navigation from './top_navigation.json' import error_recovery from './error_recovery.json' export const en = { shared, + anonymous, app_settings, + branded, change_pipette, protocol_command_text, device_details, @@ -48,19 +46,13 @@ export const en = { labware_landing, labware_position_check, module_wizard_flows, - more_network_and_system, - more_panel, pipette_wizard_flows, - protocol_calibration, protocol_details, protocol_info, protocol_list, protocol_setup, - robot_advanced_settings, robot_calibration, - robot_connection, robot_controls, - robot_info, run_details, top_navigation, error_recovery, diff --git a/app/src/assets/localization/en/labware_landing.json b/app/src/assets/localization/en/labware_landing.json index 63212567561..17eebda979d 100644 --- a/app/src/assets/localization/en/labware_landing.json +++ b/app/src/assets/localization/en/labware_landing.json @@ -22,8 +22,6 @@ "labware": "labware", "last_updated": "Last updated", "open_labware_creator": "Open Labware Creator", - "opentrons_def": "Opentrons Definition", - "opentrons_labware_def": "Opentrons labware definition", "show_in_folder": "Show in folder", "unable_to_upload": "Unable to upload file", "yes_delete_def": "Yes, delete definition" diff --git a/app/src/assets/localization/en/labware_position_check.json b/app/src/assets/localization/en/labware_position_check.json index f08c465f7fa..4072826650a 100644 --- a/app/src/assets/localization/en/labware_position_check.json +++ b/app/src/assets/localization/en/labware_position_check.json @@ -27,9 +27,6 @@ "detach_probe": "Remove calibration probe", "ensure_nozzle_position_odd": "Ensure that the {{tip_type}} is centered above and level with {{item_location}}. If it isn't, tap Move pipette and then jog the pipette until it is properly aligned.", "ensure_nozzle_position_desktop": "Ensure that the {{tip_type}} is centered above and level with {{item_location}}. If it isn't, use the controls below or your keyboard to jog the pipette until it is properly aligned.", - "error_modal_header": "Something went wrong", - "error_modal_problem_in_app": "There was an error performing Labware Position Check. Please restart the app. If the problem persists, please contact Opentrons Support", - "error_modal_problem_on_robot": "There was an error processing your request on the robot", "exit_screen_confirm_exit": "Exit and discard all labware offsets", "exit_screen_go_back": "Go back to labware position check", "exit_screen_subtitle": "If you exit now, all labware offsets will be discarded. This cannot be undone.", diff --git a/app/src/assets/localization/en/module_wizard_flows.json b/app/src/assets/localization/en/module_wizard_flows.json index 636bb368662..a502b0ef797 100644 --- a/app/src/assets/localization/en/module_wizard_flows.json +++ b/app/src/assets/localization/en/module_wizard_flows.json @@ -21,12 +21,10 @@ "exit": "Exit", "firmware_up_to_date": "{{module}} firmware up to date.", "firmware_updated": "{{module}} firmware updated!", - "get_started": "To get started, remove labware from the deck and clean up the working area to make the calibration easier. Also gather the needed equipment shown to the right.The calibration adapter came with your module. The pipette probe came with your Flex pipette.", "install_adapter": "Place calibration adapter in {{module}}", "install_calibration_adapter": "Install calibration adapter", "location_occupied": "A {{fixture}} is currently specified here on the deck configuration", "module_calibrating": "Stand back, {{moduleName}} is calibrating", - "module_calibration_failed": "Module calibration was unsuccessful. Make sure the calibration adapter is fully seated on the module and try again. If you still have trouble, contact Opentrons Support.{{error}}", "module_calibration": "Module calibration", "module_secured": "The module must be fully secured in its caddy and secured in the deck slot.", "module_too_hot": "Module is too hot to proceed to module calibration", diff --git a/app/src/assets/localization/en/more_network_and_system.json b/app/src/assets/localization/en/more_network_and_system.json deleted file mode 100644 index a6fd1593b38..00000000000 --- a/app/src/assets/localization/en/more_network_and_system.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "description": "Description", - "driver_version": "Driver Version", - "launch_realtek_adapter_drivers_site": "Launch Realtek Adapter Drivers Site", - "manufacturer": "Manufacturer", - "network_and_system_title": "Network & System", - "u2e_adapter_information": "USB-to-Ethernet Adapter Information", - "u2e_driver_update_alert": "Update available for Realtek USB-to-Ethernet adapter driver" -} diff --git a/app/src/assets/localization/en/more_panel.json b/app/src/assets/localization/en/more_panel.json deleted file mode 100644 index 63b594c1f37..00000000000 --- a/app/src/assets/localization/en/more_panel.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "app": "App", - "custom_labware": "Custom Labware", - "network_and_system": "Network & System", - "resources": "Resources" -} diff --git a/app/src/assets/localization/en/pipette_wizard_flows.json b/app/src/assets/localization/en/pipette_wizard_flows.json index 5d13142349c..c1694816f00 100644 --- a/app/src/assets/localization/en/pipette_wizard_flows.json +++ b/app/src/assets/localization/en/pipette_wizard_flows.json @@ -77,7 +77,6 @@ "return_probe_error": "Return the calibration probe to its storage location before exiting. {{error}}", "single_mount_attached_error": "Single mount pipette is selected when this is the 96 channel flow", "single_or_8_channel": "{{single}} or {{eight}} pipette", - "something_seems_wrong": "There may be a problem with your pipette. Exit setup and contact Opentrons Support for assistance.", "stand_back": "Stand back, robot is in motion", "try_again": "try again", "unable_to_detect_probe": "Unable to detect calibration probe", diff --git a/app/src/assets/localization/en/protocol_calibration.json b/app/src/assets/localization/en/protocol_calibration.json deleted file mode 100644 index 1c65b64721f..00000000000 --- a/app/src/assets/localization/en/protocol_calibration.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "cal_data_existing_data": "Existing data", - "cal_data_legacy_definition": "Calibration Data N/A", - "cal_data_not_calibrated": "Not yet calibrated", - "cal_data_updated_data": "Updated data", - "cal_panel_title": "Placement and Calibration", - "labware_cal_labware_title": "labware", - "labware_cal_tipracks_title": "tip racks", - "labware_cal_title": "Labware Calibration", - "module_connect_description": "Power up and plug in the required module(s) via the OT-2 USB Ports. Place the modules as shown in the deck map.", - "module_connect_duplicate_description": "Plug the modules in to the USB ports as listed in the Placement and Calibration Panel. Check out our help docs for more information on using modules of the same type.", - "module_connect_instruction": "Plug the modules in to the USB ports as listed below, from left to right.", - "module_connect_missing_tooltip": "Connect module(s) to proceed to labware calibration", - "module_connect_proceed_button": "Continue to labware setup", - "modules_deck_slot_title": "slot", - "modules_module_title": "module", - "modules_title": "modules", - "modules_update_software_tooltip": "Update robot software to see USB port information", - "modules_usb_order_title": "USB order (L to R)", - "modules_usb_port_title": "USB port", - "tip_length_cal_title": "Tip Length Calibration" -} diff --git a/app/src/assets/localization/en/protocol_info.json b/app/src/assets/localization/en/protocol_info.json index d1e73288dcd..20d618db601 100644 --- a/app/src/assets/localization/en/protocol_info.json +++ b/app/src/assets/localization/en/protocol_info.json @@ -10,7 +10,6 @@ "creation_method": "Creation Method", "custom_labware_not_supported": "Robot doesn't support custom labware", "date_added": "Date Added", - "delete_protocol_from_app": "Delete the protocol, make changes to address the error, and resend the protocol to this robot from the Opentrons App.", "delete_protocol": "Delete protocol", "description": "Description", "drag_file_here": "Drag and drop protocol file here", @@ -69,16 +68,11 @@ "required_cal_data_title": "Calibration Data", "required_quantity_title": "Quantity", "required_type_title": "Type", - "rerunning_protocol_modal_body": "Opentrons displays the connected robot’s last protocol run on on the Protocol Upload page. If you run again, Opentrons loads this protocol and applies Labware Offset data if any exists.Clicking “Run Again” will take you directly to the Run tab. If you’d like to review the deck setup or run Labware Position Check before running the protocol, navigate to the Protocol tab.If you recalibrate your robot, it will clear the last run from the upload page. A run can have the following statuses:Not started: when this protocol was loaded on to the robot, it was closed before the user ran itCanceled: when this protocol was loaded on to the robot, it was canceled before the run completedComplete: when this protocol was loaded on to the robot, it was closed after the protocol run completed", - "rerunning_protocol_modal_header": "How Rerunning A Protocol Works", - "rerunning_protocol_modal_link": "Learn more about Labware Offset Data", - "rerunning_protocol_modal_title": "See How Rerunning a Protocol Works", "robot_name_last_run": "{{robot_name}}’s last run", "robot_type_first": "{{robotType}} protocols first", "run_again": "Run again", "run_protocol": "Run protocol", "run_timestamp_title": "Run timestamp", - "send_a_protocol_to_store": "Send a protocol from the Opentrons App to get started.", "simulation_in_progress": "Simulation in Progress", "timestamp": "+{{index}}", "too_many_pins_body": "Remove a protocol in order to add more protocols to your pinned list.", diff --git a/app/src/assets/localization/en/protocol_list.json b/app/src/assets/localization/en/protocol_list.json index d014c27abae..76c1b068d94 100644 --- a/app/src/assets/localization/en/protocol_list.json +++ b/app/src/assets/localization/en/protocol_list.json @@ -1,5 +1,4 @@ { - "a_robot_software_update_is_available": "A robot software update is required to run protocols with this version of the Opentrons App. Go to Robot", "delete_protocol_message": " and its run history will be permanently deleted.", "last_updated_at": "Updated {{date}}", "left_mount": "left mount", diff --git a/app/src/assets/localization/en/protocol_setup.json b/app/src/assets/localization/en/protocol_setup.json index fe3f490b1eb..08e94f26138 100644 --- a/app/src/assets/localization/en/protocol_setup.json +++ b/app/src/assets/localization/en/protocol_setup.json @@ -121,8 +121,6 @@ "lpc_disabled_modules_not_connected": "Make sure all modules are connected before running Labware Position Check", "lpc_disabled_no_tipracks_loaded": "Labware Position Check requires that the protocol loads a tip rack", "lpc_disabled_no_tipracks_used": "Labware Position Check requires that the protocol has at least one pipette that picks up a tip", - "magnetic_module_attention_warning": "Opentrons recommends securing labware with the module’s bracket. See how to secure labware to the Magnetic Module", - "magnetic_module_extra_attention": "Opentrons recommends securing labware with the module’s bracket", "map_view": "Map View", "missing_gripper": "Missing gripper", "missing_instruments": "Missing {{count}}", @@ -130,7 +128,6 @@ "missing_pipettes": "Missing {{count}} pipette", "missing": "Missing", "modal_instructions_title": "{{moduleName}} Setup Instructions", - "modal_instructions": "For step-by-step instructions on setting up your module, consult the Quickstart Guide that came in its box. You can also click the link below or scan the QR code to visit the modules section of the Opentrons Help Center.", "module_and_deck_setup": "Modules & deck", "module_connected": "Connected", "module_disconnected": "Disconnected", @@ -239,22 +236,16 @@ "run_disabled_modules_not_connected": "Make sure all modules are connected before proceeding to run", "run_labware_position_check": "run labware position check", "run": "Run", - "secure_labware_explanation_magnetic_module": "Opentrons recommends ensuring your labware locks to the Magnetic Module by adjusting the black plate bracket on top of the module. Please note there are two sizes of plate brackets supplied with your module: standard and deep well. These brackets can be removed and swapped by unscrewing the modules thumb screw (the silver knob on the front).", - "secure_labware_explanation_thermocycler": "Opentrons recommends securing your labware to the Thermocycler module by closing its latch. Doing so ensures level and accurate plate placement for optimal results.", "secure_labware_instructions": "Secure labware instructions", "secure_labware_modal": "Securing labware to the {{name}}", "secure": "Secure", "setup_for_run": "Setup for Run", - "setup_instructions_description": "For step-by-step instructions on setting up your module, consult the Quickstart Guide that came in its box or scan the QR code to visit the modules section of the Opentrons Help Center.", "setup_instructions": "setup instructions", "setup_is_view_only": "Setup is view-only once run has started", "slot_location": "Slot {{slotName}}", "slot_number": "Slot Number", "status": "Status", "step": "STEP {{index}}", - "thermocycler_attention_warning": " Labware must be secured with the module’s latch. See how to secure labware to the Thermocycler Module Thermocycler lid must be open when robot moves to the slots it occupies. Opentrons will automatically open the lid to move to these slots during Labware Position Check.", - "thermocycler_extra_attention_gen_1": "Labware must be secured with the module’s latch. Thermocycler lid must be open when robot moves to the slots it occupies. Opentrons will automatically open the lid to move to these slots during Labware Position Check.", - "thermocycler_extra_attention_gen_2": "The lid will automatically open when moving to these slots during Labware Position Check.", "tip_length_cal_description_bullet": "Perform Tip Length Calibration for each new tip type used on a pipette.", "tip_length_cal_description": "This measures the Z distance between the bottom of the tip and the pipette’s nozzle. If you redo the tip length calibration for the tip you used to calibrate a pipette, you will also have to redo that Pipette Offset Calibration.", "tip_length_cal_title": "Tip Length Calibration", @@ -273,6 +264,5 @@ "view_setup_instructions": "View setup instructions", "volume": "Volume", "what_labware_offset_is": "A Labware Offset is a type of positional adjustment that accounts for small, real-world variances in the overall position of the labware on a robot’s deck. Labware Offset data is unique to a specific combination of labware definition, deck slot, and robot.", - "why_use_lpc": "Labware Position Check is intended to correct for minor variances. Opentrons does not recommend using Labware Position Check to compensate for large positional adjustments. Needing to set large labware offsets could indicate a problem with robot calibration.", "with_the_chosen_value": "With the chosen values, the following error occurred:" } diff --git a/app/src/assets/localization/en/robot_advanced_settings.json b/app/src/assets/localization/en/robot_advanced_settings.json deleted file mode 100644 index 8ce165ecaa0..00000000000 --- a/app/src/assets/localization/en/robot_advanced_settings.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "download_logs_button": "download", - "download_logs_description": "Access logs from this robot.", - "download_logs_label": "download logs", - "log_opt_out_explanation": "If your OT-2 is connected to the internet, Opentrons will collect logs from your robot to troubleshoot issues and identify error trends.", - "log_opt_out_heading": "Robot Logging", - "log_opt_out_instruction": "If you would like to disable log collection, please click "Opt out" below.", - "open_jupyter_description": "Open the Jupyter Notebook running on this OT-2 in your web browser. (Experimental feature! See documentation for more details.)", - "open_jupyter_label": "Jupyter Notebook", - "opt_in": "Sounds Good!", - "opt_out": "Opt Out", - "reset_button": "reset", - "reset_description": "Restore robot to factory configuration.", - "reset_label": "factory reset", - "title": "advanced settings", - "update_from_file_description": "If your app is unable to auto-download robot updates, you can download the robot update yourself and update your robot manually.", - "update_from_file_label": "Update robot software from file" -} diff --git a/app/src/assets/localization/en/robot_calibration.json b/app/src/assets/localization/en/robot_calibration.json index 9828ed706c4..1e96d8d1c24 100644 --- a/app/src/assets/localization/en/robot_calibration.json +++ b/app/src/assets/localization/en/robot_calibration.json @@ -11,13 +11,11 @@ "calibrate_z_axis_on_block": "Calibrate z-axis on block", "calibrate_z_axis_on_slot": "Calibrate z-axis in slot 5", "calibrate_z_axis_on_trash": "Calibrate z-axis on trash bin", - "calibration_block_description": "This block is a specially made tool that fits perfectly on your deck and helps with calibration.If you do not have a Calibration Block, please email support@opentrons.com so we can send you one. In your message, be sure to include your name, company or institution name, and shipping address. While you wait for the block to arrive, you can use the flat surface on the trash bin of your robot instead.", "calibration_complete": "Calibration complete", "calibration_dashboard": "Calibration Dashboard", "calibration_health_check": "Calibration Health Check", "calibration_health_check_intro_body": "Calibration Health Check diagnoses problems with Deck, Tip Length, and Pipette Offset Calibration.You will move the pipettes to various positions, which will be compared against your existing calibration data.If there is a large difference, you will be prompted to redo some or all of your calibrations.", "calibration_health_check_results": "Calibration Health Check Results", - "calibration_on_opentrons_tips_is_important": "It’s extremely important to perform this calibration using the Opentrons tips and tip racks specified above, as the robot determines accuracy based on the known measurements of these tips.", "calibration_recommended": "Calibration recommended", "calibration_status": "Calibration Status", "calibration_status_description": "For accurate and precise movement, calibrate the robot's deck, pipette offsets, and tip lengths.", @@ -84,8 +82,6 @@ "need_help": "Need help?", "no_pipette": "No pipette attached", "no_tip_length": "Calibrate your pipette to see saved tip length", - "opentrons": "opentrons", - "opentrons_tip_racks_recommended": "Opentrons tip racks are highly recommended. Accuracy cannot be guaranteed with other tip racks.", "pick_up_tip": "Pick up tip", "pipette_name_and_serial": "{{name}}, {{serial}}", "pipette_offset_calibration": "Pipette Offset Calibration", diff --git a/app/src/assets/localization/en/robot_connection.json b/app/src/assets/localization/en/robot_connection.json deleted file mode 100644 index 46d1874d51c..00000000000 --- a/app/src/assets/localization/en/robot_connection.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "connect": "connect", - "connected_description": "Your app is currently connected to your robot via {{type}} at IP address {{ip}}", - "connection_label": "this robot is currently", - "connection_status_default": "idle", - "connection_status_disconnected": "unknown - connect to view status", - "connection_status_not_connectable": "not connectable", - "connection_title": "status", - "disconnect": "disconnect", - "disconnected_description": "Your app is trying to connect to your robot via {{type}} at IP address {{ip}}", - "failed_connection_heading": "Could not connect to robot", - "health_status_not_ok": "not responding correctly to requests", - "health_status_ok": "responding to requests", - "health_status_unreachable": "unreachable", - "internet_status_full": "Internet: The robot is connected to a network and has full access to the Internet.", - "internet_status_limited": "Internet: The robot is connected to a network, but it has no access to the Internet.", - "internet_status_none": "Internet: The robot is not connected to any network.", - "internet_status_portal": "Internet: The robot is behind a captive portal and cannot reach the full Internet.", - "internet_status_unknown": "Internet: Unknown", - "internet_status": "Internet: Unknown", - "ip": "{{type}} IP: {{ip}}", - "last_resort": "If your robot remains unresponsive, please reach out to our Support team.", - "mac": "{{type}} MAC Address: {{mac}}", - "no_server_message": "This OT-2 has been seen recently, but it is currently {{status}} at IP address {{ip}}.We recommend power-cycling your robot.", - "server_message": "Your OT-2's API server is {{status}} at IP address {{ip}}.We recommend the following troubleshooting steps:
    1. Power-cycle your robot
    2. If power-cycling does not work, please update your robot's software
      (Note: your robot's update server is still responding and should accept an update.)
    ", - "subnet": "{{type}} Subnet Mask: {{subnet}}", - "success_banner": "{{robot}} successfully connected", - "title": "connectivity", - "unresponsive_title": "Unable to establish connection with robot", - "wired": "wired", - "wireless": "wireless" -} diff --git a/app/src/assets/localization/en/robot_info.json b/app/src/assets/localization/en/robot_info.json deleted file mode 100644 index f608623ac4c..00000000000 --- a/app/src/assets/localization/en/robot_info.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "api_version_min_max": "min: {{min}}, max: {{max}}", - "firmware_version": "firmware version", - "robot_name": "robot name", - "server_version": "server version", - "supported_api_versions": "supported protocol API versions", - "title": "information" -} diff --git a/app/src/assets/localization/en/run_details.json b/app/src/assets/localization/en/run_details.json index 90a6977806e..53fbf0956ff 100644 --- a/app/src/assets/localization/en/run_details.json +++ b/app/src/assets/localization/en/run_details.json @@ -22,7 +22,6 @@ "comment_step": "Comment", "comment": "Comment", "complete_protocol_to_download": "Complete the protocol to download the run log", - "contact_information": "Download the robot logs from the Opentrons App and send it to support@opentrons.com for assistance.", "current_step_pause_timer": "Timer", "current_step_pause": "Current Step - Paused by User", "current_step": "Current Step", @@ -101,8 +100,6 @@ "run_completed": "Run completed.", "run_cta_disabled": "Complete required steps on Protocol tab before starting the run", "run_failed_modal_body": "Error occurred when protocol was {{command}}", - "run_failed_modal_description_desktop": "Download the run log and send it to support@opentrons.com for assistance.", - "run_failed_modal_description": "Please contact support@opentrons.com with relevant information for assistance with troubleshooting.", "run_failed_modal_header": "{{errorName}}: {{errorCode}} at protocol step {{count}}", "run_failed_modal_title": "Run failed", "run_failed_splash": "Run failed", diff --git a/app/src/assets/localization/en/shared.json b/app/src/assets/localization/en/shared.json index adb939134f8..fe1a9bb21e6 100644 --- a/app/src/assets/localization/en/shared.json +++ b/app/src/assets/localization/en/shared.json @@ -11,10 +11,8 @@ "clear_data": "clear data", "close_robot_door": "Close the robot door before starting the run.", "close": "close", - "computer_in_app_is_controlling_robot": "A computer with the Opentrons App is currently controlling this robot.", "confirm_placement": "Confirm placement", "confirm_position": "Confirm position", - "confirm_terminate": "This will immediately stop the activity begun on a computer. You, or another user, may lose progress or see an error in the Opentrons App.", "confirm_values": "Confirm values", "confirm": "Confirm", "continue_activity": "Continue activity", @@ -35,7 +33,6 @@ "exit": "exit", "extension_mount": "extension mount", "flow_complete": "{{flowName}} complete!", - "general_error_message": "If you keep getting this message, try restarting your app and/or robot. If this does not resolve the issue please contact Opentrons Support.", "get_started": "Get started", "github": "GitHub", "go_back": "Go back", diff --git a/app/src/i18n.ts b/app/src/i18n.ts index 9e03af972c0..38bb6803b45 100644 --- a/app/src/i18n.ts +++ b/app/src/i18n.ts @@ -5,49 +5,43 @@ import { initReactI18next } from 'react-i18next' import { resources } from './assets/localization' import { titleCase } from '@opentrons/shared-data' -i18n.use(initReactI18next).init( - { - resources, - lng: 'en', - fallbackLng: 'en', - debug: process.env.NODE_ENV === 'development', - ns: [ - 'shared', - 'robot_advanced_settings', - 'robot_calibration', - 'robot_connection', - 'robot_controls', - 'robot_info', - 'top_navigation', - ], - defaultNS: 'shared', - interpolation: { - escapeValue: false, // not needed for react as it escapes by default - format: function (value, format, lng) { - if (format === 'upperCase') return value.toUpperCase() - if (format === 'lowerCase') return value.toLowerCase() - if (format === 'capitalize') return capitalize(value) - if (format === 'sentenceCase') return startCase(value) - if (format === 'titleCase') return titleCase(value) - return value - }, - }, - keySeparator: false, // use namespaces and context instead - saveMissing: true, - missingKeyHandler: (lng, ns, key) => { - process.env.NODE_ENV === 'test' - ? console.error(`Missing ${lng} Translation: key={${key}} ns={${ns}}`) - : console.warn(`Missing ${lng} Translation: key={${key}} ns={${ns}}`) +import type { InitOptions } from 'i18next' + +const i18nConfig: InitOptions = { + resources, + lng: 'en', + fallbackLng: 'en', + debug: process.env.NODE_ENV === 'development', + defaultNS: 'shared', + interpolation: { + escapeValue: false, // not needed for react as it escapes by default + format: function (value, format, lng) { + if (format === 'upperCase') return value.toUpperCase() + if (format === 'lowerCase') return value.toLowerCase() + if (format === 'capitalize') return capitalize(value) + if (format === 'sentenceCase') return startCase(value) + if (format === 'titleCase') return titleCase(value) + return value }, }, - err => { - if (err) { - console.error( - 'Internationalization was not initialized properly. error: ', - err - ) - } + keySeparator: false, // use namespaces and context instead + saveMissing: true, + missingKeyHandler: (lng, ns, key) => { + process.env.NODE_ENV === 'test' + ? console.error(`Missing ${lng} Translation: key={${key}} ns={${ns}}`) + : console.warn(`Missing ${lng} Translation: key={${key}} ns={${ns}}`) + }, +} + +const i18nCb = (err?: Error): void => { + if (err != null) { + console.error( + 'Internationalization was not initialized properly. error: ', + err + ) } -) +} + +void i18n.use(initReactI18next).init(i18nConfig, i18nCb) -export { i18n } +export { i18n, i18nCb, i18nConfig } diff --git a/app/src/index.tsx b/app/src/index.tsx index f6f4918d769..e37435c9aba 100644 --- a/app/src/index.tsx +++ b/app/src/index.tsx @@ -5,10 +5,8 @@ import { Provider } from 'react-redux' import { ConnectedRouter } from 'connected-react-router' -import { I18nextProvider } from 'react-i18next' import { ApiClientProvider } from '@opentrons/react-api-client' -import { i18n } from './i18n' import { createLogger } from './logger' import { uiInitialized } from './redux/shell' @@ -38,9 +36,7 @@ root.render( - - - + diff --git a/app/src/organisms/AdvancedSettings/OverridePathToPython.tsx b/app/src/organisms/AdvancedSettings/OverridePathToPython.tsx index 0ca2d39579f..57331d825bf 100644 --- a/app/src/organisms/AdvancedSettings/OverridePathToPython.tsx +++ b/app/src/organisms/AdvancedSettings/OverridePathToPython.tsx @@ -30,7 +30,7 @@ import { import type { Dispatch } from '../../redux/types' export function OverridePathToPython(): JSX.Element { - const { t } = useTranslation('app_settings') + const { t } = useTranslation(['app_settings', 'branded']) const pathToPythonInterpreter = useSelector(getPathToPythonOverride) const dispatch = useDispatch() const trackEvent = useTrackEvent() @@ -54,7 +54,7 @@ export function OverridePathToPython(): JSX.Element { {t('override_path_to_python')} - {t('opentrons_app_will_use_interpreter')} + {t('branded:opentrons_app_will_use_interpreter')} () const isLabwareOffsetCodeSnippetsOn = useSelector( getIsLabwareOffsetCodeSnippetsOn @@ -47,7 +47,7 @@ export function ShowLabwareOffsetSnippets(): JSX.Element { {t('show_labware_offset_snippets')} - {t('show_labware_offset_snippets_description')} + {t('branded:show_labware_offset_snippets_description')} () const [showUpdateModal, setShowUpdateModal] = React.useState(false) - const { t } = useTranslation('app_settings') + const { t } = useTranslation(['app_settings', 'branded']) const { makeToast } = useToaster() const { removeActiveAppUpdateToast } = useRemoveActiveAppUpdateToast() @@ -54,10 +54,14 @@ export function AlertsModal({ toastIdRef }: AlertsModalProps): JSX.Element { // Only run this hook on app startup React.useEffect(() => { if (hasJustUpdated) { - makeToast(t('opentrons_app_successfully_updated'), SUCCESS_TOAST, { - closeButton: true, - disableTimeout: true, - }) + makeToast( + t('branded:opentrons_app_successfully_updated'), + SUCCESS_TOAST, + { + closeButton: true, + disableTimeout: true, + } + ) dispatch(toggleConfigValue('update.hasJustUpdated')) } }, []) @@ -65,7 +69,7 @@ export function AlertsModal({ toastIdRef }: AlertsModalProps): JSX.Element { React.useEffect(() => { if (createAppUpdateAvailableToast) { toastIdRef.current = makeToast( - t('opentrons_app_update_available_variation'), + t('branded:opentrons_app_update_available_variation'), WARNING_TOAST, { closeButton: true, diff --git a/app/src/organisms/AppSettings/ConnectRobotSlideout.tsx b/app/src/organisms/AppSettings/ConnectRobotSlideout.tsx index eaef9184985..1935cd33d78 100644 --- a/app/src/organisms/AppSettings/ConnectRobotSlideout.tsx +++ b/app/src/organisms/AppSettings/ConnectRobotSlideout.tsx @@ -43,7 +43,7 @@ export function ConnectRobotSlideout({ const [mostRecentDiscovered, setMostRecentDiscovered] = React.useState< boolean | null >(null) - const { t } = useTranslation(['app_settings', 'shared']) + const { t } = useTranslation(['app_settings', 'shared', 'branded']) const dispatch = useDispatch() const refreshDiscovery = (): unknown => dispatch(startDiscovery()) const isScanning = useSelector(getScanning) @@ -81,7 +81,7 @@ export function ConnectRobotSlideout({ {t('ip_description_first')} - {t('ip_description_second')} + {t('branded:ip_description_second')} - {t('restore_description')} + {t('branded:restore_description')} - {t('learn_uninstalling')} + {t('branded:learn_uninstalling')} - {t('previous_releases')} + {t('branded:previous_releases')} diff --git a/app/src/organisms/CalibrateTipLength/AskForCalibrationBlockModal.tsx b/app/src/organisms/CalibrateTipLength/AskForCalibrationBlockModal.tsx index 41a21ff0cba..c767cb4ee39 100644 --- a/app/src/organisms/CalibrateTipLength/AskForCalibrationBlockModal.tsx +++ b/app/src/organisms/CalibrateTipLength/AskForCalibrationBlockModal.tsx @@ -40,7 +40,7 @@ interface Props { } export function AskForCalibrationBlockModal(props: Props): JSX.Element { - const { t } = useTranslation(['robot_calibration', 'shared']) + const { t } = useTranslation(['robot_calibration', 'shared', 'branded']) const [rememberPreference, setRememberPreference] = React.useState( true ) @@ -77,7 +77,7 @@ export function AskForCalibrationBlockModal(props: Props): JSX.Element { , supportLink: ( diff --git a/app/src/organisms/CalibrationPanels/ChooseTipRack.tsx b/app/src/organisms/CalibrationPanels/ChooseTipRack.tsx index fed5ab02911..08e0b22e51b 100644 --- a/app/src/organisms/CalibrationPanels/ChooseTipRack.tsx +++ b/app/src/organisms/CalibrationPanels/ChooseTipRack.tsx @@ -75,7 +75,7 @@ export function ChooseTipRack(props: ChooseTipRackProps): JSX.Element { robotName, defaultTipracks, } = props - const { t } = useTranslation(['robot_calibration', 'shared']) + const { t } = useTranslation(['robot_calibration', 'shared', 'branded']) const pipSerial = usePipettesQuery( {}, { @@ -143,7 +143,7 @@ export function ChooseTipRack(props: ChooseTipRackProps): JSX.Element { customTipRacks.length > 0 ? [ { - label: t('opentrons'), + label: t('branded:opentrons_tip_rack_name'), options: opentronsTipRacksOptions, }, { @@ -233,14 +233,14 @@ export function ChooseTipRack(props: ChooseTipRackProps): JSX.Element { - {t('opentrons_tip_racks_recommended')} + {t('branded:opentrons_tip_racks_recommended')} - {t('calibration_on_opentrons_tips_is_important')} + {t('branded:calibration_on_opentrons_tips_is_important')} diff --git a/app/src/organisms/ChooseRobotSlideout/AvailableRobotOption.tsx b/app/src/organisms/ChooseRobotSlideout/AvailableRobotOption.tsx index c65e69ce163..148b9e30e35 100644 --- a/app/src/organisms/ChooseRobotSlideout/AvailableRobotOption.tsx +++ b/app/src/organisms/ChooseRobotSlideout/AvailableRobotOption.tsx @@ -53,7 +53,7 @@ export function AvailableRobotOption( registerRobotBusyStatus, } = props const { ip, local, name: robotName } = robot ?? {} - const { t } = useTranslation('protocol_list') + const { t } = useTranslation(['protocol_list', 'branded']) const dispatch = useDispatch() const robotModel = useSelector((state: State) => getRobotModelByName(state, robotName) @@ -160,7 +160,7 @@ export function AvailableRobotOption( > , }} diff --git a/app/src/organisms/ConfigurePipette/ConfigFormResetButton.tsx b/app/src/organisms/ConfigurePipette/ConfigFormResetButton.tsx index d97524f1e59..32ac241955d 100644 --- a/app/src/organisms/ConfigurePipette/ConfigFormResetButton.tsx +++ b/app/src/organisms/ConfigurePipette/ConfigFormResetButton.tsx @@ -17,13 +17,13 @@ export interface ButtonProps { export function ConfigFormResetButton(props: ButtonProps): JSX.Element { const { onClick, disabled } = props - const { t } = useTranslation(['shared', 'device_details']) + const { t } = useTranslation(['shared', 'branded']) return ( - {t('deck_fixture_setup_modal_bottom_description')} + {t('branded:deck_fixture_setup_modal_bottom_description')} diff --git a/app/src/organisms/Devices/ConnectionTroubleshootingModal.tsx b/app/src/organisms/Devices/ConnectionTroubleshootingModal.tsx index 560eedb235b..03cbde6898a 100644 --- a/app/src/organisms/Devices/ConnectionTroubleshootingModal.tsx +++ b/app/src/organisms/Devices/ConnectionTroubleshootingModal.tsx @@ -48,7 +48,7 @@ export function ConnectionTroubleshootingModal(props: Props): JSX.Element { steps={[t('restart_the_robot'), t('restart_the_app')]} /> - {t('contact_support_for_connection_help', { + {t('branded:contact_support_for_connection_help', { support_email: SUPPORT_EMAIL, })} diff --git a/app/src/organisms/Devices/ProtocolRun/RunFailedModal.tsx b/app/src/organisms/Devices/ProtocolRun/RunFailedModal.tsx index dbaeff488b8..e03287f5959 100644 --- a/app/src/organisms/Devices/ProtocolRun/RunFailedModal.tsx +++ b/app/src/organisms/Devices/ProtocolRun/RunFailedModal.tsx @@ -51,7 +51,7 @@ export function RunFailedModal({ setShowRunFailedModal, highestPriorityError, }: RunFailedModalProps): JSX.Element | null { - const { i18n, t } = useTranslation(['run_details', 'shared']) + const { i18n, t } = useTranslation(['run_details', 'shared', 'branded']) const modalProps: LegacyModalProps = { type: 'error', title: t('run_failed_modal_title'), @@ -89,7 +89,7 @@ export function RunFailedModal({
    - {t('run_failed_modal_description_desktop')} + {t('branded:run_failed_modal_description_desktop')} { - const { t } = useTranslation(['protocol_setup', 'shared']) + const { t } = useTranslation(['protocol_setup', 'shared', 'branded']) const moduleName = getModuleName(props.type) return createPortal( - {t(`secure_labware_explanation_${snakeCase(moduleName)}`)} + {t(`branded:secure_labware_explanation_${snakeCase(moduleName)}`)} { - const { t } = useTranslation(['protocol_setup', 'shared']) + const { t } = useTranslation(['protocol_setup', 'shared', 'branded']) return createPortal( { /> - {t('why_use_lpc')} + {t('branded:why_use_lpc')} - {t('connection_lost_description')} + {t('branded:connection_lost_description')} { @@ -65,7 +65,7 @@ export function RobotServerVersion({ {isFlex ? ( - {t('robot_server_version_ot3_description')} + {t('branded:robot_server_version_ot3_description')} ) : null} diff --git a/app/src/organisms/Devices/RobotSettings/AdvancedTab/SoftwareUpdateModal.tsx b/app/src/organisms/Devices/RobotSettings/AdvancedTab/SoftwareUpdateModal.tsx deleted file mode 100644 index 9884676e224..00000000000 --- a/app/src/organisms/Devices/RobotSettings/AdvancedTab/SoftwareUpdateModal.tsx +++ /dev/null @@ -1,121 +0,0 @@ -import * as React from 'react' -import { useTranslation } from 'react-i18next' -import { useSelector } from 'react-redux' -import { - DIRECTION_COLUMN, - Flex, - JUSTIFY_FLEX_END, - PrimaryButton, - SecondaryButton, - SPACING, - StyledText, - TYPOGRAPHY, -} from '@opentrons/components' -import { getShellUpdateState } from '../../../../redux/shell' -import { useCurrentRunId } from '../../../../organisms/ProtocolUpload/hooks' -// import { ReleaseNotes } from '../../../../molecules/ReleaseNotes' - -import { ExternalLink } from '../../../../atoms/Link/ExternalLink' -import { Banner } from '../../../../atoms/Banner' -import { LegacyModal } from '../../../../molecules/LegacyModal' -import { CONNECTABLE, REACHABLE } from '../../../../redux/discovery' -import { Divider } from '../../../../atoms/structure' -import { useRobot } from '../../hooks' -import { handleUpdateBuildroot } from '../UpdateBuildroot' - -const TECHNICAL_CHANGE_LOG_URL = - 'https://github.com/Opentrons/opentrons/blob/edge/CHANGELOG.md' -const ISSUE_TRACKER_URL = - 'https://github.com/Opentrons/opentrons/issues?q=is%3Aopen+is%3Aissue+label%3Abug' -const RELEASE_NOTES_URL = 'https://github.com/Opentrons/opentrons/releases' - -interface SoftwareUpdateModalProps { - robotName: string - closeModal: () => void -} - -export function SoftwareUpdateModal({ - robotName, - closeModal, -}: SoftwareUpdateModalProps): JSX.Element | null { - const { t } = useTranslation('device_settings') - - const currentRunId = useCurrentRunId() - // ToDo: Add release notes for the new design - const updateState = useSelector(getShellUpdateState) - // const { downloaded, downloading, error, info: updateInfo } = updateState - const { info: updateInfo } = updateState - const version = updateInfo?.version ?? '' - // const releaseNotes = updateInfo?.releaseNotes - const [showUpdateModal, setShowUpdateModal] = React.useState(false) - const robot = useRobot(robotName) - - if (robot?.status !== CONNECTABLE && robot?.status !== REACHABLE) return null - - return !showUpdateModal ? ( - - {t('requires_restarting_the_robot')} - - {/* ToDo: align with new design */} - - {t('app_change_in', { version })} - - - {'None in the Opentrons (Here will be change logs)'} - - - {t('new_features')} - - - {'None in the Opentrons (Here will be features info)'} - - - {t('bug_fixes')} - - - {'None in the Opentrons (Here will be fixes info)'} - - - - {t('view_opentrons_technical_change_log')} - - - {t('view_opentrons_issue_tracker')} - - - {t('view_opentrons_release_notes')} - - - - {t('remind_me_later')} - - { - setShowUpdateModal(true) - handleUpdateBuildroot(robot) - }} - disabled={currentRunId != null} - > - {t('update_robot_now')} - - - - - ) : null -} diff --git a/app/src/organisms/Devices/RobotSettings/AdvancedTab/UpdateRobotSoftware.tsx b/app/src/organisms/Devices/RobotSettings/AdvancedTab/UpdateRobotSoftware.tsx index a8febac7092..bf7a27e389b 100644 --- a/app/src/organisms/Devices/RobotSettings/AdvancedTab/UpdateRobotSoftware.tsx +++ b/app/src/organisms/Devices/RobotSettings/AdvancedTab/UpdateRobotSoftware.tsx @@ -40,7 +40,7 @@ export function UpdateRobotSoftware({ onUpdateStart, isRobotBusy, }: UpdateRobotSoftwareProps): JSX.Element { - const { t } = useTranslation('device_settings') + const { t } = useTranslation(['device_settings', 'branded']) const { updateFromFileDisabledReason } = useSelector((state: State) => { return getRobotUpdateDisplayInfo(state, robotName) }) @@ -77,10 +77,10 @@ export function UpdateRobotSoftware({ {t('update_robot_software')} - {t('update_robot_software_description')} + {t('branded:update_robot_software_description')} - {t('update_robot_software_link')} + {t('branded:update_robot_software_link')} () const value = settings?.value ? settings.value : false const id = settings?.id ? settings.id : 'disableFastProtocolUpload' @@ -54,7 +54,7 @@ export function UseOlderProtocol({ {t('use_older_protocol_analysis_method')} - {t('use_older_protocol_analysis_method_description')} + {t('branded:use_older_protocol_analysis_method_description')} { - const actual = await importOriginal() - return { - ...actual, - getShellUpdateState: vi.fn(), - } -}) -vi.mock('../../../hooks') -vi.mock('../../../../../redux/discovery/selectors') - -const mockClose = vi.fn() - -const render = () => { - return renderWithProviders( - - - , - { i18nInstance: i18n } - ) -} - -describe('RobotSettings SoftwareUpdateModal', () => { - beforeEach(() => { - vi.mocked(useRobot).mockReturnValue(mockReachableRobot) - vi.mocked(getShellUpdateState).mockReturnValue({ - downloaded: true, - info: { - version: '1.2.3', - releaseNotes: 'this is a release', - }, - } as ShellUpdateState) - }) - - it('should render title ,description and button', () => { - render() - screen.getByText('Robot Update Available') - screen.getByText( - 'Updating the robot’s software requires restarting the robot' - ) - screen.getByText('App Changes in 1.2.3') - screen.getByText('New Features') - screen.getByText('Bug Fixes') - screen.getByText('View Opentrons technical change log') - screen.getByText('View Opentrons issue tracker') - screen.getByText('View full Opentrons release notes') - screen.getByRole('button', { name: 'Remind me later' }) - screen.getByRole('button', { name: 'Update robot now' }) - }) - - it('should have correct href', () => { - render() - const changeLogUrl = - 'https://github.com/Opentrons/opentrons/blob/edge/CHANGELOG.md' - const issueTrackerUrl = - 'https://github.com/Opentrons/opentrons/issues?q=is%3Aopen+is%3Aissue+label%3Abug' - const releaseNotesUrl = 'https://github.com/Opentrons/opentrons/releases' - - const linkForChangeLog = screen.getByRole('link', { - name: 'View Opentrons technical change log', - }) - expect(linkForChangeLog).toHaveAttribute('href', changeLogUrl) - - const linkForIssueTracker = screen.getByRole('link', { - name: 'View Opentrons issue tracker', - }) - expect(linkForIssueTracker.closest('a')).toHaveAttribute( - 'href', - issueTrackerUrl - ) - - const linkForReleaseNotes = screen.getByRole('link', { - name: 'View full Opentrons release notes', - }) - expect(linkForReleaseNotes.closest('a')).toHaveAttribute( - 'href', - releaseNotesUrl - ) - }) -}) diff --git a/app/src/organisms/Devices/RobotSettings/AdvancedTab/index.ts b/app/src/organisms/Devices/RobotSettings/AdvancedTab/index.ts index 1c5cb506bc7..86e45ab1f73 100644 --- a/app/src/organisms/Devices/RobotSettings/AdvancedTab/index.ts +++ b/app/src/organisms/Devices/RobotSettings/AdvancedTab/index.ts @@ -7,7 +7,6 @@ export * from './OpenJupyterControl' export * from './RobotInformation' export * from './RobotServerVersion' export * from './ShortTrashBin' -export * from './SoftwareUpdateModal' export * from './Troubleshooting' export * from './UpdateRobotSoftware' export * from './UsageSettings' diff --git a/app/src/organisms/Devices/RobotSettings/ConnectNetwork/DisconnectModal.tsx b/app/src/organisms/Devices/RobotSettings/ConnectNetwork/DisconnectModal.tsx index 660e04a1519..b489af43d1f 100644 --- a/app/src/organisms/Devices/RobotSettings/ConnectNetwork/DisconnectModal.tsx +++ b/app/src/organisms/Devices/RobotSettings/ConnectNetwork/DisconnectModal.tsx @@ -47,7 +47,7 @@ export const DisconnectModal = ({ onCancel, robotName, }: DisconnectModalProps): JSX.Element => { - const { t } = useTranslation(['device_settings', 'shared']) + const { t } = useTranslation(['device_settings', 'shared', 'branded']) const wifiList = useWifiList(robotName) const { wifi } = useSelector((state: State) => @@ -144,7 +144,7 @@ export const DisconnectModal = ({ {isError ? ( - {t('shared:general_error_message')} + {t('branded:general_error_message')} ) : null} diff --git a/app/src/organisms/Devices/RobotSettings/RobotSettingsPrivacy.tsx b/app/src/organisms/Devices/RobotSettings/RobotSettingsPrivacy.tsx index 267171774d9..9a0f0164fc5 100644 --- a/app/src/organisms/Devices/RobotSettings/RobotSettingsPrivacy.tsx +++ b/app/src/organisms/Devices/RobotSettings/RobotSettingsPrivacy.tsx @@ -25,8 +25,8 @@ const INFO_BY_SETTING_ID: { } } = { disableLogAggregation: { - titleKey: 'share_logs_with_opentrons', - descriptionKey: 'share_logs_with_opentrons_description', + titleKey: 'branded:share_logs_with_opentrons', + descriptionKey: 'branded:share_logs_with_opentrons_description', invert: true, }, } @@ -34,7 +34,7 @@ const INFO_BY_SETTING_ID: { export function RobotSettingsPrivacy({ robotName, }: RobotSettingsPrivacyProps): JSX.Element { - const { t } = useTranslation('device_settings') + const { t } = useTranslation(['device_settings', 'branded']) const settings = useSelector((state: State) => getRobotSettings(state, robotName) ) diff --git a/app/src/organisms/Devices/RobotSettings/UpdateBuildroot/UpdateRobotModal.tsx b/app/src/organisms/Devices/RobotSettings/UpdateBuildroot/UpdateRobotModal.tsx index f02ad6ae3ce..4a1ffaec5ea 100644 --- a/app/src/organisms/Devices/RobotSettings/UpdateBuildroot/UpdateRobotModal.tsx +++ b/app/src/organisms/Devices/RobotSettings/UpdateBuildroot/UpdateRobotModal.tsx @@ -155,7 +155,7 @@ export function UpdateRobotModal({ > - {t('update_requires_restarting')} + {t('update_requires_restarting_robot')} diff --git a/app/src/organisms/EmergencyStop/EstopPressedModal.tsx b/app/src/organisms/EmergencyStop/EstopPressedModal.tsx index cb32ae550b9..3cd06ff2fd8 100644 --- a/app/src/organisms/EmergencyStop/EstopPressedModal.tsx +++ b/app/src/organisms/EmergencyStop/EstopPressedModal.tsx @@ -73,7 +73,7 @@ function TouchscreenModal({ isEngaged, closeModal, }: EstopPressedModalProps): JSX.Element { - const { t } = useTranslation('device_settings') + const { t } = useTranslation(['device_settings', 'branded']) const [isResuming, setIsResuming] = React.useState(false) const { acknowledgeEstopDisengage } = useAcknowledgeEstopDisengageMutation() const modalHeader: ModalHeaderBaseProps = { @@ -94,7 +94,7 @@ function TouchscreenModal({ - {t('estop_pressed_description')} + {t('branded:estop_pressed_description')} - {t('estop_pressed_description')} + {t('branded:estop_pressed_description')} - {t('download_logs')} + {t('branded:firmware_update_download_logs')} { const { serialNumber, firmwareVersion, isExpanded, onCloseClick } = props - const { i18n, t } = useTranslation(['device_details', 'shared']) + const { i18n, t } = useTranslation(['device_details', 'shared', 'branded']) return ( { if (createdMaintenanceRunId == null) { createMaintenanceRun({}) @@ -108,7 +108,7 @@ export const BeforeBeginning = ( displayName: t('hex_screwdriver'), subtitle: t('provided_with_robot_use_right_size'), }, - [GRIPPER_LOADNAME]: { displayName: t('gripper') }, + [GRIPPER_LOADNAME]: { displayName: t('branded:gripper') }, } const { bodyI18nKey, equipmentLoadNames } = INFO_BY_FLOW_TYPE[flowType] diff --git a/app/src/organisms/GripperWizardFlows/MountGripper.tsx b/app/src/organisms/GripperWizardFlows/MountGripper.tsx index 7e1636f6d05..a7049cf447d 100644 --- a/app/src/organisms/GripperWizardFlows/MountGripper.tsx +++ b/app/src/organisms/GripperWizardFlows/MountGripper.tsx @@ -58,7 +58,7 @@ export const MountGripper = ( props: GripperWizardStepProps ): JSX.Element | null => { const { proceed, isRobotMoving } = props - const { t } = useTranslation(['gripper_wizard_flows', 'shared']) + const { t } = useTranslation(['gripper_wizard_flows', 'shared', 'branded']) const isOnDevice = useSelector(getIsOnDevice) const [showUnableToDetect, setShowUnableToDetect] = React.useState(false) const [isPending, setIsPending] = React.useState(false) @@ -119,7 +119,7 @@ export const MountGripper = ( ) : ( { const { proceed, successfulAction, isRobotMoving } = props - const { t, i18n } = useTranslation(['gripper_wizard_flows', 'shared']) + const { t, i18n } = useTranslation([ + 'gripper_wizard_flows', + 'shared', + 'branded', + ]) const isOnDevice = useSelector(getIsOnDevice) const infoByAction: { @@ -46,11 +50,11 @@ export const Success = ( } } = { [SUCCESSFULLY_ATTACHED_AND_CALIBRATED]: { - header: t('gripper_successfully_attached_and_calibrated'), + header: t('branded:gripper_successfully_attached_and_calibrated'), buttonText: i18n.format(t('shared:exit'), 'capitalize'), }, [SUCCESSFULLY_CALIBRATED]: { - header: t('gripper_successfully_calibrated'), + header: t('branded:gripper_successfully_calibrated'), buttonText: i18n.format(t('shared:exit'), 'capitalize'), }, [SUCCESSFULLY_ATTACHED]: { @@ -58,7 +62,7 @@ export const Success = ( buttonText: t('calibrate_gripper'), }, [SUCCESSFULLY_DETACHED]: { - header: t('gripper_successfully_detached'), + header: t('branded:gripper_successfully_detached'), buttonText: i18n.format(t('shared:exit'), 'capitalize'), }, } diff --git a/app/src/organisms/GripperWizardFlows/UnmountGripper.tsx b/app/src/organisms/GripperWizardFlows/UnmountGripper.tsx index c8e25bc0228..f0b2467e95d 100644 --- a/app/src/organisms/GripperWizardFlows/UnmountGripper.tsx +++ b/app/src/organisms/GripperWizardFlows/UnmountGripper.tsx @@ -51,7 +51,7 @@ export const UnmountGripper = ( props: GripperWizardStepProps ): JSX.Element | null => { const { proceed, isRobotMoving, goBack, chainRunCommands } = props - const { t } = useTranslation(['gripper_wizard_flows', 'shared']) + const { t } = useTranslation(['gripper_wizard_flows', 'shared', 'branded']) const isOnDevice = useSelector(getIsOnDevice) const [isPending, setIsPending] = React.useState(false) const { data: instrumentsQueryData, refetch } = useInstrumentsQuery({ @@ -100,7 +100,7 @@ export const UnmountGripper = ( return showGripperStillDetected ? ( ) : ( - {t('opentrons_def')} + {t('branded:opentrons_def')} )} diff --git a/app/src/organisms/LabwareDetails/index.tsx b/app/src/organisms/LabwareDetails/index.tsx index 4f0cc83b3a4..7787e13f57f 100644 --- a/app/src/organisms/LabwareDetails/index.tsx +++ b/app/src/organisms/LabwareDetails/index.tsx @@ -65,7 +65,7 @@ export interface LabwareDetailsProps { } export function LabwareDetails(props: LabwareDetailsProps): JSX.Element { - const { t } = useTranslation('labware_landing') + const { t } = useTranslation(['labware_landing', 'branded']) const { definition, modified, filename } = props.labware const { metadata, parameters, brand, wells, ordering } = definition const apiName = definition.parameters.loadName @@ -129,7 +129,7 @@ export function LabwareDetails(props: LabwareDetailsProps): JSX.Element { id="LabwareDetails_opentronsDef" marginLeft={SPACING.spacing4} > - {t('opentrons_def')} + {t('branded:opentrons_def')} )} diff --git a/app/src/organisms/ModuleCard/ErrorInfo.tsx b/app/src/organisms/ModuleCard/ErrorInfo.tsx index 75158e7010f..d8bb5e28b6e 100644 --- a/app/src/organisms/ModuleCard/ErrorInfo.tsx +++ b/app/src/organisms/ModuleCard/ErrorInfo.tsx @@ -29,7 +29,7 @@ interface ErrorInfoProps { } export function ErrorInfo(props: ErrorInfoProps): JSX.Element | null { const { attachedModule } = props - const { t } = useTranslation(['device_details', 'shared']) + const { t } = useTranslation(['device_details', 'shared', 'branded']) const [showErrorDetails, setShowErrorDetails] = React.useState(false) let isError: boolean = false @@ -92,7 +92,7 @@ export function ErrorInfo(props: ErrorInfoProps): JSX.Element | null { {errorDetails} ) : null} - {t('module_error_contact_support')} + {t('branded:module_error_contact_support')} diff --git a/app/src/organisms/ModuleCard/ModuleSetupModal.tsx b/app/src/organisms/ModuleCard/ModuleSetupModal.tsx index 8af56a5bcf4..21e3adb598a 100644 --- a/app/src/organisms/ModuleCard/ModuleSetupModal.tsx +++ b/app/src/organisms/ModuleCard/ModuleSetupModal.tsx @@ -26,7 +26,7 @@ interface ModuleSetupModalProps { export const ModuleSetupModal = (props: ModuleSetupModalProps): JSX.Element => { const { moduleDisplayName } = props - const { t, i18n } = useTranslation(['protocol_setup', 'shared']) + const { t, i18n } = useTranslation(['protocol_setup', 'shared', 'branded']) return createPortal( { width="50%" > - {t('modal_instructions')} + {t('branded:modal_instructions')} }} /> diff --git a/app/src/organisms/ModuleWizardFlows/index.tsx b/app/src/organisms/ModuleWizardFlows/index.tsx index 39c235bd782..be36d681950 100644 --- a/app/src/organisms/ModuleWizardFlows/index.tsx +++ b/app/src/organisms/ModuleWizardFlows/index.tsx @@ -276,7 +276,7 @@ export const ModuleWizardFlows = ( ) : ( , diff --git a/app/src/organisms/NetworkSettings/AlternativeSecurityTypeModal.tsx b/app/src/organisms/NetworkSettings/AlternativeSecurityTypeModal.tsx index b0e365d08fc..375476f2a2e 100644 --- a/app/src/organisms/NetworkSettings/AlternativeSecurityTypeModal.tsx +++ b/app/src/organisms/NetworkSettings/AlternativeSecurityTypeModal.tsx @@ -26,7 +26,7 @@ interface AlternativeSecurityTypeModalProps { export function AlternativeSecurityTypeModal({ setShowAlternativeSecurityTypeModal, }: AlternativeSecurityTypeModalProps): JSX.Element { - const { t } = useTranslation('device_settings') + const { t } = useTranslation(['device_settings', 'branded']) const history = useHistory() const modalHeader: ModalHeaderBaseProps = { title: t('alternative_security_types'), @@ -58,7 +58,7 @@ export function AlternativeSecurityTypeModal({ fontWeight={TYPOGRAPHY.fontWeightRegular} color={COLORS.grey60} > - {t('alternative_security_types_description')} + {t('branded:alternative_security_types_description')} - {t('contact_information')} + {t('branded:contact_information')} { - const { t, i18n } = useTranslation(['pipette_wizard_flows', 'shared']) + const { t, i18n } = useTranslation([ + 'pipette_wizard_flows', + 'shared', + 'branded', + ]) const { isOnDevice, handleOnClick, setShowUnableToDetect } = props const [numberOfTryAgains, setNumberOfTryAgains] = React.useState(0) return ( 2 ? t('something_seems_wrong') : undefined} + subHeader={ + numberOfTryAgains > 2 ? t('branded:something_seems_wrong') : undefined + } iconColor={COLORS.red50} isSuccess={false} > diff --git a/app/src/organisms/PipetteWizardFlows/Results.tsx b/app/src/organisms/PipetteWizardFlows/Results.tsx index 04549e686df..fda57800151 100644 --- a/app/src/organisms/PipetteWizardFlows/Results.tsx +++ b/app/src/organisms/PipetteWizardFlows/Results.tsx @@ -60,7 +60,11 @@ export const Results = (props: ResultsProps): JSX.Element => { setShowErrorMessage, nextMount, } = props - const { t, i18n } = useTranslation(['pipette_wizard_flows', 'shared']) + const { t, i18n } = useTranslation([ + 'pipette_wizard_flows', + 'shared', + 'branded', + ]) const pipetteName = attachedPipettes[mount] != null ? attachedPipettes[mount]?.displayName : '' @@ -263,7 +267,8 @@ export const Results = (props: ResultsProps): JSX.Element => { } } ` - subHeader = numberOfTryAgains > 2 ? t('something_seems_wrong') : undefined + subHeader = + numberOfTryAgains > 2 ? t('branded:something_seems_wrong') : undefined button = ( <> {isOnDevice ? ( diff --git a/app/src/organisms/ProtocolSetupModulesAndDeck/SetupInstructionsModal.tsx b/app/src/organisms/ProtocolSetupModulesAndDeck/SetupInstructionsModal.tsx index 7fbbf4f048e..c7acb6f2a42 100644 --- a/app/src/organisms/ProtocolSetupModulesAndDeck/SetupInstructionsModal.tsx +++ b/app/src/organisms/ProtocolSetupModulesAndDeck/SetupInstructionsModal.tsx @@ -26,7 +26,7 @@ interface SetupInstructionsModalProps { export function SetupInstructionsModal({ setShowSetupInstructionsModal, }: SetupInstructionsModalProps): JSX.Element { - const { i18n, t } = useTranslation('protocol_setup') + const { i18n, t } = useTranslation(['protocol_setup', 'branded']) const modalHeader: ModalHeaderBaseProps = { title: i18n.format(t('setup_instructions'), 'capitalize'), iconName: 'information', @@ -45,7 +45,9 @@ export function SetupInstructionsModal({ gridGap={SPACING.spacing40} > - {t('setup_instructions_description')} + + {t('branded:setup_instructions_description')} + () const allRobotSettings = useSelector((state: State) => @@ -62,7 +62,7 @@ export function Privacy({ lineHeight={TYPOGRAPHY.lineHeight36} fontWeight={TYPOGRAPHY.fontWeightRegular} > - {t('opentrons_cares_about_privacy')} + {t('branded:opentrons_cares_about_privacy')} } onClick={() => dispatch(toggleAnalyticsOptedIn())} diff --git a/app/src/organisms/RobotSettingsDashboard/RobotSystemVersionModal.tsx b/app/src/organisms/RobotSettingsDashboard/RobotSystemVersionModal.tsx index e1fffe74e30..8e2a8675f18 100644 --- a/app/src/organisms/RobotSettingsDashboard/RobotSystemVersionModal.tsx +++ b/app/src/organisms/RobotSettingsDashboard/RobotSystemVersionModal.tsx @@ -51,7 +51,7 @@ export function RobotSystemVersionModal({ > diff --git a/app/src/organisms/TakeoverModal/TakeoverModal.tsx b/app/src/organisms/TakeoverModal/TakeoverModal.tsx index 3dab071bdec..c87f33fc150 100644 --- a/app/src/organisms/TakeoverModal/TakeoverModal.tsx +++ b/app/src/organisms/TakeoverModal/TakeoverModal.tsx @@ -32,7 +32,7 @@ export function TakeoverModal(props: TakeoverModalProps): JSX.Element { confirmTerminate, terminateInProgress, } = props - const { i18n, t } = useTranslation('shared') + const { i18n, t } = useTranslation(['shared', 'branded']) const terminateHeader: ModalHeaderBaseProps = { title: t('terminate') + '?', @@ -46,7 +46,7 @@ export function TakeoverModal(props: TakeoverModalProps): JSX.Element { - {t('confirm_terminate')} + {t('branded:confirm_terminate')} - {t('computer_in_app_is_controlling_robot')} + {t('branded:computer_in_app_is_controlling_robot')} ) : null} {(downloading || downloaded) && error == null ? ( - + closeModal(true)} closeOnOutsideClick={true} footer={appUpdateFooter} @@ -191,7 +194,7 @@ export function UpdateAppModal(props: UpdateAppModalProps): JSX.Element { > - {t('update_requires_restarting')} + {t('branded:update_requires_restarting_app')} diff --git a/app/src/organisms/UpdateRobotBanner/index.tsx b/app/src/organisms/UpdateRobotBanner/index.tsx index ced443a2018..86e2201bf84 100644 --- a/app/src/organisms/UpdateRobotBanner/index.tsx +++ b/app/src/organisms/UpdateRobotBanner/index.tsx @@ -25,7 +25,7 @@ export function UpdateRobotBanner( props: UpdateRobotBannerProps ): JSX.Element | null { const { robot, ...styleProps } = props - const { t } = useTranslation('device_settings') + const { t } = useTranslation(['device_settings', 'branded']) const { autoUpdateAction } = useSelector((state: State) => { return getRobotUpdateDisplayInfo(state, robot?.name) @@ -40,7 +40,7 @@ export function UpdateRobotBanner( > - {t('robot_software_update_required')} + {t('branded:robot_software_update_required')} handleUpdateBuildroot(robot)} diff --git a/app/src/pages/AppSettings/GeneralSettings.tsx b/app/src/pages/AppSettings/GeneralSettings.tsx index 99bdf464d04..553f0e56356 100644 --- a/app/src/pages/AppSettings/GeneralSettings.tsx +++ b/app/src/pages/AppSettings/GeneralSettings.tsx @@ -54,7 +54,7 @@ const GITHUB_LINK = const ENABLE_APP_UPDATE_NOTIFICATIONS = 'Enable app update notifications' export function GeneralSettings(): JSX.Element { - const { t } = useTranslation(['app_settings', 'shared']) + const { t } = useTranslation(['app_settings', 'shared', 'branded']) const dispatch = useDispatch() const trackEvent = useTrackEvent() const [ @@ -113,7 +113,7 @@ export function GeneralSettings(): JSX.Element { type="warning" onCloseClick={() => setShowUpdateBanner(false)} > - {t('opentrons_app_update_available_variation')} + {t('branded:opentrons_app_update_available_variation')} - {t('versions_sync')} + {t('branded:versions_sync')} @@ -218,7 +218,7 @@ export function GeneralSettings(): JSX.Element { alignItems={ALIGN_CENTER} justifyContent={JUSTIFY_SPACE_BETWEEN} > - {t('receive_alert')} + {t('branded:receive_alert')} () const analyticsOptedIn = useSelector((s: State) => getAnalyticsOptedIn(s)) diff --git a/app/src/pages/ConnectViaUSB/index.tsx b/app/src/pages/ConnectViaUSB/index.tsx index 961da9b6092..72130c5444c 100644 --- a/app/src/pages/ConnectViaUSB/index.tsx +++ b/app/src/pages/ConnectViaUSB/index.tsx @@ -22,7 +22,7 @@ import { StepMeter } from '../../atoms/StepMeter' import { MediumButton } from '../../atoms/buttons' export function ConnectViaUSB(): JSX.Element { - const { i18n, t } = useTranslation(['device_settings', 'shared']) + const { i18n, t } = useTranslation(['device_settings', 'shared', 'branded']) const history = useHistory() // TODO(bh, 2023-5-31): active connections from /system/connected isn't exactly the right way to monitor for a usb connection - // the system-server tracks active connections by authorization token, which is valid for 2 hours @@ -92,7 +92,7 @@ export function ConnectViaUSB(): JSX.Element { color={COLORS.grey60} textAlign={TYPOGRAPHY.textAlignCenter} > - {t('find_your_robot')} + {t('branded:find_your_robot')} @@ -134,7 +134,7 @@ export function ConnectViaUSB(): JSX.Element { {t('connect_via_usb_description_2')} - {t('connect_via_usb_description_3')} + {t('branded:connect_via_usb_description_3')} diff --git a/app/src/pages/InitialLoadingScreen/__tests__/InitialLoadingScreen.test.tsx b/app/src/pages/InitialLoadingScreen/__tests__/InitialLoadingScreen.test.tsx index 940c7694c54..a7e9076bb63 100644 --- a/app/src/pages/InitialLoadingScreen/__tests__/InitialLoadingScreen.test.tsx +++ b/app/src/pages/InitialLoadingScreen/__tests__/InitialLoadingScreen.test.tsx @@ -2,33 +2,31 @@ import * as React from 'react' import { vi, it, describe, beforeEach, afterEach } from 'vitest' import { screen } from '@testing-library/react' +import { useRobotSettingsQuery } from '@opentrons/react-api-client' + import { renderWithProviders } from '../../../__testing-utils__' -import { getOnDeviceDisplaySettings } from '../../../redux/config' import { getIsShellReady } from '../../../redux/shell' import { InitialLoadingScreen } from '..' -import type { OnDeviceDisplaySettings } from '../../../redux/config/schema-types' +import type { UseQueryResult } from 'react-query' +import type { RobotSettingsResponse } from '@opentrons/api-client' +vi.mock('@opentrons/react-api-client') vi.mock('../../../redux/config') vi.mock('../../../redux/shell') -const mockSettings = { - sleepMs: 60 * 1000 * 60 * 24 * 7, - brightness: 4, - textSize: 1, - unfinishedUnboxingFlowRoute: null, -} as OnDeviceDisplaySettings - const render = () => { return renderWithProviders() } describe('InitialLoadingScreen', () => { beforeEach(() => { - vi.mocked(getOnDeviceDisplaySettings).mockReturnValue(mockSettings) vi.mocked(getIsShellReady).mockReturnValue(false) + vi.mocked(useRobotSettingsQuery).mockReturnValue(({ + data: { settings: [] }, + } as unknown) as UseQueryResult) }) afterEach(() => { diff --git a/app/src/pages/InitialLoadingScreen/index.tsx b/app/src/pages/InitialLoadingScreen/index.tsx index 5171b2720b3..d57519bfa3b 100644 --- a/app/src/pages/InitialLoadingScreen/index.tsx +++ b/app/src/pages/InitialLoadingScreen/index.tsx @@ -1,5 +1,4 @@ import * as React from 'react' -import { Redirect } from 'react-router-dom' import { useSelector } from 'react-redux' import { ALIGN_CENTER, @@ -10,30 +9,23 @@ import { JUSTIFY_CENTER, SPACING, } from '@opentrons/components' -import { getOnDeviceDisplaySettings } from '../../redux/config' +import { useRobotSettingsQuery } from '@opentrons/react-api-client' import { getIsShellReady } from '../../redux/shell' -const getTargetPath = ( - isShellReady: boolean, - unfinishedUnboxingFlowRoute: string | null -): string | null => { - if (!isShellReady) { - return null - } - if (unfinishedUnboxingFlowRoute != null) { - return unfinishedUnboxingFlowRoute - } - - return '/dashboard' -} -export function InitialLoadingScreen(): JSX.Element { - const { unfinishedUnboxingFlowRoute } = useSelector( - getOnDeviceDisplaySettings - ) +export function InitialLoadingScreen({ + children, +}: { + children?: React.ReactNode +}): JSX.Element { const isShellReady = useSelector(getIsShellReady) - const targetPath = getTargetPath(isShellReady, unfinishedUnboxingFlowRoute) - return ( + // ensure robot-server api is up and settings query data available for localization provider + const { settings } = + useRobotSettingsQuery({ retry: true, retryDelay: 1000 }).data ?? {} + + return isShellReady && settings != null ? ( + <>{children} + ) : ( - {targetPath != null && } ) } diff --git a/app/src/pages/Labware/hooks.tsx b/app/src/pages/Labware/hooks.tsx index caf37544be5..b1453738652 100644 --- a/app/src/pages/Labware/hooks.tsx +++ b/app/src/pages/Labware/hooks.tsx @@ -69,7 +69,7 @@ export function useLabwareFailure(): { labwareFailureMessage: string | null clearLabwareFailure: () => unknown } { - const { t } = useTranslation('labware_landing') + const { t } = useTranslation(['labware_landing', 'branded']) const dispatch = useDispatch() const labwareFailure = useSelector(getAddLabwareFailure) @@ -82,7 +82,7 @@ export function useLabwareFailure(): { } else if (failedFile?.type === 'DUPLICATE_LABWARE_FILE') { errorMessage = t('duplicate_labware_def') } else if (failedFile?.type === 'OPENTRONS_LABWARE_FILE') { - errorMessage = t('opentrons_labware_def') + errorMessage = t('branded:opentrons_labware_def') } labwareFailureMessage = failedFile != null diff --git a/app/src/pages/NetworkSetupMenu/index.tsx b/app/src/pages/NetworkSetupMenu/index.tsx index 7250eaa3dda..11909bdb77f 100644 --- a/app/src/pages/NetworkSetupMenu/index.tsx +++ b/app/src/pages/NetworkSetupMenu/index.tsx @@ -34,13 +34,13 @@ const NetworkSetupOptions = [ { title: 'usb', iconName: 'usb' as IconName, - description: 'connection_description_usb', + description: 'branded:connection_description_usb', destinationPath: '/network-setup/usb', }, ] export function NetworkSetupMenu(): JSX.Element { - const { t } = useTranslation('device_settings') + const { t } = useTranslation(['device_settings', 'branded']) return ( <> @@ -73,7 +73,7 @@ export function NetworkSetupMenu(): JSX.Element { color={COLORS.grey60} textAlign={TYPOGRAPHY.textAlignCenter} > - {t('network_setup_menu_description')} + {t('branded:network_setup_menu_description')} - {t('send_a_protocol_to_store')} + {t('branded:send_a_protocol_to_store')} ) diff --git a/app/src/pages/ProtocolDashboard/ProtocolCard.tsx b/app/src/pages/ProtocolDashboard/ProtocolCard.tsx index 1ed35b8632f..305ca99c7bc 100644 --- a/app/src/pages/ProtocolDashboard/ProtocolCard.tsx +++ b/app/src/pages/ProtocolDashboard/ProtocolCard.tsx @@ -60,7 +60,7 @@ export function ProtocolCard(props: { showFailedAnalysisModal, setShowFailedAnalysisModal, ] = React.useState(false) - const { t, i18n } = useTranslation('protocol_info') + const { t, i18n } = useTranslation(['protocol_info', 'branded']) const protocolName = protocol.metadata.protocolName ?? protocol.files[0].name const longpress = useLongPress() const queryClient = useQueryClient() @@ -264,7 +264,9 @@ export function ProtocolCard(props: { }} /> - {t('delete_protocol_from_app')} + + {t('branded:delete_protocol_from_app')} + () const localRobot = useSelector(getLocalRobot) @@ -57,7 +57,7 @@ export function AnalyticsOptInModal({ } return ( - + () const localRobot = useSelector(getLocalRobot) const robotName = localRobot?.name != null ? localRobot.name : 'no name' @@ -144,7 +148,7 @@ export function RobotSettingsList(props: RobotSettingsListProps): JSX.Element { setCurrentOption('Privacy')} iconName="privacy" /> diff --git a/app/src/pages/Welcome/index.tsx b/app/src/pages/Welcome/index.tsx index 47bb9cbcb50..f5c1ac686bd 100644 --- a/app/src/pages/Welcome/index.tsx +++ b/app/src/pages/Welcome/index.tsx @@ -17,7 +17,7 @@ import screenImage from '../../assets/images/on-device-display/welcome_backgroun const IMAGE_ALT = 'Welcome screen background image' export function Welcome(): JSX.Element { - const { t } = useTranslation(['device_settings', 'shared']) + const { t } = useTranslation(['device_settings', 'shared', 'branded']) const history = useHistory() return ( @@ -30,7 +30,7 @@ export function Welcome(): JSX.Element { {IMAGE_ALT} - {t('welcome_title')} + {t('branded:welcome_title')} diff --git a/app/src/redux/robot-settings/types.ts b/app/src/redux/robot-settings/types.ts index 5571be6a441..3f998311c46 100644 --- a/app/src/redux/robot-settings/types.ts +++ b/app/src/redux/robot-settings/types.ts @@ -1,20 +1,10 @@ +import type { + RobotSettings, + RobotSettingsField, + RobotSettingsResponse, +} from '@opentrons/api-client' import type { RobotApiRequestMeta } from '../robot-api/types' -export interface RobotSettingsField { - id: string - title: string - description: string - value: boolean | null - restart_required?: boolean -} - -export type RobotSettings = RobotSettingsField[] - -export interface RobotSettingsResponse { - settings: RobotSettings - links?: { restart?: string } -} - export interface PerRobotRobotSettingsState { settings: RobotSettings restartPath: string | null @@ -94,3 +84,6 @@ export type RobotSettingsAction = | UpdateSettingAction | UpdateSettingSuccessAction | UpdateSettingFailureAction + +// TODO(bh, 2024-03-26): update type imports elsewhere to @opentrons/api-client +export type { RobotSettings, RobotSettingsField, RobotSettingsResponse } diff --git a/react-api-client/src/robot/__tests__/useRobotSettingsQuery.test.tsx b/react-api-client/src/robot/__tests__/useRobotSettingsQuery.test.tsx new file mode 100644 index 00000000000..2f980be9473 --- /dev/null +++ b/react-api-client/src/robot/__tests__/useRobotSettingsQuery.test.tsx @@ -0,0 +1,81 @@ +import * as React from 'react' +import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest' +import { QueryClient, QueryClientProvider } from 'react-query' +import { renderHook, waitFor } from '@testing-library/react' + +import { getRobotSettings } from '@opentrons/api-client' +import { useHost } from '../../api' +import { useRobotSettingsQuery } from '..' + +import type { + HostConfig, + Response, + RobotSettingsResponse, +} from '@opentrons/api-client' +import type { UseRobotSettingsQueryOptions } from '../useRobotSettingsQuery' + +vi.mock('@opentrons/api-client') +vi.mock('../../api/useHost') + +const HOST_CONFIG: HostConfig = { hostname: 'localhost' } +const ROBOT_SETTINGS_RESPONSE: RobotSettingsResponse = { + settings: [ + { + id: 'enableOEMMode', + title: 'Enable OEM Mode', + description: 'a mode for an OEM', + value: false, + }, + ], +} + +describe('useRobotSettingsQuery hook', () => { + let wrapper: React.FunctionComponent< + { children: React.ReactNode } & UseRobotSettingsQueryOptions + > + + beforeEach(() => { + const queryClient = new QueryClient() + const clientProvider: React.FunctionComponent< + { children: React.ReactNode } & UseRobotSettingsQueryOptions + > = ({ children }) => ( + {children} + ) + + wrapper = clientProvider + }) + + afterEach(() => { + vi.resetAllMocks() + }) + + it('should return no data if no host', () => { + vi.mocked(useHost).mockReturnValue(null) + + const { result } = renderHook(() => useRobotSettingsQuery(), { wrapper }) + + expect(result.current?.data).toBeUndefined() + }) + + it('should return no data if robot settings request fails', () => { + vi.mocked(useHost).mockReturnValue(HOST_CONFIG) + vi.mocked(getRobotSettings).mockRejectedValue('oh no') + + const { result } = renderHook(() => useRobotSettingsQuery(), { wrapper }) + + expect(result.current?.data).toBeUndefined() + }) + + it('should return robot settings response data', async () => { + vi.mocked(useHost).mockReturnValue(HOST_CONFIG) + vi.mocked(getRobotSettings).mockResolvedValue({ + data: ROBOT_SETTINGS_RESPONSE, + } as Response) + + const { result } = renderHook(() => useRobotSettingsQuery(), { wrapper }) + + await waitFor(() => { + expect(result.current?.data).toEqual(ROBOT_SETTINGS_RESPONSE) + }) + }) +}) diff --git a/react-api-client/src/robot/index.ts b/react-api-client/src/robot/index.ts index 8a539abcea9..4b296d6a4fe 100644 --- a/react-api-client/src/robot/index.ts +++ b/react-api-client/src/robot/index.ts @@ -3,3 +3,4 @@ export { useEstopQuery } from './useEstopQuery' export { useLightsQuery } from './useLightsQuery' export { useAcknowledgeEstopDisengageMutation } from './useAcknowledgeEstopDisengageMutation' export { useSetLightsMutation } from './useSetLightsMutation' +export { useRobotSettingsQuery } from './useRobotSettingsQuery' diff --git a/react-api-client/src/robot/useRobotSettingsQuery.ts b/react-api-client/src/robot/useRobotSettingsQuery.ts new file mode 100644 index 00000000000..455457ec83b --- /dev/null +++ b/react-api-client/src/robot/useRobotSettingsQuery.ts @@ -0,0 +1,21 @@ +import { useQuery } from 'react-query' +import { getRobotSettings } from '@opentrons/api-client' +import { useHost } from '../api' + +import type { UseQueryResult, UseQueryOptions } from 'react-query' +import type { HostConfig, RobotSettingsResponse } from '@opentrons/api-client' + +export type UseRobotSettingsQueryOptions = UseQueryOptions + +export function useRobotSettingsQuery( + options: UseRobotSettingsQueryOptions = {} +): UseQueryResult { + const host = useHost() + const query = useQuery( + [host as HostConfig, 'robot_settings'], + () => getRobotSettings(host as HostConfig).then(response => response.data), + { enabled: host !== null, ...options } + ) + + return query +} From efc6bd62b402594e81fd7e081ae587d550c5b6d7 Mon Sep 17 00:00:00 2001 From: Jeremy Leon Date: Fri, 12 Apr 2024 15:58:34 -0400 Subject: [PATCH 111/194] fix(api): raise an error if protocol's defined parameters have duplicate variable names (#14888) --- .../protocol_api/_parameter_context.py | 4 +++ .../protocols/parameters/validation.py | 13 +++++++- .../protocol_api/test_parameter_context.py | 31 +++++++++++++++++-- .../protocols/parameters/test_validation.py | 7 +++++ 4 files changed, 51 insertions(+), 4 deletions(-) diff --git a/api/src/opentrons/protocol_api/_parameter_context.py b/api/src/opentrons/protocol_api/_parameter_context.py index 7773653a9c5..8c9debd882c 100644 --- a/api/src/opentrons/protocol_api/_parameter_context.py +++ b/api/src/opentrons/protocol_api/_parameter_context.py @@ -52,6 +52,7 @@ def add_int( description: A description of the parameter as it will show up on the frontend. unit: An optional unit to be appended to the end of the integer as it shown on the frontend. """ + validation.validate_variable_name_unique(variable_name, set(self._parameters)) parameter = parameter_definition.create_int_parameter( display_name=display_name, variable_name=variable_name, @@ -88,6 +89,7 @@ def add_float( description: A description of the parameter as it will show up on the frontend. unit: An optional unit to be appended to the end of the float as it shown on the frontend. """ + validation.validate_variable_name_unique(variable_name, set(self._parameters)) parameter = parameter_definition.create_float_parameter( display_name=display_name, variable_name=variable_name, @@ -115,6 +117,7 @@ def add_bool( default: The default value the boolean parameter will be set to. This will be used in initial analysis. description: A description of the parameter as it will show up on the frontend. """ + validation.validate_variable_name_unique(variable_name, set(self._parameters)) parameter = parameter_definition.create_bool_parameter( display_name=display_name, variable_name=variable_name, @@ -145,6 +148,7 @@ def add_str( Mutually exclusive with minimum and maximum. description: A description of the parameter as it will show up on the frontend. """ + validation.validate_variable_name_unique(variable_name, set(self._parameters)) parameter = parameter_definition.create_str_parameter( display_name=display_name, variable_name=variable_name, diff --git a/api/src/opentrons/protocols/parameters/validation.py b/api/src/opentrons/protocols/parameters/validation.py index 166055df504..9410db294ed 100644 --- a/api/src/opentrons/protocols/parameters/validation.py +++ b/api/src/opentrons/protocols/parameters/validation.py @@ -1,5 +1,5 @@ import keyword -from typing import List, Optional, Union, Literal +from typing import List, Set, Optional, Union, Literal from .types import ( AllowedTypes, @@ -16,6 +16,17 @@ DESCRIPTION_MAX_LEN = 100 +def validate_variable_name_unique( + variable_name: str, other_variable_names: Set[str] +) -> None: + """Validate that the given variable name is unique.""" + if variable_name in other_variable_names: + raise ParameterNameError( + f'"{variable_name}" is already defined as a variable name for another parameter.' + f" All variable names must be unique." + ) + + def ensure_display_name(display_name: str) -> str: """Validate display name is within the character limit.""" if len(display_name) > DISPLAY_NAME_MAX_LEN: diff --git a/api/tests/opentrons/protocol_api/test_parameter_context.py b/api/tests/opentrons/protocol_api/test_parameter_context.py index 4d839d72667..7dcc246f216 100644 --- a/api/tests/opentrons/protocol_api/test_parameter_context.py +++ b/api/tests/opentrons/protocol_api/test_parameter_context.py @@ -46,6 +46,9 @@ def subject(api_version: APIVersion) -> ParameterContext: def test_add_int(decoy: Decoy, subject: ParameterContext) -> None: """It should create and add an int parameter definition.""" + subject._parameters["other_param"] = decoy.mock( + cls=mock_parameter_definition.ParameterDefinition + ) param_def = decoy.mock(cls=mock_parameter_definition.ParameterDefinition) decoy.when(param_def.variable_name).then_return("my cool variable") decoy.when( @@ -60,6 +63,7 @@ def test_add_int(decoy: Decoy, subject: ParameterContext) -> None: unit="foot candles", ) ).then_return(param_def) + subject.add_int( display_name="abc", variable_name="xyz", @@ -70,11 +74,16 @@ def test_add_int(decoy: Decoy, subject: ParameterContext) -> None: description="blah blah blah", unit="foot candles", ) + assert param_def is subject._parameters["my cool variable"] + decoy.verify(mock_validation.validate_variable_name_unique("xyz", {"other_param"})) def test_add_float(decoy: Decoy, subject: ParameterContext) -> None: """It should create and add a float parameter definition.""" + subject._parameters["other_param"] = decoy.mock( + cls=mock_parameter_definition.ParameterDefinition + ) param_def = decoy.mock(cls=mock_parameter_definition.ParameterDefinition) decoy.when(param_def.variable_name).then_return("my cooler variable") decoy.when(mock_validation.ensure_float_value(12.3)).then_return(3.21) @@ -83,7 +92,6 @@ def test_add_float(decoy: Decoy, subject: ParameterContext) -> None: decoy.when( mock_validation.ensure_float_choices([{"display_name": "foo", "value": 4.2}]) ).then_return([{"display_name": "bar", "value": 2.4}]) - decoy.when( mock_parameter_definition.create_float_parameter( display_name="abc", @@ -96,6 +104,7 @@ def test_add_float(decoy: Decoy, subject: ParameterContext) -> None: unit="lux", ) ).then_return(param_def) + subject.add_float( display_name="abc", variable_name="xyz", @@ -106,11 +115,16 @@ def test_add_float(decoy: Decoy, subject: ParameterContext) -> None: description="blah blah blah", unit="lux", ) + assert param_def is subject._parameters["my cooler variable"] + decoy.verify(mock_validation.validate_variable_name_unique("xyz", {"other_param"})) def test_add_bool(decoy: Decoy, subject: ParameterContext) -> None: """It should create and add a boolean parameter definition.""" + subject._parameters["other_param"] = decoy.mock( + cls=mock_parameter_definition.ParameterDefinition + ) param_def = decoy.mock(cls=mock_parameter_definition.ParameterDefinition) decoy.when(param_def.variable_name).then_return("my coolest variable") decoy.when( @@ -125,17 +139,23 @@ def test_add_bool(decoy: Decoy, subject: ParameterContext) -> None: description="lorem ipsum", ) ).then_return(param_def) + subject.add_bool( display_name="cba", variable_name="zxy", default=False, description="lorem ipsum", ) + assert param_def is subject._parameters["my coolest variable"] + decoy.verify(mock_validation.validate_variable_name_unique("zxy", {"other_param"})) def test_add_string(decoy: Decoy, subject: ParameterContext) -> None: """It should create and add a string parameter definition.""" + subject._parameters["other_param"] = decoy.mock( + cls=mock_parameter_definition.ParameterDefinition + ) param_def = decoy.mock(cls=mock_parameter_definition.ParameterDefinition) decoy.when(param_def.variable_name).then_return("my slightly less cool variable") decoy.when( @@ -147,6 +167,7 @@ def test_add_string(decoy: Decoy, subject: ParameterContext) -> None: description="fee foo fum", ) ).then_return(param_def) + subject.add_str( display_name="jkl", variable_name="qwerty", @@ -154,7 +175,11 @@ def test_add_string(decoy: Decoy, subject: ParameterContext) -> None: choices=[{"display_name": "bar", "value": "aaa"}], description="fee foo fum", ) + assert param_def is subject._parameters["my slightly less cool variable"] + decoy.verify( + mock_validation.validate_variable_name_unique("qwerty", {"other_param"}) + ) def test_set_parameters(decoy: Decoy, subject: ParameterContext) -> None: @@ -200,5 +225,5 @@ def test_export_parameters_for_protocol( subject._parameters = {"foo": param_def_1, "bar": param_def_2} result = subject.export_parameters_for_protocol() - assert result.x == "a" # type: ignore [attr-defined] - assert result.y == 1.23 # type: ignore [attr-defined] + assert result.x == "a" # type: ignore[attr-defined] + assert result.y == 1.23 # type: ignore[attr-defined] diff --git a/api/tests/opentrons/protocols/parameters/test_validation.py b/api/tests/opentrons/protocols/parameters/test_validation.py index 1f092a51c46..4d3b2fc83b5 100644 --- a/api/tests/opentrons/protocols/parameters/test_validation.py +++ b/api/tests/opentrons/protocols/parameters/test_validation.py @@ -12,6 +12,13 @@ from opentrons.protocols.parameters import validation as subject +def test_validate_variable_name_unique() -> None: + """It should no-op if the name is unique and raise if it is not.""" + subject.validate_variable_name_unique("one of a kind", {"fee", "foo", "fum"}) + with pytest.raises(ParameterNameError): + subject.validate_variable_name_unique("copy", {"paste", "copy", "cut"}) + + def test_ensure_display_name() -> None: """It should ensure the display name is within the character limit.""" result = subject.ensure_display_name("abc") From f695e74e31964c81c460893fab930aa741f7517d Mon Sep 17 00:00:00 2001 From: Sanniti Pimpley Date: Fri, 12 Apr 2024 16:21:28 -0400 Subject: [PATCH 112/194] feat(robot-server): limit the maximum number of analyses stored per protocol (#14885) Closes AUTH-317 # Overview Adds a max-analyses-per-protocol limit of 5 analyses. Auto-deletes the oldest analysis (es) before adding a new one if the number of analyses stored exceeds the max. # Risk assessment Low. Well tested and pretty isolated feature. --- .../protocols/analysis_memcache.py | 11 +++ .../robot_server/protocols/analysis_store.py | 2 - .../protocols/completed_analysis_store.py | 29 ++++++ .../test_completed_analysis_store.py | 89 ++++++++++++++++++- robot-server/tests/protocols/test_memcache.py | 16 ++++ 5 files changed, 144 insertions(+), 3 deletions(-) diff --git a/robot-server/robot_server/protocols/analysis_memcache.py b/robot-server/robot_server/protocols/analysis_memcache.py index 19280009bd5..3ba3156607f 100644 --- a/robot-server/robot_server/protocols/analysis_memcache.py +++ b/robot-server/robot_server/protocols/analysis_memcache.py @@ -63,3 +63,14 @@ def insert(self, key: K, value: V) -> None: self._pop_eldest(key) self._cache[key] = value self._cache_order.appendleft(key) + + def remove(self, key: K) -> None: + """Remove the cached element specified by the key. + + If no such element exists in cache, then simply no-op. + """ + try: + self._cache.pop(key) + self._cache_order.remove(key) # O(n) operation, use sparingly + except KeyError: + pass diff --git a/robot-server/robot_server/protocols/analysis_store.py b/robot-server/robot_server/protocols/analysis_store.py index b0ea474ec07..60ea3d8d743 100644 --- a/robot-server/robot_server/protocols/analysis_store.py +++ b/robot-server/robot_server/protocols/analysis_store.py @@ -129,8 +129,6 @@ def add_pending(self, protocol_id: str, analysis_id: str) -> AnalysisSummary: Returns: A summary of the just-added analysis. """ - # TODO (spp, 2024-03-19): cap the number of analyses being stored by - # auto-deleting old ones new_pending_analysis = self._pending_store.add( protocol_id=protocol_id, analysis_id=analysis_id ) diff --git a/robot-server/robot_server/protocols/completed_analysis_store.py b/robot-server/robot_server/protocols/completed_analysis_store.py index 58017e4398a..60780ab9cf4 100644 --- a/robot-server/robot_server/protocols/completed_analysis_store.py +++ b/robot-server/robot_server/protocols/completed_analysis_store.py @@ -21,6 +21,8 @@ _log = getLogger(__name__) +MAX_ANALYSES_TO_STORE = 5 + @dataclass class CompletedAnalysisResource: @@ -336,6 +338,7 @@ def get_ids_by_protocol(self, protocol_id: str) -> List[str]: async def add(self, completed_analysis_resource: CompletedAnalysisResource) -> None: """Add a resource to the store.""" + self._make_room_for_new_analysis(completed_analysis_resource.protocol_id) statement = analysis_table.insert().values( await completed_analysis_resource.to_sql_values() ) @@ -344,3 +347,29 @@ async def add(self, completed_analysis_resource: CompletedAnalysisResource) -> N self._memcache.insert( completed_analysis_resource.id, completed_analysis_resource ) + + def _make_room_for_new_analysis(self, protocol_id: str) -> None: + """Remove the oldest analyses in store if the number of analyses exceed the max allowed. + + Unlike protocols, protocol analysis IDs are not stored by any DB entities + other than the analysis store itself. So we do not have to worry about cleaning up + any other tables. + """ + analyses_ids = self.get_ids_by_protocol(protocol_id) + + # Delete all analyses exceeding max number allowed, + # plus an additional one to create room for the new one. + # Most existing databases will not have multiple extra analyses per protocol + # but there would be some internally that added multiple analyses before + # we started capping the number of analyses. + analyses_to_delete = analyses_ids[ + : len(analyses_ids) - MAX_ANALYSES_TO_STORE + 1 + ] + + for analysis_id in analyses_to_delete: + self._memcache.remove(analysis_id) + delete_statement = sqlalchemy.delete(analysis_table).where( + analysis_table.c.id == analysis_id + ) + with self._sql_engine.begin() as transaction: + transaction.execute(delete_statement) diff --git a/robot-server/tests/protocols/test_completed_analysis_store.py b/robot-server/tests/protocols/test_completed_analysis_store.py index f41594d0c5d..438cf8baada 100644 --- a/robot-server/tests/protocols/test_completed_analysis_store.py +++ b/robot-server/tests/protocols/test_completed_analysis_store.py @@ -2,12 +2,13 @@ import json from datetime import datetime, timezone from pathlib import Path -from typing import Optional, Dict +from typing import Optional, Dict, List import pytest from sqlalchemy.engine import Engine from decoy import Decoy +from robot_server.persistence.tables import analysis_table from robot_server.protocols.completed_analysis_store import ( CompletedAnalysisResource, CompletedAnalysisStore, @@ -261,3 +262,89 @@ async def test_get_rtp_values_and_defaults_by_analysis_from_db( decoy.when(memcache.get("analysis-id")).then_raise(KeyError()) result = await subject.get_rtp_values_and_defaults_by_analysis_id("analysis-id") assert result == resource.run_time_parameter_values_and_defaults + + +@pytest.mark.parametrize( + argnames=["existing_analysis_ids", "expected_analyses_ids_after_making_room"], + argvalues=[ + ( + [f"analysis-id-{num}" for num in range(8)], + [ + "analysis-id-4", + "analysis-id-5", + "analysis-id-6", + "analysis-id-7", + "new-analysis-id", + ], + ), + ( + [f"analysis-id-{num}" for num in range(5)], + [ + "analysis-id-1", + "analysis-id-2", + "analysis-id-3", + "analysis-id-4", + "new-analysis-id", + ], + ), + ( + [f"analysis-id-{num}" for num in range(4)], + [ + "analysis-id-0", + "analysis-id-1", + "analysis-id-2", + "analysis-id-3", + "new-analysis-id", + ], + ), + ( + [f"analysis-id-{num}" for num in range(2)], + ["analysis-id-0", "analysis-id-1", "new-analysis-id"], + ), + ([], ["new-analysis-id"]), + ], +) +async def test_add_makes_room_for_new_analysis( + subject: CompletedAnalysisStore, + memcache: MemoryCache[str, CompletedAnalysisResource], + protocol_store: ProtocolStore, + existing_analysis_ids: List[str], + expected_analyses_ids_after_making_room: List[str], + decoy: Decoy, + sql_engine: Engine, +) -> None: + """It should delete old analyses and make room for new analysis.""" + protocol_store.insert(make_dummy_protocol_resource("protocol-id")) + + # Set up the database with existing analyses + resources = [ + _completed_analysis_resource( + analysis_id=analysis_id, + protocol_id="protocol-id", + ) + for analysis_id in existing_analysis_ids + ] + for resource in resources: + statement = analysis_table.insert().values(await resource.to_sql_values()) + with sql_engine.begin() as transaction: + transaction.execute(statement) + + assert subject.get_ids_by_protocol("protocol-id") == existing_analysis_ids + await subject.add( + _completed_analysis_resource( + analysis_id="new-analysis-id", + protocol_id="protocol-id", + ) + ) + assert ( + subject.get_ids_by_protocol("protocol-id") + == expected_analyses_ids_after_making_room + ) + + removed_ids = [ + analysis_id + for analysis_id in existing_analysis_ids + if analysis_id not in expected_analyses_ids_after_making_room + ] + for analysis_id in removed_ids: + decoy.verify(memcache.remove(analysis_id)) diff --git a/robot-server/tests/protocols/test_memcache.py b/robot-server/tests/protocols/test_memcache.py index ce485d8984f..80acb184f20 100644 --- a/robot-server/tests/protocols/test_memcache.py +++ b/robot-server/tests/protocols/test_memcache.py @@ -22,3 +22,19 @@ def test_cache_retains_new_values() -> None: for val in range(1, 4): assert subject.contains(f"key-{val}") assert subject.get(f"key-{val}") == f"value-{val}" + + +def test_cache_removes_values_by_key() -> None: + """It should eject values when asked for it.""" + subject = MemoryCache(3, str, str) + for val in range(3): + subject.insert(f"key-{val}", f"value-{val}") + subject.remove("key-1") + assert not subject.contains("key-1") + + # Make sure cache order is updated + assert subject.contains("key-0") and subject.contains("key-2") + subject.insert("key-4", "value-4") + assert subject.contains("key-0") + subject.insert("key-5", "value-5") + assert not subject.contains("key-0") From 5c4c9fe218b419f71b80902bef8fd39926cbe7e4 Mon Sep 17 00:00:00 2001 From: Brayan Almonte Date: Fri, 12 Apr 2024 16:27:40 -0400 Subject: [PATCH 113/194] feat(system-server): add /system/oem_mode/upload_splash endpoint to change the userspace boot screen. (#14865) --- system-server/Pipfile | 1 + system-server/Pipfile.lock | 14 ++- .../system_server/system/oem_mode/router.py | 97 +++++++++++++++++- .../integration/resources/oem_mode_custom.png | Bin 0 -> 30837 bytes .../resources/oem_mode_wrong_dimensions.png | Bin 0 -> 14963 bytes .../resources/oem_mode_wrong_image_type.jpeg | Bin 0 -> 35332 bytes .../integration/test_oem_mode.tavern.yaml | 89 +++++++++++++++- 7 files changed, 196 insertions(+), 5 deletions(-) create mode 100644 system-server/tests/integration/resources/oem_mode_custom.png create mode 100644 system-server/tests/integration/resources/oem_mode_wrong_dimensions.png create mode 100644 system-server/tests/integration/resources/oem_mode_wrong_image_type.jpeg diff --git a/system-server/Pipfile b/system-server/Pipfile index 78c13a0ff55..d1ce7f43f6a 100644 --- a/system-server/Pipfile +++ b/system-server/Pipfile @@ -14,6 +14,7 @@ pydantic = "==1.10.12" importlib-metadata = ">=4.13.0,<5" sqlalchemy = "==1.4.51" pyjwt = "==2.6.0" +filetype = "==1.2.0" systemd-python = { version = "==234", markers="sys_platform == 'linux'" } server-utils = {editable = true, path = "./../server-utils"} system_server = {path = ".", editable = true} diff --git a/system-server/Pipfile.lock b/system-server/Pipfile.lock index bbaa48e640c..d7d315362f2 100644 --- a/system-server/Pipfile.lock +++ b/system-server/Pipfile.lock @@ -50,6 +50,14 @@ "markers": "python_version >= '3.7'", "version": "==0.99.1" }, + "filetype": { + "hashes": [ + "sha256:66b56cd6474bf41d8c54660347d37afcc3f7d1970648de365c102ef77548aadb", + "sha256:7ce71b6880181241cf7ac8697a2f1eb6a8bd9b429f7ad6d27b8db9ba5f1c2d25" + ], + "index": "pypi", + "version": "==1.2.0" + }, "h11": { "hashes": [ "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", @@ -231,12 +239,12 @@ }, "typing-extensions": { "hashes": [ - "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783", - "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd" + "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0", + "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==4.9.0" + "version": "==4.11.0" }, "uvicorn": { "hashes": [ diff --git a/system-server/system_server/system/oem_mode/router.py b/system-server/system_server/system/oem_mode/router.py index c8c6d96240b..0f3b9aa52f4 100644 --- a/system-server/system_server/system/oem_mode/router.py +++ b/system-server/system_server/system/oem_mode/router.py @@ -1,6 +1,17 @@ """Router for /system/register endpoint.""" -from fastapi import APIRouter, Depends, status, Response +import os +import filetype # type: ignore[import-untyped] +from fastapi import ( + APIRouter, + Depends, + status, + Response, + UploadFile, + File, + HTTPException, +) + from .models import EnableOEMMode from ...settings import SystemServerSettings, get_settings, save_settings @@ -35,3 +46,87 @@ async def enable_oem_mode_endpoint( except Exception: response.status_code = status.HTTP_500_INTERNAL_SERVER_ERROR return response + + +@oem_mode_router.post( + "/system/oem_mode/upload_splash", + summary="Upload an image to be used as the boot up splash screen.", + responses={ + status.HTTP_201_CREATED: {"message": "OEM Mode splash screen uploaded"}, + status.HTTP_400_BAD_REQUEST: {"message": "OEM Mode splash screen not set"}, + status.HTTP_413_REQUEST_ENTITY_TOO_LARGE: { + "message": "File is larger than 5mb" + }, + status.HTTP_415_UNSUPPORTED_MEDIA_TYPE: {"message": "Invalid file type"}, + status.HTTP_500_INTERNAL_SERVER_ERROR: { + "message": "OEM Mode splash unhandled exception." + }, + }, +) +async def upload_splash_image( + response: Response, + file: UploadFile = File(...), + settings: SystemServerSettings = Depends(get_settings), +) -> Response: + """Router for /system/oem_mode/upload_splash endpoint.""" + # Make sure oem mode is enabled before this request + if not settings.oem_mode_enabled: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="OEM Mode needs to be enabled to upload splash image.", + ) + + # Get the file info + file_info = filetype.guess(file.file) + if file_info is None: + raise HTTPException( + status_code=status.HTTP_415_UNSUPPORTED_MEDIA_TYPE, + detail="Unable to determine file type", + ) + + # Only accept PNG files + accepted_file_types = ["image/png", "png"] + content_type = file_info.extension.lower() + if ( + file.content_type not in accepted_file_types + or content_type not in accepted_file_types + ): + raise HTTPException( + status_code=status.HTTP_415_UNSUPPORTED_MEDIA_TYPE, + detail="Unsupported file type", + ) + + file_size = 0 + for chunk in file.file: + file_size += len(chunk) + if file_size > 5 * 1024 * 1024: # 5MB + raise HTTPException( + status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE, + detail="File is larger than 5mb.", + ) + + # TODO: Validate image dimensions + + # return the pointer back to the starting point so that the next read starts from the starting point + await file.seek(0) + + try: + # Remove the old image if exists + if settings.oem_mode_splash_custom: + os.unlink(settings.oem_mode_splash_custom) + + # file is valid, save to final location + filepath = f"{settings.persistence_directory}/{file.filename}" + with open(filepath, "wb+") as f: + f.write(file.file.read()) + + # store the file location to settings and save the dotenv + settings.oem_mode_splash_custom = filepath + success = save_settings(settings) + response.status_code = ( + status.HTTP_201_CREATED if success else status.HTTP_400_BAD_REQUEST + ) + except Exception: + response.status_code = status.HTTP_500_INTERNAL_SERVER_ERROR + + return response diff --git a/system-server/tests/integration/resources/oem_mode_custom.png b/system-server/tests/integration/resources/oem_mode_custom.png new file mode 100644 index 0000000000000000000000000000000000000000..14cf4ac12bd3d6c5601b71eee209f1582ecfc9cf GIT binary patch literal 30837 zcmeFZXHZpHv^BcHoEsG}5L7@=35tM{O-Pa`AQ_dMk({B;ih^XxDk2#aken2eBp?VJ zGAcPMQSuwx?)!c9ZoR5^?^pGHysArAiP)TT_FikvImZ}t%>7V7PGZ~U{hJAb*d`@; zQHdbd;;+mD7S7id8=%oGty?zT6gZ}nvbpT zrRv3{mvohkX?ot@6zl4A?I>KcRCDY~P1M@wronzvtar6MR5lhJ_S(3fru@~%%kFZ! zzy74xo%{J~hU-&CLEW;z`oLS2MD*E+YkAjctp2Jq*AN_V|8e5D%;sMLzwVr)WAW(C z8=X8b;9u$ywU0kN)cMDWw+h2Ia=q1C6&}VtjCqu(SR^)({=wNhLn9?)R%Cw5g>@DN zp(DJ^lGkZu{yKE_5(BOB^+T7EEaCFFAI=Dh-Y6mD;>{B?Wv{e)%B z59JQo&8&ZS(Y3GaU2`k{Y|{1i?((coK`QFHlAz{al(3RfwxK6Jdqk-3W&Q0v;&Wtz zDnzKHB7ke3Kl3`{1#j_h%a4nW?%3K*y=-P_HRc{6N~jMXb6-PGPu;^>r8DQ`sOxC8 z8f7v!w`QH8&G*j__-U~%>u<|DG4d1>j#cSxc(h5(F1^i;J1cDOSt@|G2?U zN>+Ts;2Qevyhk?DF67{!_SuQ6*jOKj>&;XM9jXN><_O-`nX3 z;vgY)@tm?#+i0h=R*=EU>aQKt<2l!J7)~EPCC%yWcf$4g6ZO#0+{Zp9VX>h-#s;rd zhXYMwOmcmK@{isNeRNAuk*hcOQU9$+j~=jxQ&Xo$mCh{c70sIM53(J%jb5H{6HYDX zh#Il(q!dOhdUR9VJ9Q4fBo7@y_n%+%pFZ*b`JVn?UUc@~TmHW<#DDQc z|9;}Zf8XPOmj3@}75|T3^gm1gZ^-=r`qY=4`u$}cLCBxB{&v=R=~p@Hi4zJ-%geEH zk%CG|s+yt7xeEljmR_gruTNlN} zrQFA%0ne3!rN!>m z;GCQ@f_u13e*gSFIsH|>`oazo7TIwApy8&Zk;%d%0aEGd>AfAfwl>p)iVq(=c)Di8 zc4alIvPZNH30K%QZQS@2AM_|BBxLWCOO@5t0ZCWWq|MEfA0IgFL%Sh=|A}i}!c(75 zHN2KRuxaboz_xd0BeR{j8UKg~jySnUe-YQ&3cUG{HSuPb_zQ)Ow6*KkCnj8pOMLch zul>rBvx<_^%V}jDozzs_;#40-k;KD?4>wl$F>{)BzZCaw#BVA)IAk5;82&0RrXt}( zuUJ-AR@IfBkmYU;*cJ>|!jQn5~hU1CKc81sRi&%1 zKD&HGMOF1R_CeXwGF4huR;ApR>8fUyxuV{u$8RfLn0WLbv=umKNJ~m8n3$L(K7W2T zC@_#cpi)1>UK=m8?8te6vllD6IC?6si`($?vllye?XqZN>OZmn$?($rlxa;E-!Wm~ zrR?XaZ{B8f--`3b;pH(z8hQEmNhF%`V-k8?E6qj4p%)e2#IVd6`nY?&s=iGj;H+U!QA~*H1 z_H23g$x1dZ``Y?6T`^i(+E`qa3b`#@Tv>_SGHtitCyYaTa}@%y(^IH#?WlK*gbaq-t^NAu3S zCd2EB*rWYC`WpuvcW${_;r`1SvN*)I#F4|*Qd1QVht=Qqw@(rA(;pw5Tmd@v#vBvo`J-FwbTa

    RS?=ZlC73)MGM)^gZY>GH$GN%XL;3`rCtqFRr1VvXFo<%T#`Vt4 zb45r=OFzx~A&ep}l1O}=4$(iS zrZTF#orhvWK2j(lYf|OL^Cv{(j3p+z3j45fJV>4i8t)QiW?p1xXA93XUP*VIvvgh@ z%Vub}r<$T66?5sy=w$Y-TetLa;N;$I*LY{jy1X!h_ne%Z^k<85x^;WQwKZ!AFK?%@ zca&a)!{lT&w+wDn_(teV-cwxLy|0|%jC2#VSqx6DuGP%G!Z+rMh3tow)eBwTtzEY+ zcUkm$nm7OL_a*19O5k-iP6tF+e(RBImucDA7Q2{!z4YE>o#)Dx=2UI7x#H!Yr90aB zoF?jH?q8W%#+HueaZ=KBqi=5jvU7SJbo^=+@)*mB6JKJ2+ao2;1crp<_t}JAK9avu zcy|k375NGWqmKGS<%Gki75uA0d}htPB}vQ@-4?nAAop^t=R3W8!+nV)TpGFEf z=FJo_?cK{>=rUsyHft@x2=I4JZ~g6aKYsks#{py%pr=$<%Y;lEWoNg}GmyD_Ideg1 za%w7a{Kx9=Up)?su4a_fpwnGWr7^SHxVgDEZQAsC{X$c zz=m`K8NpI%DJdyAzQ7YmK8Kl^?Kjb!zi=VaFgI1F@EAVNVP3O3m@{Byd2wXV&W8`k zN{Zu(qF05e=tEhu`Tk*DWkto)>1n&8TwKAf6NOii9A2Hfy?*EJ-7L7*{iiKCI5o2Z zU1o+`4O+)e3U@~iDE@p!T3=>_d7UV9!tbm$xp z56>w74zC9-tUphcS92G0baiz(EQc@7O$>aa0-TS!Uh(u0k5Mgsf@U^X!BFQUk-PMTr;H|9L76L0=zwA1N99I zDr03rSU5Rl{R0A6j~CR zdyJi3K_-}^-rhBo*Tg^1V>$0aNK$h0WeX%>uElZ`dzG6v-!@r@03mXl32 zfjAnm=uLji5_X(MX7@MGA+lv8KM3TahfJ)2i@SvdB_1ozy z1Pfr0Uz`Ho2x9oL#87GupR;cb~F0jb5VzkdC47#euEi!D(*-yy-9jytiVLkno^ zMn?BX1m69qaPzK$*4L+5ExQU58X6iJ-|Ce&rW;f%U%Pg2;Le|d`D35siWLyc$lXD_ zPh+xr26iV}woMpl+WzOabKxC*b|1A*ak5#_(9m$@7RJWKX=X;C#MwKAqo`Rl&D!C& zXm{@X@iZ$C>f1CttkM5~_A z;$V!god9F_ki%e>uX#sK{fqKML}I7s4g}oDn?5O7*(P`A z*RtWCa+&V_c>Y0?AlSad%Fu<_o9!u{E;dWf(&-8MVGXBucX+M;+x8=xC7AOPT{#m)bX(ENz*>ai=YFZ+G?a6YNH znlWuK!>;HtHq*C_k<&fn0a!$h?o~>@qt!kkhvX*P$lm_`s&Xbz4gjnW`;q3>Sg{=p zf-Jys0oW~BB_*Zgl_J#63jEIKo3YY8Q#I3r^`(cZdCXf*m^8e4INm)ncvX)&>M=oB zpFnoNCI*8Uln+l-RaW}#J7rk0W{2=W+y2jQ+j-n(nyxlvS!k-ix%PKQfitA1yaT5U zAHNP5My*#**MGQm?}?J{)jx;gqO0y~+Li3V87o3f$gojyaB|MXk15K;x8>QJg|GOy z&9)k;-rs6e8{vlxUJZoK(2zy~1Rn;Ws@h1Q6qn8@6z|9TPF-@D8y|bx_onQXudndr zgA2iBs_ugv0TgQJ6reVB@Vem%0l;VB)ukyOlLnUioYjN%F$TRAe*RHWas<)o z_WS$&usOeq3Q6alC#NF})%)wBxu^PTb_GRl`Gw*PdaIKmzbDXD00o$4MhL?TUh*{$^f=@ zcXfb2-S0-5w`}3mxr)oABEI>lrs;5AzkXfS{eHc1Ab_?MP|#?5cHPlnlW!$|+sscI zjOhhdHdzOV9195`-+c?juQ=F++;^Y{@GYznX4Y{^P zNkw`CwUJ>=Z{G%8)pb`XS{Ocla{rUCVrDSV=4ADrMye77+~*bZY|GBkJbK{X;Pu<~ zEC0^L+o%iQh_UIbTFRjU(lOcxPBA^!PDz|#=imrb&oHb!o>uTfxq22vrV_}MOI=-E z)qMvu@g?ahxAV{U8~DZ!OcYG{eC47b_3}+H&2A((7(R@eFcZi&W;Yo$Yp70GcAQDN z7lBQwYe101jSigdcDoYHrF~5LXifL8AKu*3X*z|YZI&{ZzI-e%m(kIQQjbPvOIf>i zZHt9~%j~F0^8i`dZYq>+B(=bw?4VXW235P0JP7YckMxHd6SgxK0J%0&IRW4sEAt3>KxkuK1?4_j=yB2Y|kgqw0Ot zS_OmI?kd-%WmWL&o_TkXw(T+?M7zD*?Qh?%d3)p|we|t{7+PJJC|XSI4DS1WpDFd{ zsrmVN9&m$8e^m2v!=^+PRgpAx4UJ=`jB3e66qvE8jTY-p9FW>Xd`Z<7@*deuM<=vg zbm%acP_%K_<=TB~yng(+;oHv7x$X1}V=fINqvb3^TYE0*+g2t~t*<>>XJq6cA z+=Q?+@7mpnZ3p->orKbR9UI!pMLzR+EIIv}n@js?%O)4`^z-M--UGy&>66S5|GcXA z3);ooO?2UI$oR~$eo<;@lGO+(#CF=U;|o2}Vw=MpA#?VuR|o!W4@JmhDTn@yb;(c& zH-N*Bo}Ne^;*H(Tv+`T9w^Rx#8hqNTD~M~}`Afm^3s>7c?J?J_+TCdf8?ZlgUr+A%07ZaDu{Wxk|8ILs>*Sslz zm=ViNfMsqZjy*TB-o?nsB`-Svs((Rjbtz-1hiTP1VPyqrNPnmyR&_bJrDrB5xOT?c zjHR+ZMxv(6XJt0KxYbSI^FH^W@bJZUhmU^_Sc%2uh%<_)Z85nk^HT#UQD?r;6wUV^ zsF|?KYWm!2?V)jR)%yWJS^j4p9CJ;jD_8imPqg^sQiM-psYC|%5y0B#rXJiv9dsT^ z$<|7*>-Jc*$lwmNDrYQ?{&Nj0kDnYk9i|pAC`eKMoQ6;@i zG7`j}ocx^HrN)JE&mP03&Q;)BW0&cV>TWCZ17R0VxIU(S!}~0EW^)Z#gS3lFp1S>b z@v2AoJwje=Ia58_`{Bbw1OYLwqY_$0tU+~f*oakUo(jKt%eVAu&xL6d*%|Ybw)|SR zgMXHX8Qo#%t2x~r*xRe0Ar}4Ynei0Ax*&B4?QN;6+lWg~_Q%eTd#b7iL*%KAU3AB} zutA}&nMnz~T}~fdTm5tKv@r>)N4*EYhFV8+%xWdeMkr~kl#h>sMR#8KBgR`!+@;g` zqBVzTba3vk3-$mwk)4^2I?`UDgHDNWT>t)WRa3qYl>)2Tlp&b^^>W_SJU zFu?(8$-4TxTx{IskhYc<$Cf=DPm$)>iA%nhpq0Z z9Oo&+Bx>SM;_=dSo$m|b6I+b-_dgtPMAl|rR8m?x-S}BXbHLG@9WwU=^A=`#Z2K?& zffo4=WB#L~<|q?@28Z|U+eeb)eaI8q_pB%>nv@@c{i6A!a)|fmo%U@kBkd{7R=`Xf^w>vg{|5#-+zO3!2O?V`5@$2Wo#zOmG(!6)C+lRWWIK z^Wx6^`_*~&Bcl!LWd=GGq$`0?$xxR#6FXD07uknoBiX~N_-Bo!^-na1n^TVWl>25& z<#TA)dD72hpg?DEzZS9^d~VkMCj$FVZd1((C0sLA3t7gpi8k60+Wd? zW2+grE8iSkKWUnKOeL`b5MoGJXgzT$#!reEvwnZd@blgT?fk6Qc~?`l{6MgCIt5QS zHWaiEwY-gyZPOo$Vf1uMlCZPO)XK4bpPUDIw8?(=Stlo_yU~i$oet-3$gKHy$J#e< z+ElM~b>yRSttt4>PC7cH*@3CaNqtm%^NWnfpypmF6kDCIvx}nTh%D0@(Kk=6ZJ;^U zyq=I3nF;LA101!jwtr*t^+=69lhZeADcZosLU#F4%}R`ohnJC*?gj6Uj8MbOKdKMvJ=JSp~2%uT)i6TM4=J zC#4nf2&rZCA1R$!9<%a1Wwvrl=tg|n^7}@x2VP>SdzxSGAA>!JcJYkFz^mspC)v$xuuaCP7*X?l(^S(p&HnH_=78;Rf zZho*>WIcTNJXFEM2TofCLeM>W^e7F$xVX5ue^}Tt5>rsES-(_gcJG^d(c-vJWo2c( za<)}>Q3J5IpyC>t0ghjd62v2K@3B3^^l0mqsdWL(e!Qt4!n&&f8t$(ra7rDQvY}n8 zWxA^|v#?x5u3gzI2C3|@pPygeS^{xHZ%#!`lq^F8J8!Ek1^WF&$5`$>m~NN|jp^d-Sc>V?0~+Sf`%|2z-AUmyN->X*A`FNEMm5jY4E7{|{ z)N#H_OGn2R8X6ilys)?^gOkjVwc5d%BYF99ASzvGAw905p`-uJn>U-q^msz`aaWE@ zjxrL|0096i?am8Bab-}Qu5Q~g z8f%sCWkFDl#t*f|B+-a>e;>eu{i6NNzA9STnF2^nWbJsz>GFj5i>5W#HmUMb@_ZgLp^WKjx>6vUIwys~ZLBR{Gi>Y~QYk~E5YKDzV(cDG9KMX(YFDNK@ z1x7uI%v%LuQ1!qo|IKu5R^0kvkl;Oqn@Soa)>{sBXGUYZVDgPqQ)wV-0m~1DURkBKMo5;c?q@V z+>BWQP|tG<0q@GR*jFAN`yXC_c2$6ogXnf3PqAef)}*`aG}l~S)+ zb9@QFdq5wz;JExaxbkd@fIKyMWs%TiO%ku-nvbg{KVS2A_rBAX5&A|(BMXuC!;OB> zXwO5sV%xBT5u9yzYrbPzvQD9f=+e&%n|AD&t0?mE@!@x!d@B>be2E7SE{F5ijOumY-XY4KqVbMrabgFmaB@8> zf+gwaDUC~nv4}kNwZ5B=xQJ;ARB3Och9dEi6N)9B+?jj0wAgXJ(<^%wMMXu);5BK> z9P+Grpsl9H4zT?+B7&X-^`t%r5xnn5nYYci5~>Wj$J+HsxdxT~tV$@hLLRSQz4{o# zy&`^5bYYNr=*x>z?Pvq8-tYNpj+r=oSW!{YaL0rv zC{)dY7OKu?mx5m83Y)<%hca$abGq*!Z)yu!ezi_>bNg;zJN_-rb2+IoO;=?8{6X0&#yj&T78oszcHyAcp(AOv+J5bX)5ph?!K8*n z$_xA>Y-abQ;^N|5Kf3^gy?$*}bzCKp1?)4&lFEyfnfM%Orq@;A%yaX{#SF#`CY^cj z?%caqg&xlMLN4lwWUMr;*ro!bI>wB)c!RZPkpu<2i0vixgx5AT+Yg^M65qz7JQhq^ z(+#}8W@ktT9lLR2?<*(*Wd2cqXBvb9B&DSl@i{`^3gkbn#O~bRj+ezu+#@buWDyW( zjiDw|K#OBEg*coqR{B@+`_c>Cn4O&jp}6`x_vzE8o5ijuE3;r%PLE8O?xRuw+}ggE zM7WH}0@pKU?Tx(HoW9?s)5n3_Q+TgtAGvB#PCqu0dR8BLhkxLrkORl@-H!T|41Hpx z5?ekU5G*xlS#Qgp$9V~XC2$_lf41*RS^QVEGJJYuFThYrYpTLBfd%b5^B4U-gbAY zQk8%Y^Z}y)uynaj1R=?E{^~}Y9@o*)q4kg#Pg_|m1h3AII!<+vAR->|C1eF(xATfy zKqDs>$SB=o+2ts@Sd|7p(ei%~miJOjFClGDfj2piW$Q%h;ECgxpj52dv*L-In z^)K8Py|B}lQU0y_czS(SS6cLZRem#^w(1HbbN1^2u78xCUu@=wn>))0VMR3dr$;FA z3%Dw=<>`QGkv%_MIW;n$l9C$E0}C&2V9=HtW`d(I$IiZa$-&gwwIs5|bUWd<`k4Ae zc5S2&6Z56VC(L->1K4WvM&AwYN#*mEp(bz0tUU3RJ0;yPhwWP>@N-{6T$F#5h-+1r zMLWys(-Z?=CI&&fr-OsW^WSdoPoqRRlUu(h82Tv0}T^S`sJ;hw+vEmikCFGTQR?0?FG+_~rJijLv4VS1QCU^EEg* zkMpEHWWPIR{~s~Ug5K`^sWDOP&t-o1k-m-TC`;)1;??Ic96S>pwjE{tLG@-G!n1WynI z&A1OP8a@FU{nx&`ZNTky+~cb|ICXpDAh)QfPM6!1tTReNEI1qwV1DbH>$f9>ot12C zY$Ec;aHbk!Bz8P}{8(Sy<)GP->{A2kR*u(eHWHwgkN~|!(^xwmdV2?C7}bT%w4yLo z16g@2c8j=7pGfKgV&kx+)P$erO#Mg3vOduKn21-;IKr^8r3D!Gm3*`)DH$hRO^YIP z%;fw)v29$O2Fa@rH$)#XQllmbE~p5TyD8GgDzK zsj=rQyiJri@LP7U1ArHd72mycM-r_U7It>YFkX|x=wsT{iOi9gX6K&vdjecq_O)E3 zH1X{mX>iE3?D_O$6LCyIM~4fYH%ja_{KC7zDGdon78VxC^XETU(akwtDfVzDGgmBH z9ft06IQG2R5C8B0;bs62UP_93#_Qkn^SO-|4ZPUMBo9SmFl%+T%`AE{Gp*o!Eu{9&@s}lZ)XGGEWfs$C|QpVG&6VtFf(vI0KnkIlDnMh=@)fe$M}#i z{rD}~D(H0G4&m>4g;qHM1?XeFtN3YSrE5mV*llgge`=r_rEB z0;c%`oo9$HqLUdubMF?awc5L-V9|zaH3i#9z#B&<`SP*dyLVsQkXLmsQ;D^0TLy}=EZKkuI8^Oi z&bO~$Gw1m0hj@+s0bb*8H15;LTBfN<17>H(_7M+^UA4O-wF+HSf;rT0Q2s+SR7E?9 zm%_JqtKD<&H@(ghZBI1UZ{2&4RA7?BMwkyCJc~e-Fv`nCV?;(>9n3L`XTw>n3ok_$ zZJZQ_;3A67h)ZE#jIU?|XGzXiw>$1=NzPbYUa>R$UR}f#Z+LUQ^jxG}HCtJlPj~ut zqbA@U`gUP*(*z`k`hw_U5JSsuG1)NQdbjWS7VZ4^ml+3XlF?ji9iGT>n@_)Z@ggoC z2{ri_lsQRs5BnF;cCdRHX%hOPQ)LYyGogHvy2PQ&d4K_?3W{Oc7?}P173Nz1HH5knQkt$1KNHLvnN6Cd9 zA{W`(LWA?)1Q*S5^{#m&Z>Gss_GNXRDN|2gy@`R_|9y03qBnaAz7wP3wZrj}Rk4dQv9&rLwt&UOK) z-w?laS-^O}#LJ9%r9s5#Mcvdc`7(~Fw(Fq#+}|S(wXW5@4mJ>l%u=ubkZ^@f>uM zD?37iYh$u=O6(pI{-UHyN=iO}P%navN&9PdHW;5(jgxf@jk7;mQPb?)RGpYvBIV8X z3l2J`(NSfgdRo3yIzfPZi;5MATRM{q&{(J$t@H;WAqC&l_x$v+{I)3 zSX>Y!=*-yNAhr+KUk9t z9@v-mMuzfD=?;o4>BR*k-y3ux!xFOe5PeQeR)+xNCrH06Vzv+`02*?l?Z*-pV)D zG1KB?t||Orj~PWYK79D_3LLEg+Y7tnUudL)Rr!&PSASL+^K?-Ns^7-OIM?6)JMsVt zKdND4OLjSM79NP+=+z}Q29ph7*kQx=V{J{b!CxC34RJ)&_@eK!LJ4C7HOh{cBMTDT zG@M!II33r%tM%GWu_YCCd(^)PRriyKg&I0vl5EzAt!mBjtMC7$zVNvyBLm0#fh00t z%iBL<(`ZNT*J1CMWF2Yvlqou}f;M>GXP$-mc_~~CN1`p9kooWAtEUQp(EZS}`$~SI zEiowacyFy4h+G((k1L6mAnQQSC51+V4yRn;#Kn;wt`{#}Siaqcau!4oj+yA$ z)%f-(Cn%L!Fs>QwD$IXP34OH0rmvarkRsdGHz+`8og}QwobAY-2`&7!A*J_sXwER8 z{07r7U-}ZtKaKNc9HyUyjo~`A15#{GnLUonkLhY!cz|$5ZRRZ$WQZS@r4Md8HVgYq zp<$>w(@~J|?8Nn3%BfoFS31!SuX;p#+`NGc{S`G^y^?(<&TC6(yI-xr**ZZ=l*#+Y z2S!GYqi0|5Mk%F5>XQK}rRP8T1;~$AcI_bkXIkNGn9TIsU`NAfA;((Fv9FUy*88n? zbkd8sWL8_PYUm8b+21&m^BURC)T7WGfouR4$5pt54e^IHK+QfPn-=o1LX3kAjFZH_v-4&swG#d%gC8FsI#`i- z^0f;V$YH@_`<5*v_ES?X*XNN^Ru1fJi??T7Ph$~tAcAcEz8rd|2F;WHnuw%_N0E7J zy14B!I`cAc2WdOZce{*m)>suu%FBm9hZK+{zFCsZlBSXdDP7V7#nsp2D;A90s4>HE z=+L2^G&JA$EZ3t8OlE8SPmlLaez~w?*mCpFNJ2)754kxL9H3vK#Vv z1`}4^&7OPe)eEgg(XA=k&^)ig;QZA^N>b8#SX5q4F3Twt`jrKRfBo9E@gRvci=U7Z zv~4^$Zri4UqrQjR3ROsg{&Zc0fZok+vYC^z_)jO&hxA5ry(`epvpYF*xBAKZ;H2wXZO+FRaST9o-2&1Z(V;o&!+ED)1k9KF$>V>v@Pf1eocd% zW>ha8B_h}1q06P6_sY4D6tIpix=;32h6Nh|Svpvm0RgCdZ`r`)_RVHd9Gm>QPsFBo zBb68AX%OvWFosPP*-Z7z^1(D2qTU6j)s)!%8PK?`;0a7Zd)`%cW>unAsu$>>=G5gL zx_h2D`y$749-eT^Uk^Ww!GWF#+i3o08&6o@hf(&zg6-jdb1*X^Yp=b{@`63g&#rMJ zivYmt1svkA&vEC010#*q zxQwgz(ASdWl*Q(Icjdp!My?9ut_W0u+qgDi!SnsCbzFQAqUiq&yZ`v|;sV#H!;;l8 z`=pPOR`L&}rJD%hGf+z3d9HdS%?5pqlTY&UBI2}*P(jsa+7)r70GrpQ96-36e-PcB zS!Hl=c~k0;D$cOIQW({Gw)QXUrvO)^8xM9r)K_Wvont*$SLEJ3M&|J+BNt90(j26e z;6Hmo4pG+G#z03G=gf1~Z}oD2mcy7voZ1G`$@Bj92D8KixU;AHWK~#Z@D-VT2Ae|X zknb8C-;n;AaI-{L@bY}QFB=DVp*_K5|5d9PmK3bV@xTB6TQ1vt3msP=Mc1=4vD*g` z*Ji-eimr{?v~xgZuS5IG;A{e!_HN`}Yr_Ap&1L5dU^P_T;kZ%=9}*m_$KrC5a?w)@ z3J8d)wt>eg9(Ufhd~%!FxFC$;_r=;<5><}d!*eM+R##DBt(N~TYu@3;jT;fO^JADD zND3YU8k&5j2Z_x5RMmhlGZlDMq^bXfn0n(Tp4eBfS}g1?Aa8O+-G1MhRmv(8tXANI zmP5(>!Mh)riQSKkf?%K0;2UZkgU+5v+WFkoQFI&cZ971k`%|lX;=gy?-@%lX*a?$A zc-^P#hdx7r;?3^#E9H@g<#}Y*_Q%IfwEOoD^Deppu92o^m1;X&v#+4sPC{7=Arn(= zRgUApz<99y(_+P^`%<F1qV_Fr&6g_($N|Bw-P)^Sa;2{tT=tv&NTKb^~r56 zxZ5{pwI-C$EVxtRiMmUKeYS-)q{lsJNuWHojQ>W3dS-zbMGIZ6VRXk8(Un#}W1-1F zsTe|i0>%&2W^@c~$Debny(xJy!U{uo_7(-#t*u%8vRkr~wey=_xEDE2=!9EbfxpDM zBB{=0dho^Uc&FLSQ^VLmK^W}H8JIQ^pPBBZQuGXva#cXs(`E9>427wQ)OO<>$WZxn z%kYhGc<4ROx9}w0_+=x*Fr@X;KRy4}_E%mIHDO&q@-+CA%ok3hfk;|b;jSRRap@Lu zzzR(2w50lsV@!_DWSX}sL1Q`yav#lW(r}UF=cK9kpJ^FY*(&|*XyzTini>dmfq-2k zOaanJ+MEcx#@GoQ2UZB=!SLt|wbjDtSw(uBpoV0acH($RTU)=;X477ZX=zb&9!`ie z&$EaWb=QDyV({V4CVuT;;Fby;lUM|A-erfN=xA=w`D$IVeJkhA5snIi_O%QI3Q|x~ z%C#{7BXCIQc?<8CzyD5hvXtRMX>2?QIR0VKKan;PX zA2~w0lI+Gi)X^+XsdkKlGKJG)Cw6b!KAww^=~>}sAf=mMQ&YQib#?J!&Gzj;>vS>o z>;?t~{vjcA?X%g4-uqa5x5X zhpW{2b3?(C#BAtTUCq{6!mAWgFbor~DXo^V5$OHh-kne`p zRW7DddV6~%;hWaVwGjt#`FQQbgW}@iy<69tbQR?Cnl?&?ownk_ZtMZ4z+1hOT14tS z0%o_?uvJdIU(gW$ZDcv%}I zOCST$+cIXaPD{&~=5OP;VLQF`!=B)%G(LM(pac7aZWe+_VWb18zjVH7>f1)Tu=FIEB$qM(H zeIf0%b0S^@;dLZ?TzCia5(W;&8mj#ve3A-6bBbm)u#uL{K4kU2x8;lhxT*IB@{(}1 z_C-2&`FSpn=*m&Dkd$4PRz$H{Lm+%I@7fudd~+kwUJNmJMEjF;1;MJ;+d!uTL+T{j zjcxi$&FwcoN%$k)j6!;zW+1Qz?)>#-Q@|kwb}m(@($Aq2Xqo!I#+$subXwoQz<}z= z4cKFkiHUW`HjBHt6~K;jvrQCQV%QO`<6K-0^Oc{;87Y*y@9B%TlNdrWe}f}yt>o}Q z76n7+3mJBVbr|&|rF}%Oh7pFd|-+n~$*J|Q?CTW>$qaM$+iX7OlpC?HjB zX=$kusU-3TL!qNBrpm~R2`>n5RhI_rX#}{5SXrBgX}J`w+y>T#X(YWp+`8C=1q=rv z8sfAp`=`))C5O>My6H%J?`%d*e)Y}caF&#P`jmIk+F_`Hl@IpV`_o_9qTODij7GlZ zhQVhvy#qC$WF~SM9psowhwu4A#B&Aq>?*Y1o8X}etT(m`d$od2v?*rD7~e7e$?gSf zOnLV=)g567_(*EzXXE&r|58ucAXkOS* zk_uht63JnOzJ>aR?{N^=;qlg4of1JV#xgj-h?LnVsnHCW~{ap)AN zff~zUK=f;D_XS-r0Vv+&bcG9C9Gwd-BuRRQ*6n@WpFtrZNzi>s{vpuBtOs#L3Au(m zQT^BRk@m=`Ng*N4NHI?x>`h{Jw@3S-Y6eEeRB|#Ed#w!fB{|rEnbddl{0iU`fH}#? zEzbdq14O@^JCP8Tk&nWJ98=+riRoTdcr72D z3a!DAawrQ#6<9hR4ZLB~`uiFftb)l?2R_z`KF{D0Nu7c_f_10hbA}4ZBS!##EZ*5eyqr-6E$l#qT|$HAt9!*=OCuC z!1OPX&~IjhLa~ozk45+MDp|8JQIM`zz8xKNTA?*s(!iCyWbXtDvl{viub@WexjTX! zYvtKVk)u&KeWa6Hke(Pt5+g>F(J&(YR+8{Tkg_g$Wtfc$hU4u~TH1uwxy#PZ^Y{~# z9AHTbbf28Vz;B=luMS+r3hC3X+G8_T2p-&u=#}ZXd=JaH@y?xac+UtzMDr#+*A1_v zc9CsAw4kKmb9jY*KPoUYjA1VW7dQ`|3cXAkJYX{ih9f=OPct)l1zcy7m(bOL8>jr! z_Ur~s(>jm7Eq_IB3f?YvAj1!ZMZ2q#xC6o?9Su2)l8fLoZ_koNpIn}e>VH2dgU0WF zp4oxQ|K|(KLVg|m^XH#74EZ;&e}9B3_U8*?CHZys&!43K{m<9Ie}6>phWzsS^XLCw z=Ku7fe?RfyzrXe0deMLS#Q)kSdDXfNQkN8Nqd%j-=(%(gX5`(To}Q>mJ{X*nK~x+e zBOgZ>?x&Z5J3%}E=;6v{2f83J0AwD2c>2Mb0V?J1QxnB(OHr7xo!m(yewrB9`~Tv( z0>o0(PgiQf;W;v?*?(iq9~sX>{qGriLN47ra^T6OeZo#}(2i?~EdkP=2MJtWvrnB_tI47nxR%u&gb$ zn7zX}N^??jZ>bl}xqUucXr5lDuCQ6BuI07q!#WxTW=H+A=TtXvd2_6}ZGG#3*CEAE zPxo%UzD36C?1^&$8`ZtcS%%~qmX}tZB^9fW8z(k&CX5&84hRphC&Vo##3hcjFAohT zf(1h&7K_|D{{8#+pFLOX?cXV@s7N7m0(?lIn(2d!LL0mA;(64W4)kLT;5cc3fJfqB zl1~W>3y*pvzil;N{rU9~)DvmQJ?QaQV5rAV%mI|Z022VDLj!KB8F{f0()`P7{N=2$ zh{#xxyjqHCLVWyFNYPc`0KF*Kec~I>Of7GDa>+Jj7H;Nv^dfZjDH<5WlI3p!nU!w_ z(?vE_9fec1LW%H5=K`}x&!CqOkCOimWrfdu!S2F%DQNdFx-*uiSIH*_KvJCfai)<} z9`Q(uQcG_r!!vk)g;Vit+bwY2s!dHz_M%r|Hd8`jrUsjye| z5nrFJ-Cv{9hJGK(S~Wea$%*>X0d7D%64ord#zYAEv`I+INnjrrMynwp*tXAead0T( zYAij4A3b@(x0Llpjl&c?x^c(w!wVX~&4)ynR@u3@t2o*)e@6A&M z(5bVbUjPaf|* zc^|vCG^bA z!`#Xx?~yduXcjoV{Wn@WJ+vc#{Y`qLN0mL}VC4NXR1Hbv+nL92w94~Es4-!(D~0_cZ4|fBw9mTT@HRJm;5@fq~*&HWPZSSr*}s8HAcs=lDYz17+c#$ z`mR`cd6l6#B(OE^%CCTWTs76$2qaD>20on?*hlXP_7wrRIE=QOJG4H+E9p>!@p=;g z`Sf_cWu4r#&3(10|zz*YHE_c0A{&2`pyVt{7XS zh`nj#TR{{iMMOjh?%>8^1y$<*SU&iq|4#GSi+gN$u2uN+L<=a8kfJg)A{Vj&>&Uqt zv=|$&>tRWfF^a8@oh2Uu6arncb?hxX8OPwEwj0zcFDt{dhYB7%cvfThR*mDvJB`)v zA1*L5GCGJDGzTG3UG|s?@2)^20+;>K(pM9e3JwZ?2wF(YN-8R>{NZCv{oV2HXOFU~ zCaK`)kk2^6NGECB@u3$ujPV)rsX&e4%RMB(gaG;wHrf_*e!O1H;xw~mDV{VOk@x_>zMiDN_M)w6h+%X+My zpS<6{guIcuxF)o#dr9D>_{EFyh)<2e>kxlfh&NZrHpT)Rn5LBO*LzTZu>HG;FQZ-P zHOCI!9zlo(K8S}UWTY(w4>`~q-ZXOu$kppoQbj+?xO>z*PbAqDRRYPK;N2m)%I1(c_1#j|TO}-VftA8Z3BZ zRs65XIQi&3G=89m!wa21cgYK%6L}FuoGrXa5REVJ#1ZXf4BT>Kkxz9j6wLRB16PzD z;9o=f)zJs2l;JL9fo~>lriVnbxQ()pA5s~ed;RuvCk6V$$?)(&1q4o32;Wmvz-n@6 zddicZK!WChy#h9`$VngDVwCn!@bBvsGsq=M3JN6$_-~WP4;B~+FmctSa&m^P05-zQ z?4w8U3B-BXn;KYHbyiN_(qlCekShDjBNKQidi3ALnXa_D@$dLRfLSr>3B>kPq(o zUzc4z(wU##FKU>fu9~cN{kQ!hreoOwSPHJ6iU(JeL)A{rFHTEKdr{1HXJjKxhu^+^ z+t00wn8tBj4Dpngl>D;P0bq|V{-5P07o&V~ZY~7AYm;%(xQF%SUs^!(3eO_jY}K;W z9PS0Ot=zf1Ve8%!mY26knc8;bUBR@sqkzJ z%F+_)X2tBG60!$LuS#xCN2DG-ypw@pjAtYV?GJKt;_*FX?2Yr#G-tW=K61iC$N$_QqSX$J`Gu;+w0V z;`CL7mspE5+yg4v%#CX&q^9oiE;~$8MR3FbHu+!_#kYEmG=)Y4o}9ER~tO?Qsqc`#oxmoPrSCsvM!F~sJ| zZ#fLX*7Y`!#2VD4Xk@|q*!FHeqn4-Hy&den+~fDs|+v^6@-gK3bnSp5U) zcy3c*P>{*^z@zLnuaJQNUvF2});2e#!9nD0Dc}Mj0fcAxlZt&|+*^KDRBERMrNG zEM?0Y;W+p8J@mY|Up$}to>$L{`@HZ;b^QN+=lAiPtwe@1d#{Q?vvPSo!k@N7-5cU=&cLB^{ z?-oA~L7+m+PZ|3P?&?Ys1df5`x}9HW$qt+$9Q4#klrtYYYBC(m@KO0KP4e=aTu8*(ZwsmF(0s8cmx_O|Gf?9EEPTZ{8?tTZp;aqa z9_!z=40I7{2lZL#&kuCAgKHCyV#KPa-Fo5wuH+$TX<(z(+0%G?lfc^?_w?0={`|dE z=RT32vqnGVjIQs9L~C#*BxJvDNIBq3EMV2BtCOQ+3%dN!v9wbS#;~P|m*#0|zXSl| zfSXBtl^OiY_RyS!#%(*o)oyIPWB~vUHN(9`u!7{URFGWc-D@2>XT?2(3Ar4qC8s>z zHs(P_U6$z9QBdtoEa~m-y@o>mIgT*7ke$0QUH=_;S5TJ4C+8yMS@QMyw7m>p1*Rd+ z1AY!FZS(Z@Mws3m&hs#F(%hfDNMeX5xLs5a$w>=&cey5!SQohK>I11-0FYNeWP`+T zdyLO6PJD2pCp{ry8y)W%%?o{j%vSW{6QqUC_FD>afRnhLogM6M_|XdSF{g@rWSEX> z;P@@r8E@|B&dOG|02Y26q#UZAgj{Tvdm{9)362mlsCVDgoz2}kavPP@pihD2GtC$~!39Pi= z40xHOJO@z+q2te=kav`qhiLA?>5s3P#67$YK^y=io?w+TJ@o+~#AgC?r!suf9P5hj zhhj*=Ruc2KsGwleelE(=HCIM-xkIMh6(};2`}!C7=pyyl4LgA8L=HYJV1-pxmNzz< zE|5R68uUOa;dtkSrK6{}4NOupJUCJQ3CJrjF_5e4xC5Utft|I|GjxUjwv#ZcF|I^ub4r#z|*4ovf-l zCP_B?w~d^^3P*Ts75G<_lx&6|t%XK=fCb6KCkDeT)L-V9s{=kSFII{}ar3p|vqx-p zUYSK8?rd$qh%TvuPm8+k-W`BMRC(L%s-ItVeMw#2Ari}?qMQM#&Yjm*#nxnii6SO5 z0i#)XGe)7}2E*GNgRs!{W^`$n7ma}!xyRVp_BLIS|f}JcPEljR1hjg#rP3j@-zdYf1+ampO%`rP2;%%-!=^r zws~oe|HfOsbgg;X(ISt5`y`TCB^W;apJ?b&2~vpa^!%;1;F+wHlF|dys@8Tfyx8h& z|D0vXtp76{$$!8f!K*7I4 zPbZfxhk`vA`Fg9P<3p$tA3Aie29e>2C&&3KXoanq=LX_`JcGui@VMgJ*1i-ltA(X@ z{CcO)M_XHaIef#6B0(gG^_9n+fk%5M(-h+oa&Dc>luA8(Oa>ctpDTWT!IlxOdZc1; z9ell+7zT3bOClm7kTv|bK-)xeUC259QOuc1(GeLJB=|b!B^r&orVfOx@Pb zOHr>6C&4Z;&27l~Mrdfl>IFXABW52scrcz+w&~pO*ooOr?S9`237yIA{)Yi0R)d+b zlC96<$B)%SETQer$wDB#7|HYs6IeDrgR$2~1}BfdPgRK_q-5>aehsgkqW6nr3^_f&U1^l zm6A&K-UgcDVqRW_xMO)J`gpJAd8p!SA2!ZuPLQh#nBIJqbc-zJ~)!QW&@_JJ$|a zfGGntnjq))No>PY={ehB0R@I|5XNj22ex}Gs?IV&@o&=qteqGRU?gqKT@A2x&K!kLLe)3g9U(BH$kFk)sDTtpVg5EsA~E#R53SvRV@nD?X! z>63j8o=H>XgKkha-ZfMwX_yFJa|2(P!N!}b#gID{K3qR)p;0;kXpFA*KY{QHsnT)B z?l^2=!e;0u($z^r25ZW$n>aT1 zGBYr^JqF=2A(iM3Zgg~X7^_#KgDbt*aXig<1wtZaTtt{ac$aRXsDp4p=d|+qtdPuE zo}OG$`8xQj#rjfT{|55{T!V}x07Y4ppa^GgZ<51BtWiRcXWJi|p1By{G>D(ZPL>uw z*ZWIO#P*mVj2s9QGcl1>4$}w36m#c}Hcen*S4l=xd>})=s7WG=@EDOBMkb2E&gcq9>6kHife9Qd>`$L zXt3R@sNal=sl;cE4#ua~(T;m)^Hve%`O*zCNT!GRG$G2ELm&(c4tLuqhb2#}5U`fYPTK-!X0mT%Bx&RX0_N+gbV7l}#9l1tlf0ZGy*@8x0Xe~$1JCce ziw{5oy#h?NKW2Fcf=oI2?+nb@z{18^fRo|Bb1>3j(-0K;J^}D6!ybTa; zk&W_pY%^qRvieyLN9X(1>tIAow1`AI%uH$zG8gz6)MxYM<>gi^mn6nGuRJ)+cVn^r zL#!VekQP=9h!3QrU?x({UE;8bimpTsvk!CDfVSHm~$W3Jwd|Z5y(KW zoUCXsBYCogIZZ;IBbtp#9Ceu6ioj>l2BaR_YLc>N$-M(_#mPl9pN-P{d4udkDR>Fn zr!XQm*AAghbIhVUcHzC)*d3utrMAg(EEEXvpslViFDdZ~=U0eog#B3P7zwK}+Y)TV z{oFmd6J^9wkZHa(-}#LIiPMUokB*KKIkiW*Gr9eTu=I454L|&v|DhJssHAuUbfw6o zf32v7leHmvr3}MG=sE(#x2{|3IOK)Rot=sC22c;7$I2)gJ+yj3x4_Q^vv;#!zX($? z08p9~Uk=JY5gp|ocz|u-2pxtA3rrz-AT3kSM^tXbfcru4M9J*(P*Pq*qfLivpg{fu zG&@u}bCSPj`4}4YGj-_!a zbp~bU5l&qhhqs8E{q9cHMD?^QRlm%gD?7|0JL==YFFE z*5WJpRVI!m*ekN6vaBx$4o5=csTDxump*nYr=)Mzlo_3mE=B0O%);bJ0;k=KXO&X9TW}fn`N~3X~CK z_Mp7x~oAG63wQlACA%W_b5 zPOTLHBe&AIm9tr#Re6-U`^jF+et^-^X{pJZNGmKZ7WFWJ@2@l*hu>5UU)!%QEfaf8oK=(6E0gai%W%t|i&tOK?QR5O_a#M0NP^7{_owDIs zTz5cBU8~u@(Act~AJx}95s^Ruq1|XHI>pZT$61-26zf4o%72$OW^V)vd;R)sZWxb zrq#U>?$l&@vZ>x0PJ{2RBbj`{jI^f&c4^2lZ+9~nUX6A;BdQY1)V6&Y_NJ5gW?oX} z?aio&nUIR303mMy+!rY<)L7y0Mw6qKh4^rUTMr!|V+<%VQllCER|vah;`ubj8x88j zUv2@PyUoKdg9>D9GB+7f-?m#{u=IOX4RL9Dd;z7bf|^!Xb?reN`pibT&b$lskd>=f zzmTb03KuSFY8rA`hYqe@(E2@K!o3&+0b(^G%VX43hP1u7p`@)q!DR$mGtcK3geJVf zJk5Ci;C3wP>v4HdpII0((DMo!a@_aB|2QroVWpSvbBjmu0M^iECBt4k8mqk*Y;h`g zsD$ar9fsUL9?R#94jF!sL{SvW{mjXj!15cRS_7JGWDO9jQg~sqwB#Kce|-7en1b9i zUXi;8Z3NQjrZ15gh9N`Bg7>*14Fp|`g^qMOpGB}B4k0qX5eD0P@Qc$5aL)FA=;Y#K zA`KQ%1kob(EeX(bI%PJ`O35+}vv}3+=~`&r0g3O2^S~}xxw(jNXURW_A{Hfnhwf3S zxA?^%8c;J|j12Y5-hKPBioBkHzDGROkaMzB-~yEI-#Sv>j5Bb1-kkoyeBD0j4cFRgczQ(QVKQ3^#{bC#o{g5uD6$=E4f6l?QTX zY*C*(m&P~*M-SL4{$!4lQ_*95L{wfAYI8sc{;&zEo$~?TrEVOqcM{8=9H6&n;`a^T zn1belJe(ndvC2?Yg_@}1U6Ovp#<%x1#(l=SRkD2+$WkuOd~p-(4>`=p#pZOp0{H)f zYBK7)?cik;@Ub!Upjt&RvnOw2D5VHm%o8VMh6-nPU=#&V?T3LTQ=Q|rsYgSD{k^>v zUU+%V4M6a8u*VqOhv6HNU=-+SB(e86sQ*r>`fCpBr_2SCWw(Mz2Dussvepy>a$#4DL`Q2D(sZ3 z+}|DoDaq?0i^uPTrJe$I?X(eugYc;CXq(bY(kHW)5l7IWXb_Xa(gJd#q{6!u|?%Yt|mp(IMIhz-9BBUkOFO z_h=p{6-VRx#5Fx`O<(_h`BlAW)f6BbGWU_NM~t}pkGSftkdjKkywm(RDkQLfb|-)u zdpVSgsv#Z$uXQ#^#g$Z`UQf9I%#r?8rk3n(sxX`MbePbW3dB&*P&uqj!AvEr%o>4Z zcH?k_Yn#Z8@NnC>ZWlQHSHUp5pq-Ju3O}=O9(4*g`*-e4j{mhcTYW%Autjd zq@u{PuCA&{_kNBkv`)MF)+QLckuY&smi{u)ACmVJkilJoNX?213%9`A0Q4j8qGJI_ zqr}tT>?w{&5x}_x;%2ye_fp{5K+>0CQw1W3mtwvEddS7;YVVVVgOxuhvv#Y zefDg$s6-4New0j*T@CjoR(&dEQFin7-HR1fth=cf{W=cwa8X~qwN?Woja=rv4n+s} zMT*L1gXRV2UJGAwcl<;}>&l-Accb77m5h0fol}q@!Rz2bP+Lx}m>E_=%;aE@4<ufXi2)`VK~szLk(i?CN8Rf7}I&2vdrIiceQHBa9E2 zm~bj&F5_4L5s}0hqy(4g(j7`xdT`Nssy72LJbG&svXwk{Z5lu!p#e#nKNpi&N7!Ho z<6#qk3p4^}Fj14kJO(eVva^aa=?^p)gu^nEDpz4ZYaBumA3X{H-@hOkUzYyVBH~ z&_(fH-`@P+rJr1n|7-()TfKkTPFhnx>)+c}|6Th3=S%N0$YrjEae~fz`~F3@x>a|l KPO|o|=l=;%ID1&HZLq7=wY;9wD7a9DtCjF$K|B-DCVAa(n`3EvN8YADOP!lk*Q@p`DHEl z)kqG#?8}#T4KQ)8A8An9ILHLOVNY-i3QVg!-HIhcSv#Vi825`Mzui zOEo_gs>aPX`+N>z61(lPs@p3%b)MFJD(SZ#8FvVNZe0j zh$u`_Iu{?Zh&v*-Dw#7$^*JO7#<=*#HEk146D0NM>6<$!*cvK9L43aim>9TR(~b{t z1^wR}U^FnBtm%kh-!t#Qy+&)ZvLnuCSjoZX??-`h`}Zxc>Ezk)1eoHHd69~uI?pgxzEk%VsGzZot0v0wz< z>6xwz1hL(R{~>thin@bAG*?+gDYQ*w1VSRru0JEjV2I3BO2<{g(ca#|!4;BlwlHU-+HTycbc@t$H>kCG({8mUwOr;F@9TZM! zX68alPT2EqoCf&^!837>Wo6OJ5CY6X{U6a2F^#!rJL!og*0Q2*qRY!09`|a^F`>Nj zv>-P7Hl~al-Suovi0?FE<9VZtqhux_(>)~h(5C?qM_lUc_fh$NMDNB4MKg%BmT@k| z`@l)E%7fk-3y%gesl5E--{#*sQby@d5{_Wf>F^=%IF3-YWbR%rr&s_K(AUcx?wqR2r-1$>ZzBNp>Lwa8Qnt?oATRuZg936Sx_CZ zf({ta?DIMtTlXIZ_^S^1qd{gH{gkryB6mkqQg=VV(+{V<`1pCdtBSRumoFo<&s-`z z$nA-css2$z8jgU746P+ECL?*ouwyKc;zdL?f!N%{Xe#HWJR zTCpoWrtwit7L};pN>A!|Np-lhPlBqk@$-`wbe{d#W+$1EPVbVA77>U?`lRUz+VK^N zf2tciJAM0O$^Mj!Hu4=jh=Xm(>p_9z@EMD^I~3J}SbO|6R(0{h_ft2l)b2OGVwb!h z7Tjp9=5s;{_g>B8O;WN4Ta?$`?wYE&b3{KG@!~_nTQn$kcsV6vJHj46@t5l`Y;jfj zc(OTv4;AI1-=m}uLrMNUv&N(F@`V!D(Wqp;eLQFd0dnq*b@7FX`nVo@!g{{{JPwZv zQW(TDjl-jZS23;d8v{H519dLR<{BoNs3zk|`l? zK+4I`{il9r2w-u4gJEI(UdjP6s8<$Nz^`+#N>mQmtlim92pjea^U8hNw$`=)@;rp^ zxOBm=j(+*Pdrbr!E+ZXQInb%=l$19Vz>?}lc5xYm;O8rPtJA0llr8i5`6TC>8w^5_ z7=xRGgfjc#&gLS;ATVeE)80f0m2lmI3_}qrd5Ghqp-4iSKEX(#zAz6e5I-+3k9_f> z&f;kc#ac8nq|{8|y0sjSGU=L~3@g5UVyt3eLY^hiyRdl2T*VPjh|4~i9Q_j597YMt9O@x4ogX92t{dlwq zt^Pg*8lZR=t?1*6Bsgf_12x3^tL{$i*dbPiT4MqguT@YczqvqQWJ74L|MN6gTk!Eq zF2Ziqd=3Os7wYBx=>%9elybyLuuPm!fd3IJz>yUIAY)IiP<@M5r)3A8jS6CXPc}I-REw#srNbQm!k(K;< z^718PWr#4?eP?UPUvFO~@;?xbwvY?Y=Nk4(YQ*dw!0fZ#!sh7s5!o-q0E5G0U$#Uf z1Vk~M&;ggNE(-*|@9TLsP%k~TCw5Eyql{jm##b;STE{ZX%|wDzVMvV4JFl~(Zkkgf zY}6DNtX9}GoG)W1!2?oK{l>Pp#-(joaQizBBm^23y{Y<>1##&bh=v`pvm-!nibEd@ z*c<)$;#)v1if0ZaCNe|pv~?1PbrAqm|7O>ZFC(9sZx#tU>{BPA`sMct4>)zeOZRKb zy0oIugkNV)<}A=!_7#;x8JiU`oF7)r&wd8@t96gj zL%mj_Oh?vdmf~=7=$>hCqq?dN&IP6;`&->z=CVVIhX~x0bgp)wOu``^7|z_O*WP-b z3pEG?D~I%{2*pUGZX+@j`z^AcFAipEmf;nyFKV?#04d0cmi;bH#QX_JqZH@uuDqA~ z0n1NeOK11yjQVNtgDq!b6uBX%tUaT2Dnqg?TF4~EXHP8B*7(CHUhg}yJ5a>iji%$j zh|;-`3A3mFYvYx3S3>x7!f0~1vl$d6D*?6NJrM7mC?%g!!3L_p7ie0_rXD&S1(`1B zuT!nE)wIhGDzG)_?@#2fY*%a$Aa%J9#b@WZz#XyrUtjuOG&xIWfYrh8NSuWpEFM5i z5voacadsXig3zvUx_@*Ps`=9Q2|VuUXeg#X!-oy2&&5X-o=sI`!Y-&i+Bi4C?eeJ> z6?!&2@Vn5B7fB8{1!+eU3u2KAw*@v7wY?C$WccL~viD1nvUtMDg)ep+#qS|PBg6eZ zjXYLXZ-PJl`1Bfz`dQNTA_Wyvp$01Z-p=UZb#gz#9tAQ#$aantY3P@ifD_g54koD~ zBBa6y6NxRhhzP6y$fJdzK-lvdefV=DL1K+DDlyeShs0VydI>GJR+WHA1* z+6|`)7lO9YU`_nlL}>FL+(m*+ko^tYjqU*5Gi~rnM?%m9K9G@5)uT-L=3AdUoRX%+ zV|{30ZlO>3y?{u<2tXvlp8dn^+2;3+%zj__!3VY1d)CgBaFg?m#X$%(Wxt9|1;`+=#anr! z?o)4}l&$`A`)*TNEfB4@{wWhLW4W?CFFAN)~w{que*u zaCS1{s~ychYk84|3Mr(VwFF1ln{)HRl$*`nx#@A`zUWYtkH6LrtqopRQFJ`W`5U7GE)rCr zP9p|6ZP&#KeL{dHnBduXsdk&x{CvhFM$)*7tGabmQ=qJ;jlY`u3V7M`S$r}Zz zgTl!PHD4cs3bf%xHpA7ZdI47=n5#8N+#yC&?4ZtA4l$AlCr8HzViIouc|_F-Xoz8& zIni)5PAH%eECr!?6E_?e)5Qiu&|0`LQR23)r1nSPUE(Qv?GJIFfD4!?Btq)|0;*_( zRWY(e;E?{m4`TCj>W;{N9)ff{OpcH+r&SpOBG1GiHEr;i9SRUYg=dQ}SzVAoVwAv4 z0TP#?UI&O!ff+1$h6~vx@WS(7;Q{ig{WEBAT4Ng;vYbE#5pqk7{h1sEQh{r$?-QAH zRP|sBWbhWsLvyq-bh$C0-dAOJlfc=E2GqhiS>2J16A0c-*4l8g>qDC@%hPP{n%-ldw229od-aIjrL|n)k0gCOxq*Vu%*%>|!6!!k76eypd6KvRZ zcwer-`heHTA0p-!M}jh1;klW?h*af3E^=6|3rg|=V9zJ8J@JjckNM-~Yd{b~IyiY& zP+XOP{VR*y7_zwmJq1i}%eMXn1Iie>UFgHY1p2@KFRXehQu`m#9S$zQ?H?jP|Nnci z+1Q;c?`sRBo}t%vY=OgoxH^He1QW_wf>+!^s)>qx1O<2l?LZ-O;$gr|3Or!V7tpuM z5QEHktBAoA1%!WR6ds8LN<)y!KCJNp8+K}4uzehO`#EpMWT>RgAt-iPhDxH4<`z_dQM3>Z5P=^@As7oHo$KZXOc1I$caSmgho(W0>azZnk) z0`LAu!}XC#tuap-%3=1WLj;g$hz;^XS2zV_R9vb)8DnPxw}&xXRXxth-;1Ag-OST zS!GW(V}Vt<_iwe~@b0GC+MAL$UTlMU+BA-t{9E%(V=*fA{Dse|a&_e8`X1|BR?zPA zNPNLi|KePSI4*n`HzJj}rwXgZGm6e}hDm>MshiNWk_|ock1u;!_##nx^s&6KjQ6Xhp=Di+!FRS7MI)MRFFxNdJo53L@Ed5{ z9*!qsV=8aY)qR3Sl(a?pC0TacCUj@42j9&v<-Fx%vpI`bpVl91IW=)L$uIP5ng%2) zrZvjl9Mkd!BE+iWKC1NWd7@AJT(INVPKtb`RM^fA2+Qo{f2B)KI6utWEd03J8Ss-i z=3CiW(c#4#Ch~$p9}o}xbfgAn(_mG3-<;7t^`gyG#`yjut5lPE=u{6;(u(Gn&&N3S)xw2GWdi%Uj6<^m zK5SVXH``aP?%!==qCX~yY)-AW^qJN<4o83Vj*#R+M(Q7JbL1ygokpx>Zsiy1{t&;n9Iu<{n6!3MqKFYGJ(kw!#Ec4GG!V{)PQM z1Z0AXvph4Kr7PbKF>$aS+RIMfG|Fq)xr@fWr?M&|u}l7xZ9}kOKO3LwHN~i|(nzZP zHQpgtK<3QNCnCpnS114HRY+9m&x^{_BcATu!nT=BU4#Y;lhcnRODqcowvGr*;PsYA8RjIE&C1 z+aD;sTZ#5{F-&S5V~h-pFt27UB)ln;FFd_2v-y*=wbR0!v>I2}%bL&=)z4#2|00)w zNoqc3+cvD&!s_|Rbc6^0&LdsInZio-m=%+lyqy;c*lpW=f0jeFlbH>7W7`JCFO3cx zf!%?^vwq3CP4tXexvPy8-ppsuCaW_W@*Jyi*DdGHCL`Hi8}1X_$Ns&!+lr^%huF57 zE;^T_aX5zeYd=Pa8#|8m?3?koU8LK@`35Ch=ozP0i+G`pu}AlTigSxTx5FLm%7*DE zM)PlkT$(ED>gS>LN{&0KS$$N;J6L&65z0X>-ZBY-N>XkY#+Ui;&Le|0RMvfu8(dXs zO0vC9Q-}O1$1Aa^G)p{M-_pmGyV>e{dp>L#Eg^ey*Bpx`Z#ru0mwh&f{crJ%ZYwQR zZk98ccx{`z+zwqAudaan+P^f?=NEIQ4E@@SJ%TbVjM*p;m*29}yZ#_CL_&Pa+E>5b zX7?xI_~3i;#@Oz>1csI;7c##DTO^b6Q%6C?w#S7of<9xX%PF%2(U%`em6utR{BQD? zcPU&WzpdY|FhzMc)Sa2oUGn#XjkXEQ)o3;+-#KpC5{Ba;LPJgdomO;&`|YEs(5UE0!O|! z&}Cm8$G+H^Y_S5Ia zvUNg^S{Mr@ZyvkF9oJ}|(aQg3v%2Jd(XZ8iE!jPNe!M>;l>bkncrXKwu1fZ?q>)*Y z@oCrLYlDn4jV#ksMPXWXmnTG6V@UnWLt@OP#aoNl*~3fx!IlQb9J|6KY>}Bo;V5J* zPT#&1a%=SjtgD;drQvFE*vIPpo+PrXr2m7^;0Mqq=K3M&*Xs*z$44rCbJri}cR#tF zE~XXxvj?KQ3njl`_uCPn_BhqGkv-PO(XN={-VH#356AXDBF}~oX{6j|<4d>&#}bkh z+9tEdx?bbAIb;lE+dOGtRQCsGM|ACz>_hLHa=|Wb)RHYS>1lOsGORlLTuO4=u+d5U z;C@mqv<7hHnXoLMPWH)9|Fyz$ZzFlIjrCL`VnZmZZ^_qqfqN;b)fU))?1HLVu8hjo zGpeSygceD7Ftk2lC@_lMp32TwU+=|hF@747I(2>AeOGw2P0YuVv`B~Y-iaN_@R?%K zkY+P?iy6g3>P3cGo9Sa*8|iY7;m#KM@Pv`Qv(`U6OIXLgBP)-jdu|kJ_m`{x;+5!o27O_Y z($>Gd+f)aw+&NhY$>mK$I9CGEKi{e?`rQcfj3>X`y2p(-AqYfC;b!UHB(A)_K%qR3 z_rf!$!`4yDXqk{}J$WN(BDGk9*rAWL;M8V|M84+F5j`1um;8Rl9GSp{+0l{3E)6aL zViQ-2l1BBMt1suWW*f=)$nnnj{KjELF}vtJPu!rD7{qml29^s%+5Vmi+JqUmw7pA# zu3|;6H>Cr=zB1TuQ-V6GG;t`}y*H+``xt-v@MP)pfXLscT{TxSULh`~+NCcq$J@@* zKD>W6La*nQzl_Fv2Rq+aZmSJw9qYSy^CV~Fk_TfRys~j$R$lrs zP)1@ek5k-oJ+jZ9B5Xo7o;c9|sUv})(oEZ5skzOO+R7OBR@La5b7rG=NNXe`>5F+= zvR7G#972XUur*7WMk;SKioB9Tt7=m$=#amS&ofLq6km5Kofw2g&Yzi@lb$^GGb5t$ zjx73h6SDQnvE1jb5X$V8U(-VHn-AkntP=JCLwshd+n9XX*S>?)-P~-Wjfd6wW8{nJ zL(nmT77!ocb5k7gMs!Uo(KVo<{lQp^LG)J zdBW(@%Qva5Mf)DMHFKfW$GG>k7PNMJv>zGkeV50iyFx++4%bxpMG+jq$71#tlBn4U zSXK`Q*urH)9JvzR6~*j(2t(5p9NYFJ;u~>KQu()xH`SaPKTKgksdXkjChL;&5UlK2G(v zB%lHw*`VQKoT=2hP&l)dDU+1VsMgkIc0ZALB2+7hS@*l9b)JW0e&vG{&tF&>KaK+AKMH*lHo`Uhgq} zO#Q}veyJ7GF<2!(bVI%L&KQCVvHGMt(=g<mX1}{Eme-$f6B`z zZ_a-5M!k;sZx3RSrT0-OCD$N^j+raEPWNm?2B%~|@4+=ShwC;Iho+95zjv(rjt}~v zPO{qN`_^^&^(S6q{z`$ucxL(=;QcB}IL=VxVc1aIPW`2~Wy=$ms%UTI*8`JH%I%_q ztgO-1t!Kw+=ACL>F=|8<2QjY)y@^C#)?7`8d1^wvaL-rr~-q z!$v_ThQ${@NxH@yV2mT)u{>GSWn z=6e|ojU{0ud8Q9&2cx#!`n7$#th8g?LQ3|II8&F2RDT%U3|SVz<*(Ly(wpWBUEwbZ z9^7GrcbvImR<2`Wz93Dvliiu7sUm9O7WK=wPJNZTZ_c0{&E=>!>`GW86_7Ech`sw7 zI#ifNq54cpxleN@EE!TCWtTOpyP2Si#I_6qN(Z`d$^3I-x z$pU@)hfd{lu|2s51nR>==m2{>zM(Q`{UYJkH#gw;Zp*HP%<3oCu-JibChR6_876*m zb_8M0raY7)+TFPnD|wPizOTq4dj>jsONnaGTz@i_A}5I?ST1WlywCT2BTTj;!rZ7O zy*VgR@F37ohfl3Ar{thQBN$qtq7&;?4T#FIk-s9lY4&{jQ*^i*jjju;v!FzF24gC= z6Ml={o1>Dp;%LOVsOH_0=h8cUV7}h|`@LnbVngAdO+m}w2v6pdjI1t&)zI}fmUqTh zjH?S@`s5SnUi@u9V$%dA9#85(6?RTq6qB)Hu!VyCEVlUXr*GogT;D;QPPlKa4wk*X zD3mdm38hWbYx+%z{@JlI=;%BOx_Hc6K10+i!MhmWspl3ZJ}OkEcXpiqvYb$DBk_|Q z2@1opPrqxCG96c=k{h!P%MzanYR8HpN@_S~P4oP`)~|$^%IAwk4Rw60ik)AHKJ7(M z+}zP-Nqdz&W6|F%u#ETX%qk(WTSI6$y{2r`i{YwZ{>NmV!m^4F4eBG)d)pN@&*G*@ z3VfyHv^wIqT(HTwxYBb(xK z_Nbe{<3pUyzH3VV{TF5E)t~fg&rm_s2(K)_pa_Z{~p05uRfrC3@SpId~ zFp4NzH}UN3$(Pn6EPB--u~B#G_V-74EgkESKx`Ko+nzw({iNEI-uKpamCLy%F&ZyZ zhrI+X?%?&w*RoNQ8TN+O(z|)Ji+p^uZrN?7dh|47A$zMX53Q}c&PQ6v$6UV_z5>zk zxq0&3>AidxufNO<=+QIwEAAX}Aw?_Vp6NK(5p)3Ix(A#cwk3)04*o9uORU^|s zi}``l^L4`|+Q4cw1Dfhh3N518@DqS5r9G00iy77&%W*D?|K}E zwU*Mdlh$@NQT9q~OnJF3cY|(~u4$J&Z)+UCq`Aaopnn`IHqPbdcTHbZ{Zjd& z`oZEXFOl<5g@)!fTpo1&-)!=W8>_b7j0`^Q%=eAM0yNUcDnzyT|{)>s_4_3jM=eQ&|O%f#bikq=kjoqIY z7vqA79oS;(tk-tzBCh52=Y&bJYj%_AoiAlt$N0yRRK8*^%GH{$Tsq6TP@@~UElbQ- zyc=rfXpT(>deA$Rq1V3hAz9kjI*M3HZPlz@$}YmjI_6V-A@6vT-p?gy^TAi_5$fT` zStj8Ti!E&_hVa*)of@zQn+sJ?G&xhfxIeGuMtl4dQI8XE5XG;uF!r;g~vOhS84p0zvG>g(jfKx;fj5f{|{9d+zN{coQBGg{^}Ow5h?8e0BB02TU8Q_1xrTS+TCFb6a3@aJx;X#K zITwp#P3E>wV3L7uOuPf=p_Oh_nu|nZ z)w!=f8&HsZa$sIa)i^xdlz3|upSj(H1a4MjuJpjw!4EGHA~yGaF7;4Dr!?r+jDiJZ zPu{M*tUq5O=ZbqqPh_VkJ2ffcU|msIgiR~+WlVclYpnJz?tEogt;f=&Pu5h}PGB}p zh z2oikh*@BoC50#I4wSUBOc3MTJQ*ZvuWIkvHkb=n+dJ7$a|24*3hMjCdPvM`InH+98E+Q0_z88_B z;T>=VcTraN^4#-Yf#woB++3yW_fBxw&LkwRiu0H+Wjk-5rrCCg~{3;{Y{ zRTf|kim1A6$#r~g7=Rmkw_U#w;?#|&SWt$;ZQqGskg!q2VH^(%Nb&Gnm`X`ShI`rQ z9v;8g>BB=3Qri|8H zF#%r~NrA&^;SF}?2k%V1E9B7?Sw1HWp-Qiz*Yxepf=%E&8BRjF4!SzGjoK03kX7cN zpX?zqoQJhOI|ML+PPs^KS#7y4HpKC}buLDwJZ(Gy9$z}yApHdh1|0G>hx65yv(fvvK* z@%Gw+J=Ff^QY}BXNSI<1^fIG79QcPfaY54%3GTny;&$>ZKj-EJeer?17n4*bM?Bzu z#0Y~Y=Q#I+0xc5gizl!#+_nC`2VWyK*VMjNr2*Y6G*}ITOK~G+l(GOW!SR!7=b7y( zn`J9#CLS)?Tg}4V^Ch@@o*CC%nX_AzIc~g?fj%4-X{Y})!@`m%rFcDsC(!q z=F3F;l)s>H#bIOM!Iy`awgUxls+gQjkpZ_-pv!fOZ=LLXgbdMKwocyFwn>$O@fHe? zpG0W9_M6|5`=f6TWtJ(JCZ*d9e_)yJu1$CF`<|}yA04cEFifx=4s6*D#s0X zTd4gzm7}urK8RcBVnKblfB=XDBj`ei+sLNt^auc+iDnS|*86Lj0{e>CD-Yj}O+a-4 z!BjtY6*he~$V?HTz~0mY2GkJc#)8lAk?m*mbUG84I@CmYPASv zI;NWktP zj|0K@hrE5|Z5HgiBmfE3I)*oSd<9$^2sG<9j*0J=9T&?FnQ)y;+lfDEY45fHW($i} z49V6@GiM@%6j1wPYadfux|4wkX1Aed1`lHx+r2BY#pUjYH(<6?Cwc4^&xIgnbm?s; zzEfB3dW!~IYn8w+kuCp-4jqmry)C)K!KVVj&k7Ro5Agh4da zLf&*)&TL_D&X+9q( zCeEk^Lz?TPbR97v!j)Rc7hAE?AK>8fowu<^R?xzE}TjLmB#mHsH@&#o;Z&d7OrhTX)Emt z->)H1aT1^+TNk1ygfH)d((D!$*)S#`pNuPYoOmbs95Q+2%Mdg6QvyE@G~r?BC_@N1 zqP%xKC669C6rl#ZNV1ys-sMZ^0gmLhAcCMN|tIY7H zIRixczRXa`7S!9#in}bZ8Skq4_KpRjb$D0D5=uZOd{n=7{rrikfBb__wd}r>^;>b$ zNym0+W`X#A12D5OAH`JIS>hKIcrx#P>RT#d;?ZDIT0|(gk~V%UyN53645z0CmNiuW zLRvwYBC_alpPmuan*;K~`&Cs|zd-d}pgTx*4B$ZhjwB@~nZ%JHgF*p|Tc~qQoR2Gb z9N-Vz-D4*kBZ?Lhi@95#yFVpUb1Kz=#E@nx|Ir$Nlu~8hTy!IfVuRdz`O;6LPGr~+ zw-*~m3G3F0P){vC=9oh@vAvu}?LYz=`aS~y&nHjv(eXhFZR4i|_-6t*OrV~yPp&<+ z@6v47I`JX(e)Vw9#R^usz*}@Ohmd7ciQw`bLOMvH@W5$<+%)mm3r(M~HWLgYCAJGfP_H$!at69IT@FT*?|KsT%0lmz1; zm>m9@4_?9^-}Sl$XZH$5BA@&^_m{$&LlnElm>I)WcGh44GIOxOFyBkezXCgSQc$nx zB$NJfNu=#n{GD4Ud{}}X?)k|XCQ^|L5O6JWE~msX?Va_n4-0Pa*&NpNAJ+V3Jw?Dm z9k35!Pqto7930q?!l(bB>I7hR>2R>R_py$o{IW?MjFseAQCi#{ZDYmUZlH_p1{Dm| zuj=EVucm`Oj8DzL@dPzwZAsS|>5)w+=Qr430R)*{Lag_Jm=5=n@)#*dS7N1GIu5q; za{;Z!Te$#qUjHA~{vZF3D*nNZW#1-TaKqUnV+MWVcr{5np6VE~ZlBc;pgKKkK9!TaxPbEt$UMt2Q&Jz#_9h5h#}jV|y!<+9&}bFtw`*2w$2w{}&Kg1-f@ zbq<4)TfrgPMC-8gJ{G^kFPdH+?7scuG}s+U0?M%hx_lCp2|NZkTD`b= znmmkJ7Oc`8L(Uk+9qM$wOapmVrxFQ~Ur{~QK~nz$&KpK0 zY50V@hXl*ko6$0us5z20CX~dB40esP3VRWdp+2?-K-2X#`#UrVX-6&nx^wNNdBCN0~~$; z#i>(duFQ6JkzcqE+$2?xU-@j!Fg-@fUj)#|^QnaIGQ- zTm4Y-^S2S79Yk--2xYegN`$ZvAYOaO%^JOwC0kuw-C#M^GN&f#5? zWlM=0;z++du~XEri5 z34R2<dZj)BgOqU*Y%|>&9x4FrIqGYfOq;s{B;_jZqn1qZW;o z$x9*QLY>^k#f_%vy%sGaA2ZYDyY197Mfu(xli@^}dge zVnm+C*PlqYOJ%PWf}4cJiL{9^dGx_z<%g~*xw7zz0i08{V-s7mRDNoEQ?1VtrApC) zYba$|9b|3y65oQ`1cZCAj2vGC5)uUj=SzZ}>u`t~PUJHCS_laHGZMw zqbux^?RN={zN_shs1N4mu@NT;kh>Prn%Jf<=gO;F+et~u>5(fIGCVI7NKvy$$lbD! i|Hh`XaEfIje)S4vQhI1L%61z9e`KYUBum9j-uyocw#bzL literal 0 HcmV?d00001 diff --git a/system-server/tests/integration/resources/oem_mode_wrong_image_type.jpeg b/system-server/tests/integration/resources/oem_mode_wrong_image_type.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..aa95d031d93d9cc1ceb6df0aa0d060b3a9aca00a GIT binary patch literal 35332 zcmeEuWpq?a*64}5ySsGUjSzQFr{nJKLIQDj;_mM5fw*>rxC2q*Ktdn^0=(vO=Q8un zo%h!J@qWCurdB6spIzH(SDjsTc2)O}jUT%Ju8M-P0ssdq9B>=}@Z&o`AnRpo>0@g{ z`^eG9fmTsjUF*j_e2cQIthp9gQ$blx9!3oS@FgzRuHJ}J0O0E8;{{ffp*1ixqD46Y z5CL?61V96DEUmpgq_wovfd5!NtpPAe0IcFmyus=#b z5l{xy0WE+QumXGmN5BQ}0XSh-HyFkn0K@8K{sTYxAN*P{E-M(9BVY~VkOSNSSHSWQ ze&A;xz-VCQw{N}e`1$@o!C}e(0Me@;KRz%407^Old`tQ9<7?@UAK%IV0AU9JhTMMh zyO#if$VV7H>0J>uUfOONt(#!G>JqWP>@OE|pa9s@mScU*V zumAw)#=raqtNU3Gl&=E-1m-KXIRMDb0{~`wm~4~(&~8*1!(V>;-_rc)zaQIxEPwzH z|MLSYh_DY5DiRVRA`&_Z3Nk7tIwmFtItB(7Ha;#EHXb$x1}-r!9swZ{5fLU12`Mol zDLx?);ZGrO2rwQ*Bs3%>G(s#4EW-cm@?!+RLxnd$1Q6ix0C+q&1U$GOV*nLw;KL*Q zeCmGs;*WxYiiC`Yhyeeyn(&|Se^&q40x%I^Ot=WRut8iG`X~Com;ZX;zaIGi+XD}6 zhTKsuR#9WqQSVlt8Zy$91fp(yCOhzxEE&hU12*G!c3uqAn0h_OCA z2`VC%aL-6qHs>Fw)^HE^5tdMFdZMq%gmxSD4TyQVQkS$#iSMmSBpH_;s4L(i9I# z;4JJhScFZ z6gjnk7rV;G(kedsfJxa9WhXz2dXn0k(|Z7ZChMkl+yq5o>TZr29AEUIYFvb?6{6Di zcNz!e#=M^;!;FFZS7o-`0kuRHiAe_6p@0~vvn>f?kq=?Dya!SvNAFCVId?5USOdV( zQ{mF3#YR-fnaMNFi)N`F4U4Y%Ow~te{GBQvk5|}so;WPGB!!M$eQ3=9dH7>^N zZOj(1yx)~^^EayC7QKT+E1ZAU{7at!$OhSH!1mnzOHAz-{AvL3r~d$!@V7)MHA4Va z&Z#E_Tk4=r4Ph zsa#~HDoy~9J|CWO(gXl1DAK4R<)Hu?Wblwwd)Jq;IqCs(*+=r$IqPd*x#n^1ztI2~ zfR9tc6ksagip88{n8*NxiZ{=^4-qPnv$R?OiE8jvMky_TCVm3ah~EWJYEE0PMT-Jp zE2_$3P_F6l%2IbG>Nf(z$~+#i9O)y5zfFxg;`DKOCMnGQ((8Oq-8VD10<4BVH-7$&L|UlqV~3 z6}qoCdi{5;+?fehLph-t;b(Is0^po5lpX`T@`AJ_a0^}vg5K@``Lvo+An;ftFV_N) zU=da40BmQnG+1sB^g*e(@DALD-%!Sm7b)O3i6g&9kb85yhxN(7m-+nEFMFghI-_Cyxh}=3K{=X zgV42t#G5ijxnxEA#Q$0M*FD-wkDUj?dw(^!xfa{x9O_-Td-3Q*=_pt6l7X>j0Lo#h z4eV{GXFN%=1dmr5HsSG^;yX%~khZge!AMBST%d0#iN|O>*`(hhX1zUDhyD`$8z8_7 zMD3}uOR-O2GvufCx}X6=$;uW|Pce#h#8nlxi(2e3o&AJh<@@ReXU_nx&S9e5?W>Z2!NkRH7o~I?4?w|Qnc`2(gJ2$evADL zk@yrspXm`0!Z0?;r9|&5hu{OaHo?#VIR5O8N<>5$9_!|$R}K1BGin1=Y8r}s;uE-{ zqNWf7n~?s`=67aaY(LGdCoe6>EhPy3hl|{hE`@ap#{z%rzz8&G&y8mMSjZ!&Q#b?| zg|E_-<2xyoDcHbUhW=Co0KCUsL%;3*4dGad;-5aF@JvT~0`do@sbmBXR3)MVcW_d* zp(_Bh*Qbn<&`{vz9M9*7w?1R>@gi{U7(Cu%$x^^;<%Kl$u}#g=C{|vYpRQ8US^yAl zTu}aA5dctJ`6?rp*l7H0hvS%42o7BZZf$+&Kbj%5TyX^Jc=f0g*n9TP&qM>lD}%C^ zc%Cm7(9}-QRFn__bFikU-*4N0LuBnG@|twC05kn(BGv|gjG;z85aa=?|FZ#zp*~s_ zHrN$^tbR-e5Qaa_NK8~^HuNe? zU8JB%af{wEOIHPH(~tfW(897T0Gz@Mqly$lk&PR6L2e7q@Ql|c zvCU^1oeAs2#jkWrNkzDZ*IS|3Zt0e;p(sLg;=d*T1;P1D9VSobY`lmoO)-oB@TD&S z1WmybKxo+kMkC2+=c2Fycg&#k9&6_XV2yf{sVc4$uE*%TVlfpOkF$%cl_Dh`>0+ad z_zht~;TZM={kQzr1ON5F|L-0^f#nJXaA8SZ1Ozx#SgI2KkJKtG*NKb*z`^6v3X$O9 z!?K#hf?7OGyaIIe49tuzQWhz&Tp}tg!w822{{yHAt(Bfn%*AV_>B4o$?$UCDlwxrb zSIQ9~sx4c|uhP3QZ?5&8v1(aQxRUg?b*mP9TsnS;K`tsYCe@Cpq9h~nOjxzxwkccm zymJ0A*rK_Da3g*j>=B=fnlryK5_`kQocaVbQU@4P3+ihd@S`M!!bhdRpCs;pJ>p=J ze0pD)a(S=+68kSV9PL?Jz=l#-6ayL$w00IgGVRiNBzC17)x0&RpkaGp%n!VD~fmi$qHv>UE#M(I4D7iOf%8}h1s6wf1Q+~AhmZO*8Oq{}Bq%Tp|>7Q9)&7oUbs zZk8Srary&rrQi*ka^bncKsrVv-emghYbYm+;l%vb)Zj|RT~(#lioTK1oKC0soqS@v zlHX43ou*(a;b7x#$@wzCc#WNBM_-oxh2S^8ywNmruK>)su@ zy;uWDbGi=YdQK`6{&Ls-igl`w%!+omdkYO@p>Pct+0_z6uW@l#%&&0TDA z?>8;f8Q55vLt^rl(EQL#E32wxtfN+xqN}9#q){oBm=r#;XjoZM3$lr==Eo4}X*7|3 zh1j&}t>Wtd3L1JkaAcy;LxB^WO?<951&6g`5=&nMKC7z_qII$FzR+N5pw*;QTK=rc zr06#lFdYAQPC49){XnVfWupD3pb59sVv*YZHyu&j-`lF61_>(__S-~%RN>mmCKkdI z=X0EByK{rcGDV1(LKE@>+?77yU}e*>9mXc&_9PQNOFDh)N_XR~=_;ugm>XZ<`U7CU zER}OccdM%0zU@j8&GS4ITuO!~Fls?KD%Z?d*?ymIHGp}uZ#2TpJ5669;EDG#pBH*( zyYh%7`-OPqjM6Rk0iV|VUTa6<=W+Gdf)z|53s}a_j8fB^ni_EZ!zqT%7xK*qPPDD| zSRq^KFDVoR`y0^;G!DS*h9C6`I+D&@ooLnM2`bQ{3zWAy&)R%7^nL&=+j55&y4hg7 zcUu16nTtWS-Frggfn=v+oe7i%r6Q*j8}uj4typy#Q`@{gNwY+xRmSDzVU+f&)v_E5 zOZ>+~!hvfkVfRnsk;54k%EE&kPj#dl!d%`y)d*n8RJHZwnO)3xnz#V5Ir3vq&O}F} zacNc$gk8rh(qrpJh%ZH>EGW9wqZ49`EJ$DH%Xdtu&U##7KhNg&xe$Vez5@4xfnOo(;abP$B!MPsR*BkzeY{%DD|J66g`0_nrPiY*u=`8h z+J2fPiOp!vXnUO!_RynybqO6E_Yv3lJO1T3R}DR}6gHosK811*Oeulm#2T)fn>=?Fq0 zb%nu>++G)b_lq8NLHdqEt-~%AKL_&8)hiSCnp8Bc@N2s0>#ByXPj321jhl{*`Xj2V zErPQ`A4WzjKDeN-8f}cYq8@rc7OJgID1JG@EqdqEEP0lpXhPORzr%8+wx~E^8GAiI z%fw1eruES}=>v4_qT`Uo;z^u&M?vpXXa37gcf2h&kW*BaGWgymzCSlde69(RzBH3j z7Upk8xjqk)=dB&Zv_P4;LF(&x$3CA9)CcJGV+nElPpCr1l;TJT5uH2H1ktcXY{LS2 znvOl3pfdxDl@;_F@TwlY`=6iE^du}`vY_S3Mnkad;-i%Zl!im0z-RB5EB&o}gaS6z zvY>W?ufASISI5sZZup`jTV2;9m> zuc*I?4pr_`()IvEiQYX@K64XJ>|I5d(FYy}wg#PgmMpr4*BM6YQjud?H)CERnR!el;@_xdY*HT**>Ycm3;_-V2D-d3QN2ZXpFTCwP28T zDyz|^ZU3S)H|d&M+bjU3bWsIjFI7DDT3x3KULn1sC-XJ8^nBJE*$J*^?26~H9Sx|B zY&qG;sB zR^fAwQW(Xg3zPhsVo-7{Pe;`ARJYW2X0aw{$Q()GERS=Mysn9zgP%ABwKGNKzwaY(EgDpQ|2#0TV zk+F#-Q#lPzd|**I^kD|W+LvT5QVR=S0f?1?K%%xvb*JA^Qd>twnjzhKAC(N~#ILil zP1*t2m3x(v;^^^JgIhy-^wWkdN9nMW9m0sOBSuB0czJD_1r^#-0l8{CxdLlhX)@`a zD`pnzE|<^ll8K}%xd6u-y-5nogvER9GV*7keeFf|iE+6oR!}>aUOT7)NJ^40It(p^ z9zQ<`5zeuDmaMwIee?9WcLNItx7V_fRxZ8RQh`wQHZ6)FoXjkoV23Ok+y0X$oDonXvd z)Z9mELoQQQ*%?o5;B!YhqClMGZ??VCeOOj@X)M2yyDhsC%sCppm4`l_rhcLOKsXPV z03p`Fuf5?w?K^+rS8_KT?=n)7AucBm8XmPA+`Xq=3CkP>^J32MjH`sv2HKnC%`XF* z3;R7ca<6p-uNtK(Wkx2NIbNc5axrWQE;cC6o;ee2MP^`_wnP+fK zBH*aHs;%|riTEi3Yf-0gu-{;?5V8vGlBZ;ny`%A%hHeU&*z!xf-dgs{cPKK*ohznm zo%5oRFJ;r04~BBFPBw(Gu~{KD!kG_pOBUs=0$j|w?vo0n(=wWabgqyXsHLul%MX=eW;lw4&mzjGq zL5be%W%fN@h*)iXrc1ntO3ZtSy^aIEbzL_xB{6h8%Y9~Ma@OcJUgEAf-fRGE+rp2zIiJx`uM{zGAF4 zG76~~qlPNMrBA|IyF}6=5HY>F^vdHHAskK;ZUtk9;-Oa1h0>CSnnYjRiK)Rt_n$4z z5D|r2d?RQjl9G#uxu^{H%o`tSGk;JKLy1_2JoX~3vI8EQ{w0@-9$tdYgDkt!j2TMT zs2EDVVI?m+Hj9g2rZ`|m9WZ&1(zY)E{Cr~MW~jCJ`>(6II-{}!t#oPWz93XPVeDD< zDz|&+w<@dmI31ZPO?nNf$#t@q6PPmJI3zAYVE%`EOnX>Vzbl4-gdtS91toCY^EC3V?cv|X`yiGV zpf{1KO5?pUWWe|K8$Co5;{7fc_x_A~kGaHFT*0Qpf}eQMwW7jA&*<^^{aIC$ZX4?! z8_m#HAG!1T24Wl9Ol}r_8JFU1g5ueHXf-9Lwb8G>xRMK;uPV}Q4V$gAJIa$sEy`Ha z)yAR|Zu0%hXnS0ZNLzU6T09+EdT?|qzesqRcb1s4ff}xO7=epVh#;}!GY&datM9%# z`fQto9+^11LH(@ma4*3WAuI&;E)q$(>zwK_E^KSHe+;>MZ5saC>P#04Z?@cCTUsWY zo;Fm2P{M`Yqh+DdWE{q39X^r_y(J2Yafg+XSJOmu?Ue4Bn{i}xLL|wXf+YX)@VITy z^z1KQ5$!ke<)O!O`6dg`Fz?tkJ)?(*Q`mAG4P(jhY_!`oUa0Uo5Q8I~hN5j{;NtY8 zd@(tOz0{pr_Efxn;q*Y)FoZf=^i0gUu~f89(~d*LgzBH)+SE}$4rLwH=4UYoW||Nu zlG-n1f-dH~MJeqV9?@A`Pu$s-sq#8{z&jH8WaPgVzI;nmZKi^4SFn0KbQ_cRLY67ze-IJ3hCTc;mGd(a+} z<{&#G5-zk)Vpuvoo*LVhrV}}Q8qN>J#o2LNgn#jzcBsv;sv|m=q21CbCvgF z7j8LADC#RaZu<3>_CIgh(swouJjVq1_)B#M-TtD9PAe$iD+)j>iqq8XkooqPSXL!$ zB$-v}XS|MVIaw580Dqy(xuXtWt(tT`_+8SQ4x4*C1VlUl2_Kh$mX23in~+}6($gy}wY0poZFq@+PX_Gm zGqOm;$iuItV^z_L~PNmA(aOL`FkHLfOZ)Ks0~AEK3I5T`4?V;*n6)3Fx))W5PZAJp^_P0%R$ z;|hDpqIyqSRK>KiRx0v-UTBnw%5svP7uK?R;=huU5D@8eVwRID{+A z)9ds(WGb$)I;*ysd35_-cCQ zVlT1&IG^lA=c}K{M}Lc~p?(@>%&DO_5*|Z0tcu&vM0aE(Yh@Sa+1v@>q<@IN^;#a8 zRN(lQ0J;zuC!v%qq}n2YGgO^{fCte$9|C~OdBF36;K;U_LpH@!}_3r)4xrg zc|-co++tI`Y<2yta_mVD4 zJC0QlDomM9>X^jYhUQj-T2SL$RmG;- z)F{K}AlrUgZ%@WTrqM*kA73*p+~v+7VnOJHZr1?oDaf8Yv(L@ne%m>?1jJlkS}2Bl zraqEc0b;>Jc}PM77Pd)rlTQ5l!VgK;|8t|-&S2LWJ0Zj92)l`b56!$u4;IftesTjH zgUb>%I+c*IBi~EWxve&@RgzW1Wjlkf64|S%u|I&SHm@M34mWL985ylq+SI&gX3=f5 zsf?#&_ELmk3nqQpH(9--qncPW_;0_hlKZ1hMF&w-XtO3uqK?{k#|`%c-N@NcWKU#H zq?hc}hBRK7$Xx8<{Gsm98l#Z0aDGt#zoE6YP4y_Lknqmy|h~AcawoKx;f>79q3Mev5zFPbMV*3#W zyqg>i#(Da;=XfgVzy*o5Fk?)&T#9+t{Zeo}J=ZrLp*PZBhdp!@jhYgwJ9)0Low%j- zyi$jMn{ZRYvguMtGsn4KU5EVRcUtiZB15#ra=*;_U?2AKd-XL7!?lPe&tNP5Jbo0^ z4cDx((97~xPq%0T7j+c@W8KG@`orIf3-nIshGn{mtBJj86>N5tBN7cQ&1&*3_6S z)#hHtn;(?vASl|deIB)N*Sd3-0*|4qHr2ZmFfnU$jmaq6yetfLJkix;n zRg6am5#lRu(WaBbYK_t!eWdj!O04LC#CfM=6aP91+Yi9tlLnD_-|;?M=3qT{IedG< zb;iEYKEY+Lp-7p{c4bCH4hu0xr}s?Tip82H)B7irQoV*5WN{@{OSri&>qBbq;wxQ3 zyyUl<0|&~QbiX%WnkLYU7nOmC3prW6Ox3I=Cl~@5Zy2Vjo(U7dL%%bi){8vrJbswN zF)rKC^;M-K6J2(a!Ke~Vg%UrmbScba_;k~gXUX1>)=DH-=ko|;_CO?sF)m};e**izg_)qxlm3_vubm<6(4 z3U~XI)!0If+G}D7*C<=0Pg&%l18pjS76yh#nw@?%AAJ^6kH;%Q;%i2o`2BESVIQYY z%S(g_TdW3sH;KP}Z(>E%To7Q{@itw7&IsQ%19`azgp6O?iH5(7Hd$)5&T-cSnRI+@ zed=$!*;c*Qjr~g02O~4@l86c0Eb@cl+NZ!m&GW>MK0|%_M*zjH7`SIY=M)w5NouV0&Ty(#dz3FD&|XcR@i53dt}8y4|k&OTK~VzT~) zF&l#NtjH<+*{Y*cyi7)U!B($MZ>dIg@{9h{$0Z@y>0a$(Rwm~(>9yZHi+XzXzf87y z)O}l*#Dg?`GYlf-9MYGUM_cPA$q4)O{6=-*1>L92RM$GIiwyYamuRu|bSB!@%`)p$ z=yk^hVcvWY*Yejhsyp67;6Q!tB=j8-6zvH~4Ds%4bo+6so}TP7JwDJ;CbjYJvC>Cn zx!Q6v)wOC9Z?<2)UuOKlG(d&2n`SFg-%yw>`q`046T8H{Si?!Ya|4%N$v4Pd(pEtT z^12onV=YD-5}n|DOzI&b!aA#}u&hzS$>Ok>bHidt9LFA9427tk;rsFG?*zNS0+Azz zKJ9s#zUz~f??HqSV$_Q2(4O!2AA_^&Aei@nCNX2yqb< z)nI6}n%YT$zAsD_ncyHk8ZJlv0I|1rkgOUfD^cvP5MwDOexm#IS{+*ZPH?8un#)Fm#ZI=j{3Qmw4FF8SISGPpb!cWIaV4pJC5__vuAx;N#cM z*vK-DcFT3U)SpeB9mfW-?yviq#*kBJ%ClYFOP}qiVsp*##yxj6x(?bAW?)bz&R1v) zSk2FhQsHL-2@~`x@!uPqv7bIVyTwc;jw`3hDuxE?BkG`a5+Dgq1t?v|xT8$3Hq}hg zGj$T&kL>w`Y>Q`$h?_g3*IQaam$4IMrL)BJ6vdXZ?pBbIOtH75(m`w~&@$dUNnBh( z2)TQWSwMhn0vZ?Xrjgq4fV^Rr;^AjtEj zOHisxOYlci68JQm(nLJDjz)C?d!p`qpxh2Hb#5D91q|8$tHm{$`~SxASrvQ{31QJyLW ztWmAk8zX-B-GGCBcF$1D^P^o>GDXUQrOb}(GQ&mnhb$`n0dsAA*vzc|sb!~@Mz5E^ zi#lL3qv+KG*zzLPVdj9dy7}F(9EV66U*B81<*Gy(*{V^FDyTd?Lm75!#k`s4EbG1T zS;vPNcI_T~-t_n*wIrtHmD|0R;E~6ypO$M*zgXQdUohkp*()sxo$Q!Ul)j-P&ENuU zFNVkJ>C;_l`q-4Nd@NTbYfj4edb(FnYIhq4e!ik|VD|W-?DQ4h^wX?3E22o*Ha@zJ zDX`6ywywNM%ema$qX}7VMhmvu1k)^fA|Xh59Q)#^hcIDUw)O}2@(Il&+1ix&;RdK9 zSRYlO^}QVA8#R|07C2J((q5-7cNac8RxYj6+VjYNM!>kKL1+u95@ZNe$k{0byQX;4 z+T6b1)kV3p!B&kX7ESpOppa2WyHM8mMV`oq8LW?*S_+&X|DLhf$|n^HQTj^veHD?dMb!k@#y+1Q_QMLq~N%Wt*&;O2ijk}rH~zApQ? z3BU6VdQ6+xN-#d15Vfs5Iw_XiXpd1qrIAXswbZ_noUxTAf5|C@J_&)YNDj+9A2cRh z;*2$rvO4rSS4&GE%_%2!P`xOR$v3?w%w7!FwfAjFRG5uGV#Q|_G3BxXPid?Qbrn4+ zR=T}7B4-rCuj2vhct{vF90@8$*Gys#%k@ogEhY8z6x^Rp!tdUeudMw5Y~5CCU6Tlf z>-N~qEP9?WG&tN7f?Wot(?a|9Y#pqq%9)-$7Xx0X`L4eVF(r5MYhYx4>~Luwva$wM zzaFnudcinO+@mtyQ!SO_>S|DoIfqIwXD6xp+G6Lu)76G~Y#$R&@B2VKzt>{RJ$3Nu zJl=^&OrG>?w80ZJCjJ`I@`4W)kv?Kk_8%0Z;RxUYW;+jJz!9^KtBM{=0#o<9tSK`%oF{LH$@HW6fJFQr0of5J;obBr?{u7zp!3 zLATyRN2iSlLm%OT&bsKptwqO0qk$HrW3i@25qz$J7LJYQxmwx5LIzF@t+Xbd5Yl_o zaSEse=5US=_ZbF|O$3$8@l)%-QKXv$lHX#xk*kpfJMt2Pl zr{YMaK*+x>FsX)g(y~1z%30%NS}OC*AzZwgd~!-&a#ZQyV;ux3HpoCaI zrWc~CM3y2`w!9t*Ld`0=`D{yARN+==Jj(uto{wg1szs()QTZ4|%D}5u2eH($-wCR6 zHp9%$o!;rZq1EZx>R-{~J&L4ZrlOUWtw@*J)fnjCoRlksJ+q~52h8d%#mv%%keXLQYRcklEaKS z=JiI>N<^M6c9opn7Ah9WXA}{{r-OI5wsfjnbiT=3xpl?_m;RW@FHNw`q;Pe z7A_Y9l~Syf6A5x04?Iqc8Z)|b(M~H}5`+fW_7Ggq5?Zuo_+($uwR0juW39+4+LjQ* zwV0wt%q;4HCFM+OK`qEx?&GAWbLA-(BYvDnAmQj=40 zPkz8*PfZs^&n!UB$y0+R^g-FbR1}V%D#uL^!?D|{z^8&!IafxmRR=c<;jI8v?g^-C zX^A&CKwOe`hmk^lJ$p)^u#WCztV-emXJwL=Fp3+7#vtZ$Q%AjnQF;=K!;Y;b-+)X- zdc~aVOFs$Fr?m5wY5BJc1&x|C%<%a^S+87W zBdt`D!}(3(6brUum{xisY#oKd$dd4L@>;uh5XcuUKINopBB=I1l{U4!?gwvig;s3Ro?{b z;RnY->7DXj`zpi{w#uSO!-zFD3bKrIKpixFR6X+R`binB1lG!0TBSMnr#4qZ5{mG< z2fEuf58S?4i{f8t#JjM(8k85zH^xjBy7g$07WdrgYIuJR+Ct@jCBS{fC_05i}(>?J?km zTofbeaa8rrr$q8^Qgvp7JIzlmc{jUW*hL_+sX{h{xDg}U9oZ_4!Z zF3lpZYo8^1+_HMFUYVZDlP??*Ejw0it`hiodiu}wkGaHIXp@;8R+C2Q}Tda=$N&zq5Pf{zWBSQhKcPxH#m zFH;v)pk&PjepF*DKGkTNqoD_GXkySduH;q>AcuvL*u8Sw#?6Y9XCU*TFzq)NU!w5Q z5VNK2!_JGH+Lt%AXS@6-*^6E@E6eNVQ0bP6&4A(HM}Z-`l!OUjUEAY(1pk|(!@?=s zM!jO|p$C0@)F3cD-kwmAw5y3gY^1;Pgr1f^kFNOpnQYQt6#QVvsV4}&Z2ae#%o_<| zYTmKkHkAv*6@y(nNB%`mCI&9_!)mg_4nGo#jzN;kx5 zPK!*s6HPj!7tM8JRnsYFJp|o?$86YJpasv^P>ZK!zCdnvB0jdR=%Md7+=fy|?}dHQuWacsurnBZ(9?rdKdit) zd~b8vGj6vPNC|dn4wmFfBt;$170BH5e(Xt|r)G^D9~rw=OxCRcVOJ+|BGOi~drWCW z$-r8XY@Yh}eUf!x**b&2ElONSh$G#HUfFDGCMGOD!wx%YFOObWSF%!`sh@&{w!%~v z+_;Lp#jZU$Fw$-=WpGhe_$+qA^Q%&z*Gn>w(!Yiuz>H2NPxi`;c@hheUE%JmbH4w$ zE;5~#FI_1ogRG?(aqT8##NUJLM)jl@=jZr1+&`DEHu=)5UfjlaeWSe*qib80rdM8q zF0askFtBI#W_7}fT*%bX((!>+N^7J4(=HND3P^$_sN<{Em$kbmSILyjXLXx18w#DV znKs#Z%|UR;7RMw^M95=O-_9)|X=hbBCsoPu{+v4KA%2Hnmdi5r73mpn-4jrfvtv)} z=?WHFvptT7|8UP|)MACEr&Vb;{4NTyz`^~P9=EGAv)sqa=JFiv%rJ#jxqke608ZK!v$(7^tZ_J-o?O!RuwgB^m>v$LxXjY0Uf%fZf%o9C@jnTc7mP%fy}8LoFL z>d`xuB?9@XB@o_~Nhj%`ixxV{W;4MXHqudQU&~GxGG`)T(e6MvoIwGVj!>bAcr!xx zmU-u#NAu*^e?IO#J2b;vP1|(akIu=avg3VpEG0gE0ksrA{zkZ=`8@d71VGlnO`dlL z;r6>!Q82Ut5uB_p-rR90G){c4{NRf6-Bqh^)6FB95zu&+&$_fvhmO8Km;!Ol|lSD<6 zm$vt2r}-1vf0V@1r+W^ItGmPRcFxJ=z6kmR@uDCln!i**Ba6JsW6D_NNdRD5TuAJR zi#4$V8N+h(N?$r1Mxq@h7*+JPVi3P_>9Wmh?H*|$a#w1qPe~e6$p_~D6|Q$zCGrl; z>XO@2K|1k^yn$Z7;>jkNoxb> z-EmN%P#mQ7bJjbXvHYXU4l(8w#46h#0H5~SUP(@P0=%op2t4oQj38lD0=sf53hzk0 zsWUpypia0$BfliY-jTt<6NoW7 z+hpQmOb>C__;M)l&}^c{mlw`fXsJL?x8id7ADgBFAMx`>@I2O@fjuKecrtoWS!iTD ziS||e=lelzld%uW|G$-&FKc46@*~T? zyo>nUJtq$^G((W=JNnHdsmH;Xd+L<3klJWwg2uxe?$QK3TGDeXJJP0zm78=ouq3s#=#xhCeh#9hx{U{_`CzfU0c zQZYbYPvGz_?=H^kaB;J%beR1(?Temua^J+t)kTW3e;SId4e+oobWEa%z*X9zcQ?!i$Z zyCd}5Pc*;8Ecv?5{aTH=hJ<S(0_)RXXv9JVw1+V3~F1GsL{f`+$)r9`V%u2ya~z39XWyT!9gK@OJ?`SA3Qo!vh+ zQ~zKo+uLVn%F2#Ymcfs^!bk?))M|O6p?S?iP6WV<#NBJi%xTbJaVtn%!$zzZG2Jct z+@(pr4_Z=;od^#5o7K06B?Cj_rRG-9etBLQ#UFsxgO$5VgZ=sJ0<@Dt!k72be1Wm9 z?p!8>W5WYAkCu;^VOoX%x$U6NwFES+)RxGSt1wSqT=yob!_N)^oBSg)LBIBVf0>!@ z)nS^6MuDn=LrU`{dJuMIg%cO{y%=~zWO&%IlwZHj1N+&YpC?f)acMna=TKVT^9*Y( zO6^^>{Qeq^B;5~SDuRfQ{1XUMufw;0FCNrNa-%)dtxyq{)&)f(FjV8_W^dO(9_fTC znNlpgsyne=TOY)TbsUz5kI~*wlb$1a?yr}tnU55ki~}?h(~2rvKL?84*pvaw&2@;K z@KMsHT+p&LC2ymgsmP^bbh26S(aNMW%NN3XI^k~xIBEHVdIjNMb5+S}Z|N-aEW~we zAK@bt#KEH}JY}W+0T3=JGo~ps#;0x;LiLjGd)9YD_MYQY=UbkkNe6+;-mhGL>w}~& zBg_~=3Q4%_NKXQ6KGCl5##VQu5wlPX#)}t6%YJj8VKB|oQ4aSj;#cEa#*NTo{S=a; z!(nrY!nt-Xdw}7%Gj?RSOUGg#mg+|K<#o&gTzN)|5V8XQ%A+EJG{UD1#l_4wVnSxl zV&*G-JD!yUrn5(bBrj0OL8bj8{FD%55Bin9!SnCZpi9(J>b7; zKtaR+c;J(odIQj#s+(wq<*1TxYF+!kDwJ~M7~Bg66A~k|Kg|Y5y)4QNofdg)B6>NM zuvyE?24WyVA=_h;nFt?0!_mj-B-bgiYD;E&@3bNxd&2X!Y&U2v4KS2p2Zs3St+BPh zJe%(nMoOd?D1lp7J6wXHpqh*XVu@{zWX}yrU>^6$- z7~pt0auEEsbJr84#7hN3E%?d=`nECN3n+Y2qK{1>w`7#0G(7Tw>OJfj ztK2om_tN(9N}r{-4@CotQT(?l zSCq>aS;2FBV_!58OrBYKC4EiysD6EZIL@L$V_%Ij^n!QqEa7#QATVbKclh1|t!mXq zcIwJ-OzmvvDKE}zR&Lkp1EIK+F9;G08e4N;Wlubw*TNaNeB*j>4mDW$j`n4mzhoyo zank*4leK~Dnhs5M-)*z|1NMh8DSG|<;M4Evyexrf4;J+i1IiT24vI{>kH>vR*)cKR zR0PCm=RLot#yyXDeSI1Y|E`S^!)Rl<=Ap~>eI$Cpog5qqck(5941CBZ|4`$<5B#Nm+!+ttoH>)=!Qkq4|2z0yzI zv&nqCzuq)lVv!}wrp|I-S z9>xtaJ@)x^{w?L6=HU7iA7%OfYVWI~qU^f9uOSA89J;$Z1Zf?bp;JN{B@_^llANKW zySrOj1w^Dox=}z{8l(lK{05)8`Yn^r0zV`X;efHTW*37xi*%ze7 zD;ML6>rLzWg_GjS!ln3`x8Y0+g(`J_P5`}o{*tZSeVMHhsxQtmelKnfrL&DfqoGk$ zsjo+%BOYVydY^gAF$&c!`b%`4*|Z^!#Wmo0iq~q<4}B+cTB9k{f7}a8CX5h4gq-WK z-O=TBnzwjw!Mpk8bo8}s^gW4teRSJAHtBM;gg@~Zj=uqM*;<%(WH&}j&6_y>H>mx) zd<{UnzVKpKIjI=D?>eUdDO4D%dnqf5p5+;MU%mWvlHhVE-E+-M>-@s7a5WxBvMib6 z%`ev;FTQcMU4pum+`*74y>7>ffX>mFMpH&3dDQWCS7mItQmA#oz>rajy1Pq}CxtDs z-*g%WGXKuB_iLuTwxt40a{nm0f}OCeoA=%tMGW`1uiA7&iXP^U;8TB8f8}#kdi_aU z@8HLYReXu3h~;?^=8(E%&kxOfYGcn|WHKBL)ah5@X~oa*X~+18`h^{GcIrBJ!}kNA z=3m!`xF6+Oe8C0f{-8aBaEem_r%x|q?;o0wP3f(yS`{{YqvKSjI!oX1ETRYzhtgM) zn#esCNNsr^;y4O%*SWF4wW82xsBTtfx2{W%eE!b&=Aqj*)A{z0tMJG1pkk?j`N#?@ z4Y6Rj97N7nY~$T|g}P#-Q&WaZo=5kw^YjQ2^ z4PPb(X!C>(!X#$TDS#B!S@6r51kKUR5B09|;i!Rc;! zu)_B|a!{SUJK{BRr|`>%;Oj#b{P+-$5+cQ!)&|bk6m>@)K5bqSGJ!(iLJ8gmyyTZy zbjd=257}7b=ZimZ$qC`zq;@A-x`ZRYU>6s|_XfUwnTQk&Ct?Ib!<1><6v9iDMuWA( z{A|$pR1^jX74Prz%T@8qBnV%aBLGC5jz}Ch1bt@Bm#88$iUoVf!P?B#N#YCo&4_Up z92b|vgk>ZfsGFTCIlB@>_3$Wu>NL$u<*&*ZssL-95tf=eL0gSG5%|Zmf-D^`v-+&} za&Cg(68Gy2P05It9WAL{m>zdbmj}ybu^laMK6~z?KWTAz{TmRXSnfq#%JuCkZp5o8 ze3X$*Si=tS9^s+2^s0)+rHvh?edC{z&&2cYK$TLOHL6|{hEau-|5bT3!T%`eog3IJ z@#UMvjVm_I8M^mpqW*-IQJy{~49)A;WNtK%;yf=^wTS#G$Ht2NJ@O3=3e}^nE$Y(Z zFH=FCO`nw(R;inPndJ-a4RC$C&sQF`H0xwSK1SkjrXkQ|!yqV<;EO}fJ()`YLKmt` z>rCT)uShd1UPE?WuU|x$eTU^OGNV2YM-k0ze24SM490Dnc(6~d^#o8z z=y`g7HDOL*-${#>Y|;{Sp#N8?V1U(w^$>-*&mL(dHs1U?}VgCjM{76T@8=C7|;5sAXuTJwTb75?UiPHKwbyTle zVeJ}aeWI~%rJj8uzq{TQef$~MF|Oufobo69uBN)X1%W5tLL{w5LU%}p<4GAOgB4-D zWmznI3-W~5{K0DA3)^IJeQaSP;dQ*n zgmzA4|Juw5ag*1LhippzexEzyb_+$oMQEnTvFwW+g*PyGT4;t!t;= zm{#kCDzq4baJ-7|>E-t6D&L}-HylE3J4`#mjwKiK%MspkhDEU!(Sz?de!b>GTP~gi zUl5}FRXtL7M@IR*&><>5idjnhuA%-w@bjpmQX8I3%%YmMzM+|r7-}ONdKKZ}rCi)@ z1TIPF`o4o)2Y&N6^^K3}ZrLkrCvhDuS?G9ceNW{v&1C97lvbVQ0~lasCV%MZIjuah zE4&eXl~#^3%0>^|dTtK{eWLtU(6coe=bPgECW;AJcq1Dj5ZhcmRnrEqhSTDaPovDw zUTTs!jY>AxHXd0%dP3yrchG5Q^1aX~7w`Rotly#6ikTZckDBMWD*$m(MbOjeO|lfy zqlICdSM?>+GPDO2;w@yxl{sM4&%2UCW|lJDWpha@rA?4!mn3mmCP`*>R7EPt$xlOj zBFCT}4eRPmLdr!A&y{~8Z6W&rOM6Q&kI#6%=hD5&Uqsh}yb33+l8bY?LlPel-Lz$0 zjZ0RbUWK{HWiw@-d}7Bb77;0D@MU%G4JD5H-H~09m&<-O_g@@N1=93@<_F~-wFEsn zP@-Fr)Zt*4INVE@bjnrVlzmx%-b%`I8iPkUt zrN0`@_8FF^>r@4yv7ebRoOf39y+thus=ooqiP~HTLHYcf(h3mwx{8LxSIr-Xh@E6+ zxLXa9Rnq)@tnD0)O=p*KuQMK&1BD%LAL7{HZtAG*d?WSbwW)gg_`9YrL%cODVs^!6{KC>V(bY@o7bPLJCZ!=z!{Y@D<1$7 z?o?EhR&w_@_^3(sH?fwWBTd@8Ov}NKH^I}e(SspsZMHe4}p?3{vLVj@oOjGcBgAN0gXoHmZy`Z+_LZ zTrV9e5A%J$qx5t4v4Wleg3Vt)U;SfNmM7G6r6Ib7^~|8;Es*vwI;Tp*xc^gdQ94#V z0_RaocB1wx7GWgf($Pl1M!B z@F~;>wtl2wGA%>Nuo@YutJEaeu^w(poPIj?8%xf{##i);bOVGz;Th5oftW}}>CYUX zm)BtPA0@LfM+DhnTX)c#G}By`!^u}aJws2ge$N11YSu}F)$>S;AHSkSi_KTg?7-5c zpA?X9;_T5&XQd z$fOI(R{_y=F28iYYkTOuDon>=vw0}sMSvRfajPBEs#x9dPL(yyC}-;E?jR{OoK&)ws~%@N&n8KQk0gT@OAIds=q& zao78Zv+e1EaJn*|8XX!-B$1H`=hq?)89c^Kb3ZNH3HjgDZV*E5D=dYO?}~CO>q{kv zciW|^&m?$-*Ua|jl0y=uune?sf z2HywTHU6xX!SE2TL?t?EwoOcVSZWWuKI?+zD;>(Ekj%2di)Ja_{Iwa5jxhk+0uUugawR*JH4rJ+9GM7ZS#U z?2ONXYQV`d^5VJq7k7ry5|_GvQE6O>ygO?h*Y1bUW@b;J z%Yn@1(I-&kaK!hu;%a8G6Rl=tPRZZE;HS~;S@E@guO=4Btjh1dff8$%oXCJHrm6Cj zt?iCx!u?^9$E|uBoGqFlEX)^!8KPQep1nEz`mL9#ruuYp)AdW=24G<{>@S~Se0Se! z{|(Taeh7+Yf8gYGylN9&if8?;NV{fj%odt$?9x#!Q|@}|;_1-QE;!+%po1CQV9*w42&rKJOzENlz2bC%{#u=)aBoB-EPAr!YLZ) z9`-9nMkLraz2c`VGa~j{%Y0Tnzdi785Bz`Zfgm_WA{>Jl&Ki#m{9|6nt6(I`0*Npb|L@wR zatkAdp+bfJtCSHE}XU%`4P|8uY|Iv?v{ljLI zJy19X0`jK_#r%&6Whd(K7LRHLibmOg%ZW$D&jexoC**&W`h!Qc@z(yo_!tP_f5XQ^ zSc&1Ngu?;`WjCza^AEF;K>%_{6uRw_sva z6)5KI<9|;86dOQ!^42qd8i+C`2w)|K+~SG<2LG3;csL_5N(%PJQMa-v&9`;^P5M(5 z1ZPBp{Im3b@yy?9_&4+a!2`so!Si3mG5#CpUl>N>KVZ}VyTz#7j)7ZRmKF8In!l4` zMBENVloaFb0G7o-Jp!m2VW=VWx8nihcD}2iME+6ZtvE{dRx@fW{l)yxgbD@<|Cc=| z0OYom7)AU&9^V2-4|o-ns@pa5FZ>@c zigRnv-|AOk4U%Pyhulg*QG(bQC_!Rb4Afdj4BXQHEdeOye;jyQit2@UI0h<5>Ho{C z|5XGCx?PYNiU074A*}Hb3}WotPDg{>_Qk)`zdi785B&e`f&V44%Wdoz=>IkL%RdmP zlQyWMHmDDCfzUBgCv8x{WI#krfRHRy$Lh~X8$s=Qm}Teq%HdI6{67QM-iQ7Mju|nT zi{P!)A50{Yi_ORF*39RuHvz4*pG~ipZ5ZlzMpvo!1br<^Ycsj~t8wbQ?)uu7N?*ND z&QwCQfHS3k1`* zg)(`|Ls`y5bKxke#zIi=><2B2_94iV+>Mpia5C-F_Gob_^7?$YBC83y6fXer(aF^{ z=hSb#oI$e)K9Av-e7%$T!cbh;kP2S}S6>BJu;hQfbRj(mj=gI^&5anq z>cU#gT+T}IM;7vzf_iy7Kgm4qzw-g@Kp!9x{8W6=@f%2N=#7F!H0Q{eYwH_s8=8}; zG7BxZaAuI6<|?eBiG?K0hYxM(8PdIIS2aB5ntv2`K;_Z%b(RQ!Di@TTm(E?cuyO8R zR!l*PeI`q?B#Vz18pui-D9|4L%s+DfdLdV!+G~1wpM_AW|2p zh>#sH28GpUeX{gY=cr{1h7y(ZKAB%Q^qU1vf<>nN-Bei^^VBR{dE#G_>KiTb+&7PU zZuFxPpYv_Lzjv8h>tznhaG2E0rp1i4DAq#Rg@TV z>rA?adb-r+$5!JJO09|VydCT6CEGf#DQy3#IklO#5wK4wpecUdfP0L83UXwB0z#mH zs@hyHomaeE!1{NJu<=kHsZ=6n|1Q05Re2CP!KZgGEPMPQ!%?MhhzHH$xvUeXO0Icz zZoh&L?REFbn279>175mbUup8<3uao$5R4|}(mAas-_RaWqnELRGhKQKnI$!8HD1ee zS5~F?Se4qZDU9&}!8uq8QlI92P-XVc5Lngm);zC4|G3z18HVOPsXUN#;FDb{odf3% zXH(*|TCjnBmCu&LK?nkXLnw_k-W?@VG+tQ{S@V3VBUb68P+n3YzLkwvZsWk_bwpzHEC#o=ap7T0u}Ow5hJ?5r;G$%qjn9;6v~5z1Q?pyc zP5BjTh#fO!*aNeKf$pARZ|Nyxp-kZ;HRYC4Ze+RN6qml85d&V-@G}v;90+fyVD^pd z$#_<1Jk(*OPF~Sc>q?j~9c?pk;U%$}X?%s!#*zA#ezuNpV;W0}B)K81rqjzsLbgn9 zU{GU!(aOLTaVnDd%}Q}roo{tdgFbMUT@fOZFwtQ@(H*$S} zFwwf>lWWwKlVs56Q1mXAnr9N$vax~!DW7T8%oPWdVN&u^WlxRMp5x&*&v*9M-YCV~ z1bXGSrQnr&k@((2IyD(^+xKwFXZFbGA}Lka@m8$H&QzL;%nLG>+`_ju9uCQVm0*dd z?7v1I9@rNOy{w~Y$m<^KA_#p^)Z^8=FOZLpsXYwsi%=K?rJA}0&Pi`GM0_#8tE{ZK zhRNYpH}rIOoF$j1$LNWRJ}Y;Tq{Zu$xk;^xR>D07lT6vs-r_^Jqlj`k+E1k= z6rRsAm{cM(6*$Pzo9R717$1y8XRYTx;m~l z%RooZS@9d7wRk1ZkBL*%d_y6RHFuxn^q$YJ$?pQcrhI!ZegmF}adOs7rFCs$X0zR!liKb(h%tt}msr@O%urRScsf}>+{IVH}r|k1V zK#@qV*5fx&fof6b`4%SBkrE`t6q|6DgCRsKw)}yZ_=Sf`-PCpBX}=k35*-&PXJZa$+GruN+KOKmIaP3u+b;lZ0%z#f4WmT^!N5of7^9 z#&-Q2ly1zMoS;eFr}7R!3EesL%0qUNnMGeitL0hk*|t0ooKfk5v(Hrq6V-ljLCq3X zo1i<_Bk?Aa+=tP+jE&-hDgqfK*^Cs6p7_ir_PwpRBvRk%WehWR}A2u^^uJ53o6>0jW`sf%dY!p?rK)(*}I*-G>^) zJ!3XYyH8#W+3reT(OQhZaeP)|U8D&(xjJQ_M7@uGjefY>m50PvzH#{t1gcq=cf6(7 zwrX|#s+8l6yt$_5>CM%eW1TkSZaY2Rx_W}TJ&kn{=w%{8hKByQ^_z`&rpvyzEU+H) z4l%l&BHxV>>I)isZ*^ASg;d+&nLBXT>%(VqReF3Slt+&UH;dw?Eno6y%t3KXN1h=<_P5{T zMzCf!r|!UXwTMtj5fl=-N=JAZy%a2V~8vdB0(HdB_G0GGMJ z$eZXqpBPGR{h*LnsId5BPzNR5=rzl$dmp@f1^HFA*o58ml<c0#EfPu)@V(r1-8#=jXZ`F{L0$HB z?ZDR4geDQ?F8)-Gv)8@rb-rVkdSklvz5)D#Nw4=>@5t)I$@hdKZ73 zO@DQ(*Mch=WJJ)}VV>rzEG#UA6sxl&m6}Ec-&hwl^e{6(%P06)vnR8QoXD$)G+op0 zR03SbUj)gFBhSkNI_y<#`DR&u`TP8Oev&EVbiJ}NefogL0&~g~C*Hv#I=bc|f0u&F zHb$%l*T}t4T}3iZWl8og`6FoazcLeEFZAzu42Z@z-5rGp4Jw-!zozlTG&kf=c%FuE zuBxxndY}t;l@CLkKBfNB!%J)mpgSsEGdYkBdf_N-lC(e-vXw&yt!dx~9+aa6?*<7BuLQlr_1xXR+ML^5z;}Y(1M7*Jjw) zp-2yhkbB*VUg5%XOBPIXvpsh8t3&;0_d_^_Q~azfPs4*A|WJbPw|05 zwf2)Q{OP8yrU*>!PMsCC4eIh#wbQNid8*$)@Zz&a{jB}VFg~ozvnk9#0cBonmrmJF zx{{j-kS-xbAaadJ;2y1iZ^lk-mp2b@Lo4Zyz?0#R-H+Z%sxfz%Yhy=YUmOWm|`O_?jlnCi0@n6B(S#w&LDGqwZ z36w@LXK^{UKKrCe;xq1j6s(jhAD3mKwgZmOPJ}#v#fJ`1KL88Drm;ivwb!-dcBjqu2QM*>4vmzT%hPX*<{pNpr4Ql* zFM77TNCM>ykDO20j1wF9v`4a(^zPF`!4n!?R|4fp_@}BCe_t3g@i^NWh@s z@`{IElEOyYE7+J^1R~E#Sg5S#UDqUAwS1U@yUCHL+wNibi@RxgDs@bKvtM85)X@Htl!VEdn)WRTkTSgxm_hD0E4=<+Plqw6iX33 zEZo78|K35g&wAt;27QHU0?g5V3=1GA$4=hcA=T=Gu#s+1uyDWtAo-KM3T73n!)I0tM{EKTU+A%K_s?9)Bbo2c_Itcm!mh;lmxIYVE{>=%3?+5m+>(2pQVQw5etpYGqZf37>zl$Ln{Y| z+QQ)R?Hm@DVxf`WJzs3!?88^s&AY5yg*n3L?#boZCX{~->*hx6S5~h72*P?FX2&fT z&d%abtCfJfI$e4BYfYHTy_7<}?w9n~afT+rui|3QaZ4L*a0nWPIabAqNQn_BMIRWO zvmh1zW&L7bkil&m=Ve5(DijQ6vzNw^_@=^2LG}bk8G#-{BxT=pA#wl;Q?^$c8l`zc zS4hPMy9!3N>#&nK^+T8RSF~2|_QhV? zxqj7qp{m_(UH+{-7}!uojK$i#wjzZU3kO~C{Q9!iQz=HlmpHV$*S^_L^{9Sg+1c1*?MacOId@m?9>1A;$yDk_}q0;0GF((M}fy1Z_ zjpnFI)t5C%IJ{u-a^d45mn#r`9cX+!eg3Qmm2{S8`*esQQh+S*f}mz zk(q~hcyotLQ=F5(Bjab;(Igek2W~(4V}^08n&SJrl-8c`tzc%1bG%d2oi1z) zshUx*M!olIMNbs^QX?9uO~}4tcRU9OnWZmu^2?szP`MLkHGW9ITK|Cuw26|2?^Az& zUd$bw{-CXymmJXAGmkjgX~D`W8MS=b<0T2L))#$7Q9VhJl2IT0?$^w-}308m30RU`9^u5wJu{h51wduBT?Kezi%!lu! z-R008bs+hF7O#z#%Jea~_6hsu0LKEbHRU8D|Mqxn^yWx6+(sg}$>Zj5vM?8NTsTOZ znSC|zvAb5D9&=%bw~3P_!cwU3brE*J?h{5w!P6PJp~Y`8JP(QwS{3~kB#bL{jz8G5 z=TC<5xCp0{$?S&?K;vL4mQTWIFt6n*zI}`)itN$*iCHq8;76p1>(b3#S<%{q#$8oM zUF;t=4?0{~rVW&%H>E;|s($}AzGU#CrKiRZS7W5Zxlg9pn~qzXwsG+}x{)eh;ddhS zMyXds`s#4){gDT`#VkW|E^oJ`raz`Y1rfD5o%%|+92QKkk?TR%$KVkOP;M@G1gmgS zB!FK?$|4dC2PE%^`Dc^AmouB>ucn-r{gz0mV-}9LDH8?e9hs8!T{iRYWQd_Nr}FH> z_S+iSW28fTRV4shqyU8e;CjSS*qSeICZ@$SRti?*kOIPsxjA+FTqQNURF;S~VwyCC zQSh8hHuoYk+=YZ&(@gpm?Q+Np0)IN`Q$B}UB$@2nq=%bkr$)7`XlNibmiix({4KM0 z^(E#@J>1Rw#0eedk8<+MH3nsRgf?MD75RssRtj;YK<%1gdz7l;&b=%Ah4Xb#fRa^- zk{zq8>=OT=DPws>$U-(F@1K+TH(>e1c2NzXF|+BuJQ8uj{`qMyJ*e5uU0IozS$sJH zP2DFlhwplT(a4spB6eKV7JG$_VGMf zKJAE=-xdHMOLBYJpIjCE-1RYbCo>=vW!*fbS%z{4S>6>8CcMzdnZ|!pVQhA}6Dl0C zyxNC>!P82c04w*M3Jr@S) zzqI`ExbzjNOhX=(yHx1Y6!eqxKobM;7lw6J5p708+0o_?%7ac+;Tw#{GD^0#zX1&p zLcT7TOuJF1ccVz{cD5$q^G0ol<;MY7@iNZ@mPF~~yM~bltlAb3rR0G~6NRy~+Ep9^ z)K986gL4o04c-oqPk|TW8M?0{#_#zn7cO5hTeK&sDDAN@_P>iNfHL-|#Fg|#c>b!p zw?H=%IvBjgDbsb2;WL>gy}=%RSubeW)C`*>Z3Y|55?x-pGmIrcU0z1dqDT4pBSP!vnSyVL+Pi+RM)PKzz%ACXso;ybQKaT&#i%o~eTdi+lwvMw1FTOX4zSnuCY>=? zaY=RQ5U3B2ZdzE9`L%&}!pf6#`(sYxjYDf?j9Zv63WLjBU;T_sz@YAMu`8X%I%)qH z4BoVtkx}>%o0S}u(7O!jD|+Lvu<3yQ#&L~|>#>L^L*H&=wXV9?!dS)< zVHs+B-05vh&!=? zz>v8?a3P$`(tVVcqH#;E)}2&AiARKCxrjivINGJz5V^<;iBNgikd0zWU5Y%BB>c< zK}js3oTydcCw+L9&2oOeAeTNmH?;{g|9p)7ZDNdrR}-!DrZKA-2CN3B^ul)OB~t&) zh>kgd+4=sv7ao;}YicO=cweUVya*@Gx>>A`b%h+(ynDnR>*;J&4mIA4{6#BdtMDsg zftJ(|RHTrS)1-_!;t+%UocamE_OXHJ7)?{~ClGSA{q+#U~b-*j(BK#l2cu%Uu4nY76 z0I>Alxf2BstJbRQG2&~8$t3qAk-&w?dVD9Ge*>S&YzYF_Yi)#Bd#>j@#B>$2yYiJn znPa?@y9lTf)r+3vHW$kE5aFd`92GiY5)C)pGkO2Lv3(OhJ|p2*eDhkB5tvoD%MyQ- zbE;dtzYOgZEU-|i7%Pe|K}OeVwO{UV1)`T%e<#^rA$zm-@dpl@B@LUPEX>F(ZUXfp z=Js7o4y{Er=pLQW$EL#afmX&W=?0r-5GITQjO5syzT8c{gGoP1ui(DBR#h2hU|<+f z)jQ4COgrDl-B5$v4Y>IHdBuNr@KHVI_%90hpfXQnu!b3@f-z$q z2DCpP5^u1v`>Au{4pMvxa$hOB4JxpiA9?o6=9Ke%%6=}ZT+&4e+%zA>5Ap0oc4XeqW1 literal 0 HcmV?d00001 diff --git a/system-server/tests/integration/test_oem_mode.tavern.yaml b/system-server/tests/integration/test_oem_mode.tavern.yaml index 399422c96b8..9778495c6c3 100644 --- a/system-server/tests/integration/test_oem_mode.tavern.yaml +++ b/system-server/tests/integration/test_oem_mode.tavern.yaml @@ -1,5 +1,5 @@ --- -test_name: PUT Enable OEM Mode +test_name: Test enable/disable OEM Mode marks: - usefixtures: - run_server @@ -34,4 +34,91 @@ stages: content-type: application/json response: status_code: 422 +--- +test_name: Upload, and validate a good image for OEM Mode + +marks: + - usefixtures: + - run_server +stages: + - name: Enable OEM Mode + request: + url: "{host:s}:{port:d}/system/oem_mode/enable" + method: PUT + json: + "enable": true + - name: Upload PNG Image + request: &upload_splash_first + url: "{host:s}:{port:d}/system/oem_mode/upload_splash" + method: POST + files: + file: 'tests/integration/resources/oem_mode_custom.png' + response: + status_code: 201 + +--- +test_name: Dont process upload_splash request if oem mode is disabled + +marks: + - usefixtures: + - run_server + +stages: + - name: Disable OEM Mode + request: + url: "{host:s}:{port:d}/system/oem_mode/enable" + method: PUT + json: + "enable": false + - name: Upload PNG Image + request: + url: "{host:s}:{port:d}/system/oem_mode/upload_splash" + method: POST + files: + file: 'tests/integration/resources/oem_mode_custom.png' + response: + status_code: 403 + - name: Enable OEM Mode + request: + url: "{host:s}:{port:d}/system/oem_mode/enable" + method: PUT + json: + "enable": true + - name: Upload PNG Image + request: + url: "{host:s}:{port:d}/system/oem_mode/upload_splash" + method: POST + files: + file: 'tests/integration/resources/oem_mode_custom.png' + response: + status_code: 201 +--- +test_name: Validate the image before processing + +marks: + - usefixtures: + - run_server +stages: + - name: Enable OEM Mode + request: + url: "{host:s}:{port:d}/system/oem_mode/enable" + method: PUT + json: + "enable": true + - name: Upload non-PNG Image + request: + url: "{host:s}:{port:d}/system/oem_mode/upload_splash" + method: POST + files: + file: 'tests/integration/resources/oem_mode_wrong_image_type.jpeg' + response: + status_code: 415 + - name: Upload a PNG Image + request: + url: "{host:s}:{port:d}/system/oem_mode/upload_splash" + method: POST + files: + file: 'tests/integration/resources/oem_mode_custom.png' + response: + status_code: 201 From f8621b82b51c3aacfaafff932d530bd454cb4d89 Mon Sep 17 00:00:00 2001 From: Jethary Rader <66035149+jerader@users.noreply.github.com> Date: Fri, 12 Apr 2024 16:36:35 -0400 Subject: [PATCH 114/194] refactor(protocol-designer): export modal to require app 7.3.0 or higher (#14890) closes AUTH-340 --- protocol-designer/cypress/integration/migrations.spec.js | 2 +- .../src/components/FileSidebar/FileSidebar.tsx | 8 ++++---- protocol-designer/src/localization/en/alert.json | 4 ++-- protocol-designer/src/localization/en/modal.json | 2 +- protocol-designer/src/tutorial/index.ts | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/protocol-designer/cypress/integration/migrations.spec.js b/protocol-designer/cypress/integration/migrations.spec.js index 303c7b91701..4339f40be5f 100644 --- a/protocol-designer/cypress/integration/migrations.spec.js +++ b/protocol-designer/cypress/integration/migrations.spec.js @@ -127,7 +127,7 @@ describe('Protocol fixtures migrate and match snapshots', () => { cy.get('div') .contains( - 'This protocol can only run on app and robot server version 7.2.0 or higher' + 'This protocol can only run on app and robot server version 7.3.0 or higher' ) .should('exist') cy.get('button').contains('continue', { matchCase: false }).click() diff --git a/protocol-designer/src/components/FileSidebar/FileSidebar.tsx b/protocol-designer/src/components/FileSidebar/FileSidebar.tsx index 31bdfa60723..11b8d21053d 100644 --- a/protocol-designer/src/components/FileSidebar/FileSidebar.tsx +++ b/protocol-designer/src/components/FileSidebar/FileSidebar.tsx @@ -237,9 +237,9 @@ export function v8WarningContent(t: any): JSX.Element { return (

    - {t(`hint.export_v8_1_protocol_7_2.body1`)}{' '} - {t(`hint.export_v8_1_protocol_7_2.body2`)} - {t(`hint.export_v8_1_protocol_7_2.body3`)} + {t(`hint.export_v8_1_protocol_7_3.body1`)}{' '} + {t(`hint.export_v8_1_protocol_7_3.body2`)} + {t(`hint.export_v8_1_protocol_7_3.body3`)}

    ) @@ -350,7 +350,7 @@ export function FileSidebar(): JSX.Element { content: React.ReactNode } => { return { - hintKey: 'export_v8_1_protocol_7_2', + hintKey: 'export_v8_1_protocol_7_3', content: v8WarningContent(t), } } diff --git a/protocol-designer/src/localization/en/alert.json b/protocol-designer/src/localization/en/alert.json index 999c43500b0..b17e1028e3b 100644 --- a/protocol-designer/src/localization/en/alert.json +++ b/protocol-designer/src/localization/en/alert.json @@ -53,10 +53,10 @@ "title": "Missing labware", "body": "One or more module has no labware on it. We recommend you add labware before proceeding" }, - "export_v8_1_protocol_7_2": { + "export_v8_1_protocol_7_3": { "title": "Robot and app update may be required", "body1": "This protocol can only run on app and robot server version", - "body2": "7.2.0 or higher", + "body2": "7.3.0 or higher", "body3": ". Please ensure your robot is updated to the correct version." }, "change_magnet_module_model": { diff --git a/protocol-designer/src/localization/en/modal.json b/protocol-designer/src/localization/en/modal.json index 85bd89f4522..6d51439a828 100644 --- a/protocol-designer/src/localization/en/modal.json +++ b/protocol-designer/src/localization/en/modal.json @@ -49,7 +49,7 @@ "body3": "Adjust horizontal position within a well when aspirating, dispensing, or mixing.", "body4": "Assign up to three types of tip racks to a single pipette.", "body5": "Add multiple Temperature Modules to the deck (Flex only).", - "body6": "All protocols require {{app}} version 7.2.0 or later to run." + "body6": "All protocols require {{app}} version 7.3.0 or later to run." } }, "labware_selection": { diff --git a/protocol-designer/src/tutorial/index.ts b/protocol-designer/src/tutorial/index.ts index ecc17f49bb4..6d82f7832c9 100644 --- a/protocol-designer/src/tutorial/index.ts +++ b/protocol-designer/src/tutorial/index.ts @@ -11,7 +11,7 @@ type HintKey = // normal hints | 'waste_chute_warning' // blocking hints | 'custom_labware_with_modules' - | 'export_v8_1_protocol_7_2' + | 'export_v8_1_protocol_7_3' | 'change_magnet_module_model' // DEPRECATED HINTS (keep a record to avoid name collisions with old persisted dismissal states) // 'export_v4_protocol' From 40637dca9164817323277b9051e1cd1b11204167 Mon Sep 17 00:00:00 2001 From: Jamey Huffnagle Date: Fri, 12 Apr 2024 16:37:24 -0400 Subject: [PATCH 115/194] refactor(app): update Flex drop-tip modal copy (#14889) Closes EXEC-393 Flex automatic tip drop behavior at the end of a run is different from the OT-2: it keeps tips attached while the OT-2 drops them. The drop-tip wizard entry modals do not reflect this per copy, so update the copy. --- .../assets/localization/en/run_details.json | 5 +++-- .../RunningProtocol/ConfirmCancelRunModal.tsx | 2 +- .../__tests__/ConfirmCancelRunModal.test.tsx | 10 ++++------ .../RunDetails/ConfirmCancelModal.tsx | 9 +++++++-- .../__tests__/ConfirmCancelModal.test.tsx | 19 ++++++++++++++----- 5 files changed, 29 insertions(+), 16 deletions(-) diff --git a/app/src/assets/localization/en/run_details.json b/app/src/assets/localization/en/run_details.json index 53fbf0956ff..ed0fbbdc7e7 100644 --- a/app/src/assets/localization/en/run_details.json +++ b/app/src/assets/localization/en/run_details.json @@ -5,11 +5,12 @@ "anticipated": "Anticipated steps", "apply_stored_data": "Apply stored data", "apply_stored_labware_offset_data": "Apply stored Labware Offset data?", - "cancel_run_alert_info": "Doing so will terminate this run, drop any attached tips in the trash container and home your robot.", + "cancel_run_alert_info_flex": "Doing so will terminate this run and home your robot.", + "cancel_run_alert_info_ot2": "Doing so will terminate this run, drop any attached tips in the trash container, and home your robot.", "cancel_run_and_restart": "Cancel the run and restart setup to edit", "cancel_run_modal_back": "No, go back", "cancel_run_modal_confirm": "Yes, cancel run", - "cancel_run_modal_heading": "Are you sure you want to cancel this run?", + "cancel_run_modal_heading": "Are you sure you want to cancel?", "cancel_run_module_info": "Additionally, any hardware modules used within the protocol will remain active and maintain their current states until deactivated.", "cancel_run": "Cancel run", "canceling_run_dot": "canceling run...", diff --git a/app/src/organisms/OnDeviceDisplay/RunningProtocol/ConfirmCancelRunModal.tsx b/app/src/organisms/OnDeviceDisplay/RunningProtocol/ConfirmCancelRunModal.tsx index c2841101133..b29b81f76aa 100644 --- a/app/src/organisms/OnDeviceDisplay/RunningProtocol/ConfirmCancelRunModal.tsx +++ b/app/src/organisms/OnDeviceDisplay/RunningProtocol/ConfirmCancelRunModal.tsx @@ -98,7 +98,7 @@ export function ConfirmCancelRunModal({ paddingBottom={SPACING.spacing32} paddingTop={`${isActiveRun ? SPACING.spacing32 : '0'}`} > - {t('cancel_run_alert_info')} + {t('cancel_run_alert_info_flex')} {t('cancel_run_module_info')} { vi.restoreAllMocks() }) - it('should render text and buttons', () => { + it('should render correct text and buttons', () => { render(props) - screen.getByText('Are you sure you want to cancel this run?') - screen.getByText( - 'Doing so will terminate this run, drop any attached tips in the trash container and home your robot.' - ) + screen.getByText('Are you sure you want to cancel?') + screen.getByText('Doing so will terminate this run and home your robot.') screen.getByText( 'Additionally, any hardware modules used within the protocol will remain active and maintain their current states until deactivated.' ) @@ -111,7 +109,7 @@ describe('ConfirmCancelRunModal', () => { screen.getByText('Cancel run') }) - it('shoudler render the canceling run modal when run is dismissing', () => { + it('should render the canceling run modal when run is dismissing', () => { vi.mocked(useDismissCurrentRunMutation).mockReturnValue({ dismissCurrentRun: mockDismissCurrentRun, isLoading: true, diff --git a/app/src/organisms/RunDetails/ConfirmCancelModal.tsx b/app/src/organisms/RunDetails/ConfirmCancelModal.tsx index 172d8b15394..809ee0eee88 100644 --- a/app/src/organisms/RunDetails/ConfirmCancelModal.tsx +++ b/app/src/organisms/RunDetails/ConfirmCancelModal.tsx @@ -22,7 +22,7 @@ import { useStopRunMutation } from '@opentrons/react-api-client' import { getModalPortalEl } from '../../App/portal' import { LegacyModal } from '../../molecules/LegacyModal' -import { useTrackProtocolRunEvent } from '../Devices/hooks' +import { useTrackProtocolRunEvent, useIsFlex } from '../Devices/hooks' import { useRunStatus } from '../RunTimeControl/hooks' import { ANALYTICS_PROTOCOL_RUN_CANCEL } from '../../redux/analytics' @@ -39,9 +39,14 @@ export function ConfirmCancelModal( const { stopRun } = useStopRunMutation() const [isCanceling, setIsCanceling] = React.useState(false) const runStatus = useRunStatus(runId) + const isFlex = useIsFlex(robotName) const { trackProtocolRunEvent } = useTrackProtocolRunEvent(runId, robotName) const { t } = useTranslation('run_details') + const cancelRunAlertInfo = isFlex + ? t('cancel_run_alert_info_flex') + : t('cancel_run_alert_info_ot2') + const cancelRun: React.MouseEventHandler = (e): void => { e.preventDefault() e.stopPropagation() @@ -72,7 +77,7 @@ export function ConfirmCancelModal( > - {t('cancel_run_alert_info')} + {cancelRunAlertInfo} {t('cancel_run_module_info')} diff --git a/app/src/organisms/RunDetails/__tests__/ConfirmCancelModal.test.tsx b/app/src/organisms/RunDetails/__tests__/ConfirmCancelModal.test.tsx index 872a23b8daa..92fed1f5e4f 100644 --- a/app/src/organisms/RunDetails/__tests__/ConfirmCancelModal.test.tsx +++ b/app/src/organisms/RunDetails/__tests__/ConfirmCancelModal.test.tsx @@ -11,7 +11,10 @@ import { import { useStopRunMutation } from '@opentrons/react-api-client' import { i18n } from '../../../i18n' -import { useTrackProtocolRunEvent } from '../../../organisms/Devices/hooks' +import { + useIsFlex, + useTrackProtocolRunEvent, +} from '../../../organisms/Devices/hooks' import { useTrackEvent } from '../../../redux/analytics' import { renderWithProviders } from '../../../__testing-utils__' import { ConfirmCancelModal } from '../../../organisms/RunDetails/ConfirmCancelModal' @@ -56,6 +59,7 @@ describe('ConfirmCancelModal', () => { when(useTrackProtocolRunEvent).calledWith(RUN_ID, ROBOT_NAME).thenReturn({ trackProtocolRunEvent: mockTrackProtocolRunEvent, }) + vi.mocked(useIsFlex).mockReturnValue(true) props = { onClose: vi.fn(), runId: RUN_ID, robotName: ROBOT_NAME } }) @@ -66,15 +70,20 @@ describe('ConfirmCancelModal', () => { it('should render the correct title', () => { render(props) - screen.getByText('Are you sure you want to cancel this run?') + screen.getByText('Are you sure you want to cancel?') }) - it('should render the correct body', () => { + it('should render the correct body text for a Flex', () => { render(props) + screen.getByText('Doing so will terminate this run and home your robot.') screen.getByText( - 'Doing so will terminate this run, drop any attached tips in the trash container and home your robot.' + 'Additionally, any hardware modules used within the protocol will remain active and maintain their current states until deactivated.' ) + }) + it('should render correct alternative body text for an OT-2', () => { + vi.mocked(useIsFlex).mockReturnValue(false) + render(props) screen.getByText( - 'Additionally, any hardware modules used within the protocol will remain active and maintain their current states until deactivated.' + 'Doing so will terminate this run, drop any attached tips in the trash container, and home your robot.' ) }) it('should render both buttons', () => { From 3142bc2f7b924f62005190ffb6ce041d73b40d42 Mon Sep 17 00:00:00 2001 From: Ed Cormany Date: Fri, 12 Apr 2024 17:31:21 -0400 Subject: [PATCH 116/194] refactor(app): anonymize all app strings (#14884) # Overview Fully anonymize all localization strings and finalize copy. Addresses PLAT-243 # Test Plan None. Unit tests to come later. # Changelog - Removed nearly all mentions of "Opentrons" in strings. (Only one left is for OT-2 tip length calibration.) - Some very light edits elsewhere, also updated in "branded" strings. # Risk assessment minor, but it's not trivial to QA all of these changes in situ --- app/src/assets/localization/en/anonymous.json | 90 +++++++++---------- app/src/assets/localization/en/branded.json | 10 +-- .../ShowLabwareOffsetSnippets.test.tsx | 2 +- .../__tests__/SecureLabwareModal.test.tsx | 4 +- .../__tests__/DeviceResetModal.test.tsx | 2 +- .../__tests__/DisconnectModal.test.tsx | 2 +- 6 files changed, 55 insertions(+), 55 deletions(-) diff --git a/app/src/assets/localization/en/anonymous.json b/app/src/assets/localization/en/anonymous.json index ac288115d49..2bb4f67a4d7 100644 --- a/app/src/assets/localization/en/anonymous.json +++ b/app/src/assets/localization/en/anonymous.json @@ -1,72 +1,72 @@ { - "a_robot_software_update_is_available": "A robot software update is required to run protocols with this version of the Opentrons App. Go to Robot", + "a_robot_software_update_is_available": "A robot software update is required to run protocols with this version of the desktop app. Go to Robot", "about_flex_gripper": "About Gripper", - "alternative_security_types_description": "The robot supports connecting to various enterprise access points. Connect via USB and finish setup in the robot's app.", - "calibration_block_description": "This block is a specially made tool that fits perfectly on your deck and helps with calibration.If you do not have a Calibration Block, please email support@opentrons.com so we can send you one. In your message, be sure to include your name, company or institution name, and shipping address. While you wait for the block to arrive, you can use the flat surface on the trash bin of your robot instead.", - "calibration_on_opentrons_tips_is_important": "It’s extremely important to perform this calibration using the Opentrons tips and tip racks specified above, as the robot determines accuracy based on the known measurements of these tips.", + "alternative_security_types_description": "The robot supports connecting to various enterprise access points. Connect via USB and finish setup in the desktop app.", + "calibration_block_description": "This block is a specially made tool that fits perfectly on your deck and helps with calibration.If you do not have a Calibration Block, please email support so we can send you one. In your message, be sure to include your name, company or institution name, and shipping address. While you wait for the block to arrive, you can use the flat surface on the trash bin of your robot instead.", + "calibration_on_opentrons_tips_is_important": "It’s extremely important to perform this calibration using the tips and tip racks specified above, as the robot determines accuracy based on the known measurements of these tips.", "choose_what_data_to_share": "Choose what robot data to share.", - "computer_in_app_is_controlling_robot": "A computer with the Opentrons App is currently controlling this robot.", - "confirm_terminate": "This will immediately stop the activity begun on a computer. You, or another user, may lose progress or see an error in the Opentrons App.", - "connect_and_screw_in_gripper": "Connect and secure Gripper", - "connect_via_usb_description_3": "3. Launch the robot's desktop app on your computer to continue.", + "computer_in_app_is_controlling_robot": "A network-connected computer is currently controlling this robot.", + "confirm_terminate": "This will immediately stop the activity begun on a computer. You, or another user, may lose progress or see an error on that computer.", + "connect_and_screw_in_gripper": "Connect and secure gripper", + "connect_via_usb_description_3": "3. Launch the robot app on the connected computer to continue.", "connection_description_usb": "Connect directly to a computer.", - "connection_lost_description": "The Opentrons App is unable to communicate with this robot right now. Double check the USB or Wifi connection to the robot, then try to reconnect.", + "connection_lost_description": "The app is unable to communicate with this robot right now. Double check the USB or Wi-Fi connection to the robot and then try to reconnect.", "contact_information": "Contact support for assistance.", - "contact_support_for_connection_help": "If none of these work, contact Opentrons Support for help (via the question mark link in this app, or by emailing {{support_email}}.)", + "contact_support_for_connection_help": "If none of these work, contact support for help (via the question mark link in this app, or by emailing {{support_email}}.)", "deck_fixture_setup_modal_bottom_description": "For details on installing different fixture types, contact support.", - "delete_protocol_from_app": "Delete the protocol, make changes to address the error, and resend the protocol to this robot from the robot's app.", + "delete_protocol_from_app": "Delete the protocol, make changes to address the error, and resend the protocol to this robot from the desktop app.", "error_boundary_description": "You need to restart the touchscreen. Contact support for assistance.", "estop_pressed_description": "First, safely clear the deck of any labware or spills. Then, twist the E-stop button clockwise. Finally, have the robot move the gantry to its home position.", - "find_your_robot": "Find your robot in the Opentrons App to install software updates.", + "find_your_robot": "Find your robot in the Devices section of the app to install software updates.", "firmware_update_download_logs": "Contact support for assistance.", - "general_error_message": "If you keep getting this message, try restarting your app and/or robot. If this does not resolve the issue please contact Opentrons Support.", + "general_error_message": "If you keep getting this message, try restarting your app and robot. If this does not resolve the issue, contact support.", "gripper_still_attached": "Gripper still attached", "gripper_successfully_attached_and_calibrated": "Gripper successfully attached and calibrated", "gripper_successfully_calibrated": "Gripper successfully calibrated", "gripper_successfully_detached": "Gripper successfully detached", "gripper": "Gripper", - "ip_description_second": "Opentrons recommends working with your network administrator to assign a static IP address to the robot.", - "learn_uninstalling": "Learn more about uninstalling the Opentrons App", - "loosen_screws_and_detach": "Loosen screws and detach Gripper", - "modal_instructions": "For step-by-step instructions on setting up your module, consult the Quickstart Guide that came in its box. You can also click the link below or scan the QR code to visit the modules section of the Opentrons Help Center.", - "module_calibration_failed": "Module calibration was unsuccessful. Make sure the calibration adapter is fully seated on the module and try again. If you still have trouble, contact Opentrons Support.{{error}}", - "module_calibration_get_started": "To get started, remove labware from the deck and clean up the working area to make the calibration easier. Also gather the needed equipment shown to the right.The calibration adapter came with your module. The pipette probe came with your robot's pipette.", - "module_error_contact_support": "Try powering the module off and on again. If the error persists, contact Opentrons Support.", + "ip_description_second": "Work with your network administrator to assign a static IP address to the robot.", + "learn_uninstalling": "Learn more about uninstalling the app", + "loosen_screws_and_detach": "Loosen screws and detach gripper", + "modal_instructions": "For step-by-step instructions on setting up your module, consult the Quickstart Guide that came in its box.", + "module_calibration_failed": "Module calibration was unsuccessful. Make sure the calibration adapter is fully seated on the module and try again. If you still have trouble, contact support.{{error}}", + "module_calibration_get_started": "To get started, remove labware from the deck and clean up the working area to make the calibration easier. Also gather the needed equipment shown to the right.The calibration adapter came with your module. The pipette probe came with your pipette.", + "module_error_contact_support": "Try powering the module off and on again. If the error persists, contact support.", "network_setup_menu_description": "You’ll use this connection to run software updates and load protocols onto your robot.", - "opentrons_app_successfully_updated": "The Opentrons App was successfully updated.", - "opentrons_app_update": "Opentrons App update", - "opentrons_app_update_available": "Opentrons App Update Available", - "opentrons_app_update_available_variation": "An Opentrons App update is available.", - "opentrons_app_will_use_interpreter": "If specified, the Opentrons App will use the Python interpreter at this path instead of the default bundled Python interpreter.", + "opentrons_app_successfully_updated": "The app was successfully updated.", + "opentrons_app_update": "app update", + "opentrons_app_update_available": "App Update Available", + "opentrons_app_update_available_variation": "An app update is available.", + "opentrons_app_will_use_interpreter": "If specified, the app will use the Python interpreter at this path instead of the default bundled Python interpreter.", "opentrons_cares_about_privacy": "We care about your privacy. We anonymize all data and only use it to improve our products.", - "opentrons_def": "Opentrons Definition", - "opentrons_labware_def": "Opentrons labware definition", + "opentrons_def": "Verified Definition", + "opentrons_labware_def": "Verified labware definition", "opentrons_tip_racks_recommended": "Opentrons tip racks are highly recommended. Accuracy cannot be guaranteed with other tip racks.", "opentrons_tip_rack_name": "opentrons", - "previous_releases": "View previous Opentrons releases", - "receive_alert": "Receive an alert when an Opentrons software update is available.", - "restore_description": "Opentrons does not recommend reverting to previous software versions, but you can access previous releases below. For best results, uninstall the existing app and remove its configuration files before installing the previous version.", + "previous_releases": "View previous releases", + "receive_alert": "Receive an alert when a software update is available.", + "restore_description": "Reverting to previous software versions is not recommended, but you can access previous releases below. For best results, uninstall the existing app and remove its configuration files before installing the previous version.", "robot_server_version_ot3_description": "The robot software includes the robot server and the touchscreen display interface.", - "robot_software_update_required": "A robot software update is required to run protocols with this version of the Opentrons App.", + "robot_software_update_required": "A robot software update is required to run protocols with this version of the app.", "run_failed_modal_description_desktop": "Contact support for assistance.", - "secure_labware_explanation_magnetic_module": "Opentrons recommends ensuring your labware locks to the Magnetic Module by adjusting the black plate bracket on top of the module. Please note there are two sizes of plate brackets supplied with your module: standard and deep well. These brackets can be removed and swapped by unscrewing the modules thumb screw (the silver knob on the front).", - "secure_labware_explanation_thermocycler": "Opentrons recommends securing your labware to the Thermocycler module by closing its latch. Doing so ensures level and accurate plate placement for optimal results.", + "secure_labware_explanation_magnetic_module": "Ensure that your labware locks to the Magnetic Module by adjusting the black plate bracket on top of the module. There are two sizes of plate brackets supplied with your module: standard and deep well. These brackets can be removed and swapped by unscrewing the module's thumb screw (the silver knob on the front).", + "secure_labware_explanation_thermocycler": "Secure your labware to the Thermocycler Module by closing its latch. Doing so ensures level and accurate plate placement for optimal results.", "send_a_protocol_to_store": "Send a protocol to the robot to get started.", - "setup_instructions_description": "For step-by-step instructions on setting up your module, consult the Quickstart Guide that came in its box or scan the QR code to visit the modules section of the Opentrons Help Center.", - "share_app_analytics": "Share App Analytics with Opentrons", - "share_app_analytics_description": "Help Opentrons improve its products and services by automatically sending anonymous diagnostics and usage data.", + "setup_instructions_description": "For step-by-step instructions on setting up your module, consult the Quickstart Guide that came in its box.", + "share_app_analytics": "Share App Analytics", + "share_app_analytics_description": "Help improve this product by automatically sending anonymous diagnostics and usage data.", "share_display_usage_description": "Data on how you interact with the robot's touchscreen.", - "share_logs_with_opentrons": "Share Robot logs with Opentrons", - "share_logs_with_opentrons_description": "Help Opentrons improve its products and services by automatically sending anonymous robot logs. Opentrons uses these logs to troubleshoot robot issues and spot error trends.", - "show_labware_offset_snippets_description": "Only for users who need to apply Labware Offset data outside of the Opentrons App. When enabled, code snippets for Jupyter Notebook and SSH are available during protocol setup.", + "share_logs_with_opentrons": "Share robot logs", + "share_logs_with_opentrons_description": "Help improve this product by automatically sending anonymous robot logs. These logs are used to troubleshoot robot issues and spot error trends.", + "show_labware_offset_snippets_description": "Only for users who need to apply labware offset data outside of the app. When enabled, code snippets for Jupyter Notebook and SSH are available during protocol setup.", "something_seems_wrong": "There may be a problem with your pipette. Exit setup and contact support for assistance.", "these_are_advanced_settings": "These are advanced settings. Please do not attempt to adjust without assistance from support. Changing these settings may affect the lifespan of your pipette.These settings do not override any pipette settings defined in protocols.", - "update_requires_restarting_app": "Updating requires restarting the Opentrons App.", - "update_robot_software_description": "Bypass the Opentrons App auto-update process and update the robot software manually.", - "update_robot_software_link": "Launch Opentrons software update page", + "update_requires_restarting_app": "Updating requires restarting the app.", + "update_robot_software_description": "Bypass the auto-update process and update the robot software manually.", + "update_robot_software_link": "Launch software update page", "use_older_protocol_analysis_method_description": "Use an older, slower method of analyzing uploaded protocols. This changes how the OT-2 validates your protocol during the upload step, but does not affect how your protocol actually runs. Support might ask you to change this setting if you encounter problems with the newer, faster protocol analysis method.", - "versions_sync": "Learn more about keeping the Opentrons App and robot software in sync", - "want_to_help_out": "Want to help out Opentrons?", + "versions_sync": "Learn more about keeping the app and robot software in sync", + "want_to_help_out": "Want to help out?", "welcome_title": "Welcome!", - "why_use_lpc": "Labware Position Check is intended to correct for minor variances. Opentrons does not recommend using Labware Position Check to compensate for large positional adjustments. Needing to set large labware offsets could indicate a problem with robot calibration." + "why_use_lpc": "Labware Position Check is intended to correct for minor variances. Don't use Labware Position Check to compensate for large positional adjustments. Needing to set large labware offsets could indicate a problem with robot calibration." } diff --git a/app/src/assets/localization/en/branded.json b/app/src/assets/localization/en/branded.json index c28f2d9b3cc..6143400d541 100644 --- a/app/src/assets/localization/en/branded.json +++ b/app/src/assets/localization/en/branded.json @@ -10,7 +10,7 @@ "connect_and_screw_in_gripper": "Connect and secure Flex Gripper", "connect_via_usb_description_3": "3. Launch the Opentrons App on the computer to continue.", "connection_description_usb": "Connect directly to a computer (running the Opentrons App).", - "connection_lost_description": "The Opentrons App is unable to communicate with this robot right now. Double check the USB or Wifi connection to the robot, then try to reconnect.", + "connection_lost_description": "The Opentrons App is unable to communicate with this robot right now. Double check the USB or Wi-Fi connection to the robot, then try to reconnect.", "contact_information": "Download the robot logs from the Opentrons App and send it to support@opentrons.com for assistance.", "contact_support_for_connection_help": "If none of these work, contact Opentrons Support for help (via the question mark link in this app, or by emailing {{support_email}}.)", "deck_fixture_setup_modal_bottom_description": "For details on installing different fixture types, scan the QR code or search for “deck configuration” on support.opentrons.com", @@ -19,7 +19,7 @@ "estop_pressed_description": "First, safely clear the deck of any labware or spills. Then, twist the E-stop button clockwise. Finally, have Flex move the gantry to its home position.", "find_your_robot": "Find your robot in the Opentrons App to install software updates.", "firmware_update_download_logs": "Download the robot logs from the Opentrons App and send them to support@opentrons.com for assistance.", - "general_error_message": "If you keep getting this message, try restarting your app and/or robot. If this does not resolve the issue please contact Opentrons Support.", + "general_error_message": "If you keep getting this message, try restarting your app and robot. If this does not resolve the issue, contact Opentrons Support.", "gripper_still_attached": "Flex Gripper still attached", "gripper_successfully_attached_and_calibrated": "Flex Gripper successfully attached and calibrated", "gripper_successfully_calibrated": "Flex Gripper successfully calibrated", @@ -49,8 +49,8 @@ "robot_server_version_ot3_description": "The Opentrons Flex software includes the robot server and the touchscreen display interface.", "robot_software_update_required": "A robot software update is required to run protocols with this version of the Opentrons App.", "run_failed_modal_description_desktop": "Download the run log and send it to support@opentrons.com for assistance.", - "secure_labware_explanation_magnetic_module": "Opentrons recommends ensuring your labware locks to the Magnetic Module by adjusting the black plate bracket on top of the module. Please note there are two sizes of plate brackets supplied with your module: standard and deep well. These brackets can be removed and swapped by unscrewing the modules thumb screw (the silver knob on the front).", - "secure_labware_explanation_thermocycler": "Opentrons recommends securing your labware to the Thermocycler module by closing its latch. Doing so ensures level and accurate plate placement for optimal results.", + "secure_labware_explanation_magnetic_module": "Opentrons recommends ensuring your labware locks to the Magnetic Module by adjusting the black plate bracket on top of the module. There are two sizes of plate brackets supplied with your module: standard and deep well. These brackets can be removed and swapped by unscrewing the module's thumb screw (the silver knob on the front).", + "secure_labware_explanation_thermocycler": "Opentrons recommends securing your labware to the Thermocycler Module by closing its latch. Doing so ensures level and accurate plate placement for optimal results.", "send_a_protocol_to_store": "Send a protocol from the Opentrons App to get started.", "setup_instructions_description": "For step-by-step instructions on setting up your module, consult the Quickstart Guide that came in its box or scan the QR code to visit the modules section of the Opentrons Help Center.", "share_app_analytics": "Share App Analytics with Opentrons", @@ -58,7 +58,7 @@ "share_display_usage_description": "Data on how you interact with the touchscreen on Flex.", "share_logs_with_opentrons": "Share Robot logs with Opentrons", "share_logs_with_opentrons_description": "Help Opentrons improve its products and services by automatically sending anonymous robot logs. Opentrons uses these logs to troubleshoot robot issues and spot error trends.", - "show_labware_offset_snippets_description": "Only for users who need to apply Labware Offset data outside of the Opentrons App. When enabled, code snippets for Jupyter Notebook and SSH are available during protocol setup.", + "show_labware_offset_snippets_description": "Only for users who need to apply labware offset data outside of the Opentrons App. When enabled, code snippets for Jupyter Notebook and SSH are available during protocol setup.", "something_seems_wrong": "There may be a problem with your pipette. Exit setup and contact Opentrons Support for assistance.", "these_are_advanced_settings": "These are advanced settings. Please do not attempt to adjust without assistance from Opentrons Support. Changing these settings may affect the lifespan of your pipette.These settings do not override any pipette settings defined in protocols.", "update_requires_restarting_app": "Updating requires restarting the Opentrons App.", diff --git a/app/src/organisms/AdvancedSettings/__tests__/ShowLabwareOffsetSnippets.test.tsx b/app/src/organisms/AdvancedSettings/__tests__/ShowLabwareOffsetSnippets.test.tsx index 1d25cb58052..3353a497cc1 100644 --- a/app/src/organisms/AdvancedSettings/__tests__/ShowLabwareOffsetSnippets.test.tsx +++ b/app/src/organisms/AdvancedSettings/__tests__/ShowLabwareOffsetSnippets.test.tsx @@ -28,7 +28,7 @@ describe('ShowLabwareOffsetSnippets', () => { render() screen.getByText('Show Labware Offset data code snippets') screen.getByText( - 'Only for users who need to apply Labware Offset data outside of the Opentrons App. When enabled, code snippets for Jupyter Notebook and SSH are available during protocol setup.' + 'Only for users who need to apply labware offset data outside of the Opentrons App. When enabled, code snippets for Jupyter Notebook and SSH are available during protocol setup.' ) screen.getByRole('switch', { name: 'show_link_to_get_labware_offset_data' }) }) diff --git a/app/src/organisms/Devices/ProtocolRun/SetupLabware/__tests__/SecureLabwareModal.test.tsx b/app/src/organisms/Devices/ProtocolRun/SetupLabware/__tests__/SecureLabwareModal.test.tsx index 9372114973f..25c59b4e364 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupLabware/__tests__/SecureLabwareModal.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupLabware/__tests__/SecureLabwareModal.test.tsx @@ -27,7 +27,7 @@ describe('SecureLabwareModal', () => { 'Opentrons recommends ensuring your labware locks to the Magnetic Module by adjusting the black plate bracket on top of the module.' ) screen.getByText( - 'Please note there are two sizes of plate brackets supplied with your module: standard and deep well. These brackets can be removed and swapped by unscrewing the modules thumb screw (the silver knob on the front).' + "There are two sizes of plate brackets supplied with your module: standard and deep well. These brackets can be removed and swapped by unscrewing the module's thumb screw (the silver knob on the front)." ) }) it('should render magnetic module type modal and call onCloseClick when button is pressed', () => { @@ -43,7 +43,7 @@ describe('SecureLabwareModal', () => { render(props) screen.getByText('Securing labware to the Thermocycler') screen.getByText( - 'Opentrons recommends securing your labware to the Thermocycler module by closing its latch. Doing so ensures level and accurate plate placement for optimal results.' + 'Opentrons recommends securing your labware to the Thermocycler Module by closing its latch. Doing so ensures level and accurate plate placement for optimal results.' ) }) diff --git a/app/src/organisms/Devices/RobotSettings/AdvancedTab/AdvancedTabSlideouts/__tests__/DeviceResetModal.test.tsx b/app/src/organisms/Devices/RobotSettings/AdvancedTab/AdvancedTabSlideouts/__tests__/DeviceResetModal.test.tsx index 63cfd490c51..b741f3ef5c8 100644 --- a/app/src/organisms/Devices/RobotSettings/AdvancedTab/AdvancedTabSlideouts/__tests__/DeviceResetModal.test.tsx +++ b/app/src/organisms/Devices/RobotSettings/AdvancedTab/AdvancedTabSlideouts/__tests__/DeviceResetModal.test.tsx @@ -103,7 +103,7 @@ describe('RobotSettings DeviceResetModal', () => { }) screen.getByText('Connection to robot lost') screen.getByText( - 'The Opentrons App is unable to communicate with this robot right now. Double check the USB or Wifi connection to the robot, then try to reconnect.' + 'The Opentrons App is unable to communicate with this robot right now. Double check the USB or Wi-Fi connection to the robot, then try to reconnect.' ) screen.getByRole('button', { name: 'close' }) }) diff --git a/app/src/organisms/Devices/RobotSettings/ConnectNetwork/__tests__/DisconnectModal.test.tsx b/app/src/organisms/Devices/RobotSettings/ConnectNetwork/__tests__/DisconnectModal.test.tsx index adf1e9a591a..79823d81ef3 100644 --- a/app/src/organisms/Devices/RobotSettings/ConnectNetwork/__tests__/DisconnectModal.test.tsx +++ b/app/src/organisms/Devices/RobotSettings/ConnectNetwork/__tests__/DisconnectModal.test.tsx @@ -160,7 +160,7 @@ describe('DisconnectModal', () => { 'Your robot was unable to disconnect from Wi-Fi network foo.' ) screen.getByText( - 'If you keep getting this message, try restarting your app and/or robot. If this does not resolve the issue please contact Opentrons Support.' + 'If you keep getting this message, try restarting your app and robot. If this does not resolve the issue, contact Opentrons Support.' ) screen.getByRole('button', { name: 'cancel' }) screen.getByRole('button', { name: 'Disconnect' }) From fa13e3004f4e842ca8da357a097139b2e6ab189c Mon Sep 17 00:00:00 2001 From: Derek Maggio Date: Mon, 15 Apr 2024 08:09:31 -0700 Subject: [PATCH 117/194] feat(performance-metrics): add RobotContextTracker (#14862) # Overview Create RobotContextTracker class. This class provides a `track` method. The track method should be used as a decorator on either a function/method. It takes a RobotContextState enum value to label what state the RobotContextTracker is tracking. Uses `FunctionTimer` to measure execution time and stores results in a list. RobotContextTracker is defaulted not to track anything at all. To turn on tracking, instantiate the class with `should_track=True`. When not tracking, the `track` method calls the underlying wrapped function as quickly as possible. # Test Plan - See test_robot_contest_tracker.py # Changelog - Add RobotContextTracker class - Add test_robot_context_tracker.py # Review requests None # Risk assessment Low, not being used on any production code --- .../performance-metrics-test-lint.yaml | 54 ++++ performance-metrics/Makefile | 6 +- performance-metrics/Pipfile | 4 +- performance-metrics/Pipfile.lock | 21 +- .../src/performance_metrics/datashapes.py | 67 +++++ .../robot_context_tracker.py | 75 ++++++ .../test_robot_context_tracker.py | 251 ++++++++++++++++++ 7 files changed, 472 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/performance-metrics-test-lint.yaml create mode 100644 performance-metrics/src/performance_metrics/datashapes.py create mode 100644 performance-metrics/src/performance_metrics/robot_context_tracker.py create mode 100644 performance-metrics/tests/performance_metrics/test_robot_context_tracker.py diff --git a/.github/workflows/performance-metrics-test-lint.yaml b/.github/workflows/performance-metrics-test-lint.yaml new file mode 100644 index 00000000000..e57df828caf --- /dev/null +++ b/.github/workflows/performance-metrics-test-lint.yaml @@ -0,0 +1,54 @@ +# This workflow runs lint on pull requests that touch anything in the performance-metrics directory + +name: 'performance-metrics test & lint' + +on: + pull_request: + paths: + - 'performance-metrics/**' + - '.github/workflows/performance-metrics-test-lint.yaml' + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +defaults: + run: + shell: bash + +jobs: + lint: + name: 'performance-metrics test & lint' + timeout-minutes: 5 + runs-on: 'ubuntu-latest' + steps: + - name: Checkout opentrons repo + uses: 'actions/checkout@v4' + + - name: Setup Python + uses: 'actions/setup-python@v5' + with: + python-version: '3.10' + cache: 'pipenv' + cache-dependency-path: performance-metrics/Pipfile.lock + + - name: "Install Python deps" + uses: './.github/actions/python/setup' + with: + project: 'performance-metrics' + + - name: Setup + id: install + working-directory: ./performance-metrics + run: make setup + + - name: Test + if: always() && steps.install.outcome == 'success' || steps.install.outcome == 'skipped' + working-directory: ./performance-metrics + run: make test + + - name: Lint + if: always() && steps.install.outcome == 'success' || steps.install.outcome == 'skipped' + working-directory: ./performance-metrics + run: make lint diff --git a/performance-metrics/Makefile b/performance-metrics/Makefile index cce4fd7d93a..fd4dd421ad2 100644 --- a/performance-metrics/Makefile +++ b/performance-metrics/Makefile @@ -25,4 +25,8 @@ clean: .PHONY: wheel wheel: $(python) setup.py $(wheel_opts) bdist_wheel - rm -rf build \ No newline at end of file + rm -rf build + +.PHONY: test +test: + $(pytest) tests \ No newline at end of file diff --git a/performance-metrics/Pipfile b/performance-metrics/Pipfile index df5a3de89d6..a71db703e33 100644 --- a/performance-metrics/Pipfile +++ b/performance-metrics/Pipfile @@ -5,15 +5,17 @@ name = "pypi" [packages] opentrons-shared-data = {file = "../shared-data/python", editable = true} +performance-metrics = {file = ".", editable = true} [dev-packages] -pytest = "==7.2.2" +pytest = "==7.4.4" mypy = "==1.8.0" flake8 = "==7.0.0" flake8-annotations = "~=3.0.1" flake8-docstrings = "~=1.7.0" flake8-noqa = "~=1.4.0" black = "==22.3.0" +pytest-asyncio = "~=0.23.0" [requires] python_version = "3.10" diff --git a/performance-metrics/Pipfile.lock b/performance-metrics/Pipfile.lock index 61556f3dee9..5c836231b7e 100644 --- a/performance-metrics/Pipfile.lock +++ b/performance-metrics/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "fa95804888e2d45ce401c98bafc9b543cb6e1afe0a36713660d3f5517ac02b8e" + "sha256": "d811fa2b7dca8a5be8b2dba79ab7200243b2e10fb65f9ee221623f2710b24372" }, "pipfile-spec": 6, "requires": { @@ -37,6 +37,10 @@ "file": "../shared-data/python", "markers": "python_version >= '3.8'" }, + "performance-metrics": { + "editable": true, + "file": "." + }, "pydantic": { "hashes": [ "sha256:005655cabc29081de8243126e036f2065bd7ea5b9dff95fde6d2c642d39755de", @@ -333,12 +337,21 @@ }, "pytest": { "hashes": [ - "sha256:130328f552dcfac0b1cec75c12e3f005619dc5f874f0a06e8ff7263f0ee6225e", - "sha256:c99ab0c73aceb050f68929bc93af19ab6db0558791c6a0715723abe9d0ade9d4" + "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280", + "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8" ], "index": "pypi", "markers": "python_version >= '3.7'", - "version": "==7.2.2" + "version": "==7.4.4" + }, + "pytest-asyncio": { + "hashes": [ + "sha256:68516fdd1018ac57b846c9846b954f0393b26f094764a28c955eabb0536a4e8a", + "sha256:ffe523a89c1c222598c76856e76852b787504ddb72dd5d9b6617ffa8aa2cde5f" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==0.23.6" }, "snowballstemmer": { "hashes": [ diff --git a/performance-metrics/src/performance_metrics/datashapes.py b/performance-metrics/src/performance_metrics/datashapes.py new file mode 100644 index 00000000000..81b0234a723 --- /dev/null +++ b/performance-metrics/src/performance_metrics/datashapes.py @@ -0,0 +1,67 @@ +"""Defines data classes and enums used in the performance metrics module.""" + +from enum import Enum +import dataclasses +from typing import Tuple + + +class RobotContextState(Enum): + """Enum representing different states of a robot's operation context.""" + + STARTING_UP = 0, "STARTING_UP" + CALIBRATING = 1, "CALIBRATING" + ANALYZING_PROTOCOL = 2, "ANALYZING_PROTOCOL" + RUNNING_PROTOCOL = 3, "RUNNING_PROTOCOL" + SHUTTING_DOWN = 4, "SHUTTING_DOWN" + + def __init__(self, state_id: int, state_name: str) -> None: + self.state_id = state_id + self.state_name = state_name + + @classmethod + def from_id(cls, state_id: int) -> "RobotContextState": + """Returns the enum member matching the given state ID. + + Args: + state_id: The ID of the state to retrieve. + + Returns: + RobotContextStates: The enum member corresponding to the given ID. + + Raises: + ValueError: If no matching state is found. + """ + for state in RobotContextState: + if state.state_id == state_id: + return state + raise ValueError(f"Invalid state id: {state_id}") + + +@dataclasses.dataclass(frozen=True) +class RawContextData: + """Represents raw duration data with context state information. + + Attributes: + - function_start_time (int): The start time of the function. + - duration_measurement_start_time (int): The start time for duration measurement. + - duration_measurement_end_time (int): The end time for duration measurement. + - state (RobotContextStates): The current state of the context. + """ + + func_start: int + duration_start: int + duration_end: int + state: RobotContextState + + @classmethod + def headers(self) -> Tuple[str, str, str]: + """Returns the headers for the raw context data.""" + return ("state_id", "function_start_time", "duration") + + def csv_row(self) -> Tuple[int, int, int]: + """Returns the raw context data as a string.""" + return ( + self.state.state_id, + self.func_start, + self.duration_end - self.duration_start, + ) diff --git a/performance-metrics/src/performance_metrics/robot_context_tracker.py b/performance-metrics/src/performance_metrics/robot_context_tracker.py new file mode 100644 index 00000000000..188129046ff --- /dev/null +++ b/performance-metrics/src/performance_metrics/robot_context_tracker.py @@ -0,0 +1,75 @@ +"""Module for tracking robot context and execution duration for different operations.""" + +import csv +from pathlib import Path +import os + +from functools import wraps +from time import perf_counter_ns, clock_gettime_ns, CLOCK_REALTIME +from typing import Callable, TypeVar +from typing_extensions import ParamSpec +from collections import deque +from performance_metrics.datashapes import ( + RawContextData, + RobotContextState, +) + +P = ParamSpec("P") +R = TypeVar("R") + + +class RobotContextTracker: + """Tracks and stores robot context and execution duration for different operations.""" + + def __init__(self, storage_file_path: Path, should_track: bool = False) -> None: + """Initializes the RobotContextTracker with an empty storage list.""" + self._storage: deque[RawContextData] = deque() + self._storage_file_path = storage_file_path + self._should_track = should_track + + def track(self, state: RobotContextState) -> Callable: # type: ignore + """Decorator factory for tracking the execution duration and state of robot operations. + + Args: + state: The state to track for the decorated function. + + Returns: + Callable: A decorator that wraps a function to track its execution duration and state. + """ + + def inner_decorator(func: Callable[P, R]) -> Callable[P, R]: + if not self._should_track: + return func + + @wraps(func) + def wrapper(*args: P.args, **kwargs: P.kwargs) -> R: + function_start_time = clock_gettime_ns(CLOCK_REALTIME) + duration_start_time = perf_counter_ns() + try: + result = func(*args, **kwargs) + finally: + duration_end_time = perf_counter_ns() + self._storage.append( + RawContextData( + function_start_time, + duration_start_time, + duration_end_time, + state, + ) + ) + return result + + return wrapper + + return inner_decorator + + def store(self) -> None: + """Returns the stored context data and clears the storage list.""" + stored_data = self._storage.copy() + self._storage.clear() + rows_to_write = [context_data.csv_row() for context_data in stored_data] + os.makedirs(self._storage_file_path.parent, exist_ok=True) + with open(self._storage_file_path, "a") as storage_file: + writer = csv.writer(storage_file) + writer.writerow(RawContextData.headers()) + writer.writerows(rows_to_write) diff --git a/performance-metrics/tests/performance_metrics/test_robot_context_tracker.py b/performance-metrics/tests/performance_metrics/test_robot_context_tracker.py new file mode 100644 index 00000000000..d78d5054fe6 --- /dev/null +++ b/performance-metrics/tests/performance_metrics/test_robot_context_tracker.py @@ -0,0 +1,251 @@ +"""Tests for the RobotContextTracker class in performance_metrics.robot_context_tracker.""" + +import asyncio +from pathlib import Path +import pytest +from performance_metrics.robot_context_tracker import RobotContextTracker +from performance_metrics.datashapes import RobotContextState +from time import sleep + +# Corrected times in seconds +STARTING_TIME = 0.001 +CALIBRATING_TIME = 0.002 +ANALYZING_TIME = 0.003 +RUNNING_TIME = 0.004 +SHUTTING_DOWN_TIME = 0.005 + + +@pytest.fixture +def robot_context_tracker(tmp_path: Path) -> RobotContextTracker: + """Fixture to provide a fresh instance of RobotContextTracker for each test.""" + return RobotContextTracker(storage_file_path=tmp_path, should_track=True) + + +def test_robot_context_tracker(robot_context_tracker: RobotContextTracker) -> None: + """Tests the tracking of various robot context states through RobotContextTracker.""" + + @robot_context_tracker.track(state=RobotContextState.STARTING_UP) + def starting_robot() -> None: + sleep(STARTING_TIME) + + @robot_context_tracker.track(state=RobotContextState.CALIBRATING) + def calibrating_robot() -> None: + sleep(CALIBRATING_TIME) + + @robot_context_tracker.track(state=RobotContextState.ANALYZING_PROTOCOL) + def analyzing_protocol() -> None: + sleep(ANALYZING_TIME) + + @robot_context_tracker.track(state=RobotContextState.RUNNING_PROTOCOL) + def running_protocol() -> None: + sleep(RUNNING_TIME) + + @robot_context_tracker.track(state=RobotContextState.SHUTTING_DOWN) + def shutting_down_robot() -> None: + sleep(SHUTTING_DOWN_TIME) + + # Ensure storage is initially empty + assert ( + len(robot_context_tracker._storage) == 0 + ), "Storage should be initially empty." + + starting_robot() + calibrating_robot() + analyzing_protocol() + running_protocol() + shutting_down_robot() + + # Verify that all states were tracked + assert len(robot_context_tracker._storage) == 5, "All states should be tracked." + + # Validate the sequence and accuracy of tracked states + expected_states = [ + RobotContextState.STARTING_UP, + RobotContextState.CALIBRATING, + RobotContextState.ANALYZING_PROTOCOL, + RobotContextState.RUNNING_PROTOCOL, + RobotContextState.SHUTTING_DOWN, + ] + for i, state in enumerate(expected_states): + assert ( + RobotContextState.from_id(robot_context_tracker._storage[i].state.state_id) + == state + ), f"State at index {i} should be {state}." + + +def test_multiple_operations_single_state( + robot_context_tracker: RobotContextTracker, +) -> None: + """Tests tracking multiple operations within a single robot context state.""" + + @robot_context_tracker.track(state=RobotContextState.RUNNING_PROTOCOL) + def first_operation() -> None: + sleep(RUNNING_TIME) + + @robot_context_tracker.track(state=RobotContextState.RUNNING_PROTOCOL) + def second_operation() -> None: + sleep(RUNNING_TIME) + + first_operation() + second_operation() + + assert ( + len(robot_context_tracker._storage) == 2 + ), "Both operations should be tracked." + assert ( + robot_context_tracker._storage[0].state + == robot_context_tracker._storage[1].state + == RobotContextState.RUNNING_PROTOCOL + ), "Both operations should have the same state." + + +def test_exception_handling_in_tracked_function( + robot_context_tracker: RobotContextTracker, +) -> None: + """Ensures exceptions in tracked operations are handled correctly.""" + + @robot_context_tracker.track(state=RobotContextState.SHUTTING_DOWN) + def error_prone_operation() -> None: + sleep(SHUTTING_DOWN_TIME) + raise RuntimeError("Simulated operation failure") + + with pytest.raises(RuntimeError): + error_prone_operation() + + assert ( + len(robot_context_tracker._storage) == 1 + ), "Failed operation should still be tracked." + assert ( + robot_context_tracker._storage[0].state == RobotContextState.SHUTTING_DOWN + ), "State should be correctly logged despite the exception." + + +@pytest.mark.asyncio +async def test_async_operation_tracking( + robot_context_tracker: RobotContextTracker, +) -> None: + """Tests tracking of an asynchronous operation.""" + + @robot_context_tracker.track(state=RobotContextState.ANALYZING_PROTOCOL) + async def async_analyzing_operation() -> None: + await asyncio.sleep(ANALYZING_TIME) + + await async_analyzing_operation() + + assert ( + len(robot_context_tracker._storage) == 1 + ), "Async operation should be tracked." + assert ( + robot_context_tracker._storage[0].state == RobotContextState.ANALYZING_PROTOCOL + ), "State should be ANALYZING_PROTOCOL." + + +@pytest.mark.asyncio +async def test_async_operation_timing_accuracy( + robot_context_tracker: RobotContextTracker, +) -> None: + """Tests the timing accuracy of an async operation tracking.""" + + @robot_context_tracker.track(state=RobotContextState.RUNNING_PROTOCOL) + async def async_running_operation() -> None: + await asyncio.sleep(RUNNING_TIME) + + await async_running_operation() + + duration_data = robot_context_tracker._storage[0] + measured_duration = duration_data.duration_end - duration_data.duration_start + assert ( + abs(measured_duration - RUNNING_TIME * 1e9) < 1e7 + ), "Measured duration for async operation should closely match the expected duration." + + +@pytest.mark.asyncio +async def test_exception_in_async_operation( + robot_context_tracker: RobotContextTracker, +) -> None: + """Ensures exceptions in tracked async operations are correctly handled.""" + + @robot_context_tracker.track(state=RobotContextState.SHUTTING_DOWN) + async def async_error_prone_operation() -> None: + await asyncio.sleep(SHUTTING_DOWN_TIME) + raise RuntimeError("Simulated async operation failure") + + with pytest.raises(RuntimeError): + await async_error_prone_operation() + + assert ( + len(robot_context_tracker._storage) == 1 + ), "Failed async operation should still be tracked." + assert ( + robot_context_tracker._storage[0].state == RobotContextState.SHUTTING_DOWN + ), "State should be SHUTTING_DOWN despite the exception." + + +@pytest.mark.asyncio +async def test_concurrent_async_operations( + robot_context_tracker: RobotContextTracker, +) -> None: + """Tests tracking of concurrent async operations.""" + + @robot_context_tracker.track(state=RobotContextState.CALIBRATING) + async def first_async_calibrating() -> None: + await asyncio.sleep(CALIBRATING_TIME) + + @robot_context_tracker.track(state=RobotContextState.CALIBRATING) + async def second_async_calibrating() -> None: + await asyncio.sleep(CALIBRATING_TIME) + + await asyncio.gather(first_async_calibrating(), second_async_calibrating()) + + assert ( + len(robot_context_tracker._storage) == 2 + ), "Both concurrent async operations should be tracked." + assert all( + data.state == RobotContextState.CALIBRATING + for data in robot_context_tracker._storage + ), "All tracked operations should be in CALIBRATING state." + + +def test_no_tracking(tmp_path: Path) -> None: + """Tests that operations are not tracked when tracking is disabled.""" + robot_context_tracker = RobotContextTracker(tmp_path, should_track=False) + + @robot_context_tracker.track(state=RobotContextState.STARTING_UP) + def operation_without_tracking() -> None: + sleep(STARTING_TIME) + + operation_without_tracking() + + assert ( + len(robot_context_tracker._storage) == 0 + ), "Operation should not be tracked when tracking is disabled." + + +async def test_storing_to_file(tmp_path: Path) -> None: + """Tests storing the tracked data to a file.""" + file_path = tmp_path / "test_file.csv" + robot_context_tracker = RobotContextTracker(file_path, should_track=True) + + @robot_context_tracker.track(state=RobotContextState.STARTING_UP) + def starting_robot() -> None: + sleep(STARTING_TIME) + + @robot_context_tracker.track(state=RobotContextState.CALIBRATING) + def calibrating_robot() -> None: + sleep(CALIBRATING_TIME) + + @robot_context_tracker.track(state=RobotContextState.ANALYZING_PROTOCOL) + def analyzing_protocol() -> None: + sleep(ANALYZING_TIME) + + starting_robot() + calibrating_robot() + analyzing_protocol() + + robot_context_tracker.store() + + with open(file_path, "r") as file: + lines = file.readlines() + assert ( + len(lines) == 4 + ), "All stored data + header should be written to the file." From f0f3401d0c8af5881882c4bae4ed739bef5a840d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 15 Apr 2024 13:01:47 -0500 Subject: [PATCH 118/194] fix(app-testing): snapshot failure capture (#14897) This PR is an automated snapshot update request. Please review the changes and merge if they are acceptable or find you bug and fix it. Co-authored-by: y3rsh --- ...sis_snapshot[0f71566d05][OT2_P20S_None_2_7_Walkthrough].json | 2 +- ...t[20cefcac62][OT2_P300M_P20S_TC_HS_TM_2_13_SmokeTestV3].json | 2 +- ...ysis_snapshot[8df082e960][OT2_P300MLeft_MM_TM_2_4_Zymo].json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0f71566d05][OT2_P20S_None_2_7_Walkthrough].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0f71566d05][OT2_P20S_None_2_7_Walkthrough].json index d1786c8ca62..3d18e932a56 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0f71566d05][OT2_P20S_None_2_7_Walkthrough].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0f71566d05][OT2_P20S_None_2_7_Walkthrough].json @@ -3293,7 +3293,7 @@ "errorInfo": { "args": "()", "class": "AssertionError", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/protocol_engine.py\", line 503, in finish\n await exit_stack.aclose()\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 656, in aclose\n await self.__aexit__(None, None, None)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 714, in __aexit__\n raise exc_details[1]\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 697, in __aexit__\n cb_suppress = await cb(*exc_details)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 608, in _exit_wrapper\n await callback(*args, **kwds)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 102, in stop\n await p.teardown()\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 112, in teardown\n await self._action_dispatching_task\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 160, in _dispatch_all_actions\n self.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 37, in dispatch\n return self._action_dispatcher.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/actions/action_dispatcher.py\", line 30, in dispatch\n self._sink.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/state.py\", line 209, in handle_action\n substore.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/commands.py\", line 261, in handle_action\n self._state.command_history.set_command_running(running_command)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/command_history.py\", line 175, in set_command_running\n assert self.get_running_command() is None\n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/protocol_engine.py\", line 512, in finish\n await exit_stack.aclose()\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 656, in aclose\n await self.__aexit__(None, None, None)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 714, in __aexit__\n raise exc_details[1]\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 697, in __aexit__\n cb_suppress = await cb(*exc_details)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 608, in _exit_wrapper\n await callback(*args, **kwds)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 102, in stop\n await p.teardown()\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 112, in teardown\n await self._action_dispatching_task\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 160, in _dispatch_all_actions\n self.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 37, in dispatch\n return self._action_dispatcher.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/actions/action_dispatcher.py\", line 30, in dispatch\n self._sink.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/state.py\", line 209, in handle_action\n substore.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/commands.py\", line 261, in handle_action\n self._state.command_history.set_command_running(running_command)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/command_history.py\", line 175, in set_command_running\n assert self.get_running_command() is None\n" }, "errorType": "PythonException", "wrappedErrors": [] diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[20cefcac62][OT2_P300M_P20S_TC_HS_TM_2_13_SmokeTestV3].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[20cefcac62][OT2_P300M_P20S_TC_HS_TM_2_13_SmokeTestV3].json index d1aaa472fe9..ef9f55e77e7 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[20cefcac62][OT2_P300M_P20S_TC_HS_TM_2_13_SmokeTestV3].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[20cefcac62][OT2_P300M_P20S_TC_HS_TM_2_13_SmokeTestV3].json @@ -11889,7 +11889,7 @@ "errorInfo": { "args": "()", "class": "AssertionError", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/protocol_engine.py\", line 503, in finish\n await exit_stack.aclose()\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 656, in aclose\n await self.__aexit__(None, None, None)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 714, in __aexit__\n raise exc_details[1]\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 697, in __aexit__\n cb_suppress = await cb(*exc_details)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 608, in _exit_wrapper\n await callback(*args, **kwds)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 102, in stop\n await p.teardown()\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 112, in teardown\n await self._action_dispatching_task\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 160, in _dispatch_all_actions\n self.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 37, in dispatch\n return self._action_dispatcher.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/actions/action_dispatcher.py\", line 30, in dispatch\n self._sink.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/state.py\", line 209, in handle_action\n substore.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/commands.py\", line 261, in handle_action\n self._state.command_history.set_command_running(running_command)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/command_history.py\", line 175, in set_command_running\n assert self.get_running_command() is None\n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/protocol_engine.py\", line 512, in finish\n await exit_stack.aclose()\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 656, in aclose\n await self.__aexit__(None, None, None)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 714, in __aexit__\n raise exc_details[1]\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 697, in __aexit__\n cb_suppress = await cb(*exc_details)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 608, in _exit_wrapper\n await callback(*args, **kwds)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 102, in stop\n await p.teardown()\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 112, in teardown\n await self._action_dispatching_task\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 160, in _dispatch_all_actions\n self.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 37, in dispatch\n return self._action_dispatcher.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/actions/action_dispatcher.py\", line 30, in dispatch\n self._sink.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/state.py\", line 209, in handle_action\n substore.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/commands.py\", line 261, in handle_action\n self._state.command_history.set_command_running(running_command)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/command_history.py\", line 175, in set_command_running\n assert self.get_running_command() is None\n" }, "errorType": "PythonException", "wrappedErrors": [] diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8df082e960][OT2_P300MLeft_MM_TM_2_4_Zymo].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8df082e960][OT2_P300MLeft_MM_TM_2_4_Zymo].json index 0ccb1065979..d27e70c456f 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8df082e960][OT2_P300MLeft_MM_TM_2_4_Zymo].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8df082e960][OT2_P300MLeft_MM_TM_2_4_Zymo].json @@ -10913,7 +10913,7 @@ "errorInfo": { "args": "()", "class": "AssertionError", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/protocol_engine.py\", line 503, in finish\n await exit_stack.aclose()\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 656, in aclose\n await self.__aexit__(None, None, None)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 714, in __aexit__\n raise exc_details[1]\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 697, in __aexit__\n cb_suppress = await cb(*exc_details)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 608, in _exit_wrapper\n await callback(*args, **kwds)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 102, in stop\n await p.teardown()\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 112, in teardown\n await self._action_dispatching_task\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 160, in _dispatch_all_actions\n self.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 37, in dispatch\n return self._action_dispatcher.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/actions/action_dispatcher.py\", line 30, in dispatch\n self._sink.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/state.py\", line 209, in handle_action\n substore.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/commands.py\", line 261, in handle_action\n self._state.command_history.set_command_running(running_command)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/command_history.py\", line 175, in set_command_running\n assert self.get_running_command() is None\n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/protocol_engine.py\", line 512, in finish\n await exit_stack.aclose()\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 656, in aclose\n await self.__aexit__(None, None, None)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 714, in __aexit__\n raise exc_details[1]\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 697, in __aexit__\n cb_suppress = await cb(*exc_details)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 608, in _exit_wrapper\n await callback(*args, **kwds)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 102, in stop\n await p.teardown()\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 112, in teardown\n await self._action_dispatching_task\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 160, in _dispatch_all_actions\n self.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 37, in dispatch\n return self._action_dispatcher.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/actions/action_dispatcher.py\", line 30, in dispatch\n self._sink.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/state.py\", line 209, in handle_action\n substore.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/commands.py\", line 261, in handle_action\n self._state.command_history.set_command_running(running_command)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/command_history.py\", line 175, in set_command_running\n assert self.get_running_command() is None\n" }, "errorType": "PythonException", "wrappedErrors": [] From b8c08aa5aa8cddc2884916472efb81cc86c8bebd Mon Sep 17 00:00:00 2001 From: Sanniti Pimpley Date: Mon, 15 Apr 2024 14:45:38 -0400 Subject: [PATCH 119/194] refactor(robot-server): consolidate DB transactions, fix a max analyses length bug (#14904) Closes AUTH-347 # Overview #14885 added the feature to limit number of analyses we store in DB. In [this](https://github.com/Opentrons/opentrons/pull/14885#discussion_r1563058134) comment, @SyntaxColoring pointed out that we should consolidate the DB transactions for better performance, so that's what this PR does. Also fixes a bug where if the existing number of analyses in the DB was 3 and we were to add another analysis, then the formula for getting the analysis IDs to delete would result in `analysis_ids[:-1]` and it would delete all analyses except last one. # Test Plan - Tested the cases mentioned in #14885 - Tested the bug case # Risk assessment Low. Refactor + small bug fix --- .../robot_server/protocols/analysis_store.py | 2 +- .../protocols/completed_analysis_store.py | 48 ++++++++----------- .../tests/protocols/test_analysis_store.py | 2 +- .../test_completed_analysis_store.py | 32 ++++++++----- 4 files changed, 44 insertions(+), 40 deletions(-) diff --git a/robot-server/robot_server/protocols/analysis_store.py b/robot-server/robot_server/protocols/analysis_store.py index 60ea3d8d743..4f5b66ed4f8 100644 --- a/robot-server/robot_server/protocols/analysis_store.py +++ b/robot-server/robot_server/protocols/analysis_store.py @@ -196,7 +196,7 @@ async def update( completed_analysis ), ) - await self._completed_store.add( + await self._completed_store.make_room_and_add( completed_analysis_resource=completed_analysis_resource ) diff --git a/robot-server/robot_server/protocols/completed_analysis_store.py b/robot-server/robot_server/protocols/completed_analysis_store.py index 60780ab9cf4..5f72357050b 100644 --- a/robot-server/robot_server/protocols/completed_analysis_store.py +++ b/robot-server/robot_server/protocols/completed_analysis_store.py @@ -336,40 +336,34 @@ def get_ids_by_protocol(self, protocol_id: str) -> List[str]: return result_ids - async def add(self, completed_analysis_resource: CompletedAnalysisResource) -> None: - """Add a resource to the store.""" - self._make_room_for_new_analysis(completed_analysis_resource.protocol_id) - statement = analysis_table.insert().values( - await completed_analysis_resource.to_sql_values() - ) - with self._sql_engine.begin() as transaction: - transaction.execute(statement) - self._memcache.insert( - completed_analysis_resource.id, completed_analysis_resource - ) - - def _make_room_for_new_analysis(self, protocol_id: str) -> None: - """Remove the oldest analyses in store if the number of analyses exceed the max allowed. + async def make_room_and_add( + self, completed_analysis_resource: CompletedAnalysisResource + ) -> None: + """Make room and add a resource to the store. - Unlike protocols, protocol analysis IDs are not stored by any DB entities - other than the analysis store itself. So we do not have to worry about cleaning up - any other tables. + Removes the oldest analyses in store if the number of analyses exceed + the max allowed, and then adds the new analysis. """ - analyses_ids = self.get_ids_by_protocol(protocol_id) + analyses_ids = self.get_ids_by_protocol(completed_analysis_resource.protocol_id) # Delete all analyses exceeding max number allowed, # plus an additional one to create room for the new one. # Most existing databases will not have multiple extra analyses per protocol # but there would be some internally that added multiple analyses before # we started capping the number of analyses. - analyses_to_delete = analyses_ids[ - : len(analyses_ids) - MAX_ANALYSES_TO_STORE + 1 - ] - + analyses_to_delete = analyses_ids[: -MAX_ANALYSES_TO_STORE + 1] for analysis_id in analyses_to_delete: self._memcache.remove(analysis_id) - delete_statement = sqlalchemy.delete(analysis_table).where( - analysis_table.c.id == analysis_id - ) - with self._sql_engine.begin() as transaction: - transaction.execute(delete_statement) + delete_statement = analysis_table.delete().where( + analysis_table.c.id.in_(analyses_to_delete) + ) + + insert_statement = analysis_table.insert().values( + await completed_analysis_resource.to_sql_values() + ) + with self._sql_engine.begin() as transaction: + transaction.execute(delete_statement) + transaction.execute(insert_statement) + self._memcache.insert( + completed_analysis_resource.id, completed_analysis_resource + ) diff --git a/robot-server/tests/protocols/test_analysis_store.py b/robot-server/tests/protocols/test_analysis_store.py index 94d7f67f953..090cb680dfe 100644 --- a/robot-server/tests/protocols/test_analysis_store.py +++ b/robot-server/tests/protocols/test_analysis_store.py @@ -319,7 +319,7 @@ async def test_update_adds_rtp_values_and_defaults_to_completed_store( liquids=[], ) decoy.verify( - await mock_completed_store.add( + await mock_completed_store.make_room_and_add( completed_analysis_resource=expected_completed_analysis_resource ) ) diff --git a/robot-server/tests/protocols/test_completed_analysis_store.py b/robot-server/tests/protocols/test_completed_analysis_store.py index 438cf8baada..1cac25fb4e1 100644 --- a/robot-server/tests/protocols/test_completed_analysis_store.py +++ b/robot-server/tests/protocols/test_completed_analysis_store.py @@ -126,7 +126,7 @@ async def test_get_by_analysis_id_falls_back_to_sql( """It should return analyses from sql if they are not cached.""" resource = _completed_analysis_resource("analysis-id", "protocol-id") protocol_store.insert(make_dummy_protocol_resource("protocol-id")) - await subject.add(resource) + await subject.make_room_and_add(resource) # the analysis is not cached decoy.when(memcache.get("analysis-id")).then_raise(KeyError()) analysis_from_sql = await subject.get_by_id("analysis-id") @@ -143,7 +143,7 @@ async def test_get_by_analysis_id_stores_results_in_cache( """It should cache successful fetches from sql.""" resource = _completed_analysis_resource("analysis-id", "protocol-id") protocol_store.insert(make_dummy_protocol_resource("protocol-id")) - await subject.add(resource) + await subject.make_room_and_add(resource) # the analysis is not cached decoy.when(memcache.get("analysis-id")).then_raise(KeyError()) from_sql = await subject.get_by_id("analysis-id") @@ -158,7 +158,7 @@ async def test_get_by_analysis_id_as_document( """It should return the analysis serialized as a JSON string.""" resource = _completed_analysis_resource("analysis-id", "protocol-id") protocol_store.insert(make_dummy_protocol_resource("protocol-id")) - await subject.add(resource) + await subject.make_room_and_add(resource) result = await subject.get_by_id_as_document("analysis-id") assert result is not None assert json.loads(result) == { @@ -184,9 +184,9 @@ async def test_get_ids_by_protocol( resource_3 = _completed_analysis_resource("analysis-id-3", "protocol-id-2") protocol_store.insert(make_dummy_protocol_resource("protocol-id-1")) protocol_store.insert(make_dummy_protocol_resource("protocol-id-2")) - await subject.add(resource_1) - await subject.add(resource_2) - await subject.add(resource_3) + await subject.make_room_and_add(resource_1) + await subject.make_room_and_add(resource_2) + await subject.make_room_and_add(resource_3) assert subject.get_ids_by_protocol("protocol-id-1") == [ "analysis-id-1", "analysis-id-2", @@ -208,9 +208,9 @@ async def test_get_by_protocol( decoy.when(memcache.insert("analysis-id-1", resource_1)).then_return(None) decoy.when(memcache.insert("analysis-id-2", resource_2)).then_return(None) decoy.when(memcache.insert("analysis-id-3", resource_3)).then_return(None) - await subject.add(resource_1) - await subject.add(resource_2) - await subject.add(resource_3) + await subject.make_room_and_add(resource_1) + await subject.make_room_and_add(resource_2) + await subject.make_room_and_add(resource_3) decoy.when(memcache.get("analysis-id-1")).then_raise(KeyError()) decoy.when(memcache.get("analysis-id-2")).then_return(resource_2) decoy.when(memcache.contains("analysis-id-1")).then_return(False) @@ -257,7 +257,7 @@ async def test_get_rtp_values_and_defaults_by_analysis_from_db( }, ) protocol_store.insert(make_dummy_protocol_resource("protocol-id")) - await subject.add(resource) + await subject.make_room_and_add(resource) # Not in memcache decoy.when(memcache.get("analysis-id")).then_raise(KeyError()) result = await subject.get_rtp_values_and_defaults_by_analysis_id("analysis-id") @@ -297,10 +297,20 @@ async def test_get_rtp_values_and_defaults_by_analysis_from_db( "new-analysis-id", ], ), + ( + [f"analysis-id-{num}" for num in range(3)], + [ + "analysis-id-0", + "analysis-id-1", + "analysis-id-2", + "new-analysis-id", + ], + ), ( [f"analysis-id-{num}" for num in range(2)], ["analysis-id-0", "analysis-id-1", "new-analysis-id"], ), + (["analysis-id-0"], ["analysis-id-0", "new-analysis-id"]), ([], ["new-analysis-id"]), ], ) @@ -330,7 +340,7 @@ async def test_add_makes_room_for_new_analysis( transaction.execute(statement) assert subject.get_ids_by_protocol("protocol-id") == existing_analysis_ids - await subject.add( + await subject.make_room_and_add( _completed_analysis_resource( analysis_id="new-analysis-id", protocol_id="protocol-id", From 9cae291f25b9f7ef11cccf70f3a32e3e27931180 Mon Sep 17 00:00:00 2001 From: koji Date: Mon, 15 Apr 2024 15:02:29 -0400 Subject: [PATCH 120/194] refactor(components): refactor StyledText stories (#14899) * refactor(components): refactor StyledText stories --- .../atoms/StyledText/StyledText.stories.tsx | 134 ++++++++++-------- 1 file changed, 77 insertions(+), 57 deletions(-) diff --git a/components/src/atoms/StyledText/StyledText.stories.tsx b/components/src/atoms/StyledText/StyledText.stories.tsx index 12f8ab8c16a..388f7e79bdf 100644 --- a/components/src/atoms/StyledText/StyledText.stories.tsx +++ b/components/src/atoms/StyledText/StyledText.stories.tsx @@ -1,87 +1,107 @@ +/* eslint-disable storybook/prefer-pascal-case */ import * as React from 'react' -import { StyledText } from './' -import { TYPOGRAPHY } from '../../ui-style-constants' -import type { Story, Meta } from '@storybook/react' +import { SPACING, TYPOGRAPHY } from '../../ui-style-constants' +import { Flex } from '../../primitives' +import { StyledText } from './index' +import type { Meta, StoryObj } from '@storybook/react' -export default { +const meta: Meta = { title: 'Library/Atoms/StyledText', component: StyledText, -} as Meta + decorators: [ + Story => ( + + + + ), + ], +} + +export default meta -const Template: Story> = args => ( - -) +type Story = StoryObj const dummyText = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Purus sapien nunc dolor, aliquet nibh placerat et nisl, arcu. Pellentesque blandit sollicitudin vitae morbi morbi vulputate cursus tellus. Amet proin donec proin id aliquet in nullam.' -export const h1 = Template.bind({}) -h1.args = { - as: 'h1', - children: dummyText, +export const h1: Story = { + args: { + as: 'h1', + children: dummyText, + }, } -export const h2 = Template.bind({}) -h2.args = { - as: 'h2', - children: dummyText, +export const h2: Story = { + args: { + as: 'h2', + children: dummyText, + }, } -export const h3 = Template.bind({}) -h3.args = { - as: 'h3', - children: dummyText, +export const h3: Story = { + args: { + as: 'h3', + children: dummyText, + }, } -export const h6 = Template.bind({}) -h6.args = { - as: 'h6', - children: dummyText, +export const h6: Story = { + args: { + as: 'h6', + children: dummyText, + }, } -export const p = Template.bind({}) -p.args = { - as: 'p', - children: dummyText, +export const p: Story = { + args: { + as: 'p', + children: dummyText, + }, } -export const label = Template.bind({}) -label.args = { - as: 'label', - children: dummyText, +export const label: Story = { + args: { + as: 'label', + children: dummyText, + }, } -export const h2SemiBold = Template.bind({}) -h2SemiBold.args = { - as: 'h2', - fontWeight: TYPOGRAPHY.fontWeightSemiBold, - children: dummyText, +export const h2SemiBold: Story = { + args: { + as: 'h2', + fontWeight: TYPOGRAPHY.fontWeightSemiBold, + children: dummyText, + }, } -export const h3SemiBold = Template.bind({}) -h3SemiBold.args = { - as: 'h3', - fontWeight: TYPOGRAPHY.fontWeightSemiBold, - children: dummyText, +export const h3SemiBold: Story = { + args: { + as: 'h3', + fontWeight: TYPOGRAPHY.fontWeightSemiBold, + children: dummyText, + }, } -export const h6SemiBold = Template.bind({}) -h6SemiBold.args = { - as: 'h6', - fontWeight: TYPOGRAPHY.fontWeightSemiBold, - children: dummyText, +export const h6SemiBold: Story = { + args: { + as: 'h6', + fontWeight: TYPOGRAPHY.fontWeightSemiBold, + children: dummyText, + }, } -export const pSemiBold = Template.bind({}) -pSemiBold.args = { - as: 'p', - fontWeight: TYPOGRAPHY.fontWeightSemiBold, - children: dummyText, +export const pSemiBold: Story = { + args: { + as: 'p', + fontWeight: TYPOGRAPHY.fontWeightSemiBold, + children: dummyText, + }, } -export const labelSemiBold = Template.bind({}) -labelSemiBold.args = { - as: 'label', - fontWeight: TYPOGRAPHY.fontWeightSemiBold, - children: dummyText, +export const labelSemiBold: Story = { + args: { + as: 'label', + fontWeight: TYPOGRAPHY.fontWeightSemiBold, + children: dummyText, + }, } From 6f35979bb4e7385e514250e818dd024ecfc9dbdc Mon Sep 17 00:00:00 2001 From: koji Date: Mon, 15 Apr 2024 15:05:41 -0400 Subject: [PATCH 121/194] refactor(components): refactor location icon stories (#14896) * refactor(components): refactor location icon stories --- .../LocationIcon/LocationIcon.stories.tsx | 50 ++++++++----------- 1 file changed, 22 insertions(+), 28 deletions(-) diff --git a/components/src/molecules/LocationIcon/LocationIcon.stories.tsx b/components/src/molecules/LocationIcon/LocationIcon.stories.tsx index 70f9f556554..fb3fc001fd3 100644 --- a/components/src/molecules/LocationIcon/LocationIcon.stories.tsx +++ b/components/src/molecules/LocationIcon/LocationIcon.stories.tsx @@ -1,13 +1,11 @@ import * as React from 'react' - -import { Flex, SPACING } from '@opentrons/components' - +import { Flex } from '../../primitives' +import { SPACING } from '../../ui-style-constants' import { GlobalStyle } from '../../../../app/src/atoms/GlobalStyle' import { customViewports } from '../../../../.storybook/preview' -import { LocationIcon } from '.' - -import type { Story, Meta } from '@storybook/react' import { ICON_DATA_BY_NAME } from '../../icons' +import { LocationIcon } from '.' +import type { Meta, StoryObj } from '@storybook/react' const slots = [ 'A1', @@ -28,26 +26,23 @@ const slots = [ 'D4', ] -export default { - title: 'ODD/Molecules/LocationIcon', +const meta: Meta = { + title: 'Library/Molecules/LocationIcon', argTypes: { iconName: { control: { type: 'select', - options: Object.keys(ICON_DATA_BY_NAME), }, - defaultValue: undefined, + options: Object.keys(ICON_DATA_BY_NAME), }, slotName: { control: { type: 'select', - options: slots, }, - defaultValue: undefined, + options: slots, }, }, component: LocationIcon, - // Note (kk:08/29/2023) this component is located in components so avoid importing const from app parameters: { viewport: { viewports: customViewports, @@ -56,26 +51,25 @@ export default { }, decorators: [ Story => ( - <> + - + ), ], -} as Meta - -const Template: Story> = args => ( - - - -) +} +export default meta +type Story = StoryObj -export const DisplaySlot = Template.bind({}) -DisplaySlot.args = { - slotName: 'A1', +export const DisplaySlot: Story = { + args: { + slotName: 'A1', + iconName: undefined, + }, } -export const DisplayIcon = Template.bind({}) -DisplayIcon.args = { - iconName: 'ot-temperature-v2', +export const DisplayIcon: Story = { + args: { + iconName: 'ot-temperature-v2', + }, } From e5080a67783b765bcbe9e327d4de0fdc884caf57 Mon Sep 17 00:00:00 2001 From: koji Date: Mon, 15 Apr 2024 15:06:40 -0400 Subject: [PATCH 122/194] refactor(app): refactor externallink stories (#14895) * refactor(app): refactor externallink stories --- app/src/atoms/Link/ExternalLink.stories.tsx | 36 ++++++++++++--------- app/src/atoms/Link/ExternalLink.tsx | 9 ++---- 2 files changed, 22 insertions(+), 23 deletions(-) diff --git a/app/src/atoms/Link/ExternalLink.stories.tsx b/app/src/atoms/Link/ExternalLink.stories.tsx index c243304ee59..8f664d257f5 100644 --- a/app/src/atoms/Link/ExternalLink.stories.tsx +++ b/app/src/atoms/Link/ExternalLink.stories.tsx @@ -1,22 +1,26 @@ import * as React from 'react' -import { Flex, COLORS } from '@opentrons/components' -import { ExternalLink } from './ExternalLink' +import { COLORS, Flex, SPACING } from '@opentrons/components' +import { ExternalLink as ExternalLinkComponent } from './ExternalLink' -import type { Story, Meta } from '@storybook/react' +import type { Meta, StoryObj } from '@storybook/react' -export default { +const meta: Meta = { title: 'App/Atoms/ExternalLink', - component: ExternalLink, -} as Meta - -const Template: Story> = args => ( - - - -) + component: ExternalLinkComponent, + decorators: [ + Story => ( + + + + ), + ], +} +export default meta +type Story = StoryObj -export const Primary = Template.bind({}) -Primary.args = { - href: 'https://www.opentrons.com', - children: 'Open the link', +export const ExternalLink: Story = { + args: { + href: 'https://www.opentrons.com', + children: 'Open the link', + }, } diff --git a/app/src/atoms/Link/ExternalLink.tsx b/app/src/atoms/Link/ExternalLink.tsx index 4baa78afa44..e35e3515277 100644 --- a/app/src/atoms/Link/ExternalLink.tsx +++ b/app/src/atoms/Link/ExternalLink.tsx @@ -1,12 +1,7 @@ import * as React from 'react' -import { - Link, - LinkProps, - Icon, - TYPOGRAPHY, - SPACING, -} from '@opentrons/components' +import { Link, Icon, TYPOGRAPHY, SPACING } from '@opentrons/components' +import type { LinkProps } from '@opentrons/components' export interface ExternalLinkProps extends LinkProps { href: string From dc093fb69683deb2cdbe088355e47a22a7b5c143 Mon Sep 17 00:00:00 2001 From: koji Date: Mon, 15 Apr 2024 15:07:18 -0400 Subject: [PATCH 123/194] refactor(app): refactor banner component stories (#14894) * refactor(app): refactor banner component stories --- app/src/atoms/Banner/Banner.stories.tsx | 55 ++++++++++--------- .../atoms/Banner/__tests__/Banner.test.tsx | 52 ++++++++++-------- app/src/atoms/Banner/index.tsx | 3 +- 3 files changed, 60 insertions(+), 50 deletions(-) diff --git a/app/src/atoms/Banner/Banner.stories.tsx b/app/src/atoms/Banner/Banner.stories.tsx index deea5d236b4..0f3d6210075 100644 --- a/app/src/atoms/Banner/Banner.stories.tsx +++ b/app/src/atoms/Banner/Banner.stories.tsx @@ -1,36 +1,41 @@ import * as React from 'react' import { StyledText, TYPOGRAPHY } from '@opentrons/components' import { Banner } from './index' -import type { Story, Meta } from '@storybook/react' +import type { Meta, StoryObj } from '@storybook/react' -export default { +const meta: Meta = { title: 'App/Atoms/Banner', component: Banner, -} as Meta +} + +export default meta -const Template: Story> = args => ( - {'Banner component'} -) +type Story = StoryObj -export const Primary = Template.bind({}) -Primary.args = { - title: 'title', - type: 'success', +export const Primary: Story = { + args: { + children: 'Banner component', + type: 'success', + }, } -export const OverriddenIcon = Template.bind({}) -OverriddenIcon.args = { - type: 'warning', - title: 'Alert with overridden icon', - icon: { name: 'ot-hot-to-touch' }, + +export const OverriddenIcon: Story = { + args: { + type: 'warning', + children: 'Banner component', + icon: { name: 'ot-hot-to-touch' }, + }, } -export const OverriddenExitIcon = Template.bind({}) -OverriddenExitIcon.args = { - type: 'informing', - title: 'Alert with overriden exit icon', - onCloseClick: () => console.log('close'), - closeButton: ( - - {'Exit'} - - ), + +export const OverriddenExitIcon: Story = { + args: { + type: 'informing', + children: 'Banner component', + onCloseClick: () => console.log('close'), + closeButton: ( + + {'Exit'} + + ), + }, } diff --git a/app/src/atoms/Banner/__tests__/Banner.test.tsx b/app/src/atoms/Banner/__tests__/Banner.test.tsx index 126740f0c4b..f543ec98ec0 100644 --- a/app/src/atoms/Banner/__tests__/Banner.test.tsx +++ b/app/src/atoms/Banner/__tests__/Banner.test.tsx @@ -1,10 +1,9 @@ import * as React from 'react' import { describe, it, vi, expect, beforeEach } from 'vitest' -import '@testing-library/jest-dom/vitest' -import { fireEvent } from '@testing-library/react' +import { fireEvent, screen } from '@testing-library/react' +import { renderWithProviders } from '../../../__testing-utils__' import { i18n } from '../../../i18n' import { Banner } from '..' -import { renderWithProviders } from '../../../__testing-utils__' const render = (props: React.ComponentProps) => { return renderWithProviders(, { @@ -21,60 +20,67 @@ describe('Banner', () => { children: 'TITLE', } }) + it('renders success banner', () => { - const { getByText, getByLabelText } = render(props) - getByLabelText('icon_success') - getByText('TITLE') + render(props) + screen.getByLabelText('icon_success') + screen.getByText('TITLE') }) + it('renders success banner with exit button and when click dismisses banner', () => { props = { type: 'success', children: 'TITLE', onCloseClick: vi.fn(), } - const { getByText, getByLabelText } = render(props) - getByText('TITLE') - const btn = getByLabelText('close_icon') + render(props) + screen.getByText('TITLE') + const btn = screen.getByLabelText('close_icon') fireEvent.click(btn) expect(props.onCloseClick).toHaveBeenCalled() }) + it('renders warning banner', () => { props = { type: 'warning', children: 'TITLE', } - const { getByText, getByLabelText } = render(props) - getByLabelText('icon_warning') - getByText('TITLE') + render(props) + screen.getByLabelText('icon_warning') + screen.getByText('TITLE') }) + it('renders error banner', () => { props = { type: 'error', children: 'TITLE', } - const { getByText, getByLabelText } = render(props) - getByLabelText('icon_error') - getByText('TITLE') + render(props) + screen.getByLabelText('icon_error') + screen.getByText('TITLE') }) + it('renders updating banner', () => { props = { type: 'updating', children: 'TITLE', } - const { getByText, getByLabelText } = render(props) - getByLabelText('icon_updating') - getByText('TITLE') + render(props) + screen.getByLabelText('icon_updating') + screen.getByText('TITLE') }) + it('renders custom icon banner', () => { props = { type: 'warning', children: 'TITLE', icon: { name: 'ot-hot-to-touch' }, } - const { getByText, getByLabelText } = render(props) - getByLabelText('icon_warning') - getByText('TITLE') + render(props) + screen.getByLabelText('icon_warning') + screen.getByText('TITLE') }) + it('renders custom close', () => { props = { type: 'warning', @@ -82,8 +88,8 @@ describe('Banner', () => { closeButton: 'close button', onCloseClick: vi.fn(), } - const { getByText } = render(props) - const btn = getByText('close button') + render(props) + const btn = screen.getByText('close button') fireEvent.click(btn) expect(props.onCloseClick).toHaveBeenCalled() }) diff --git a/app/src/atoms/Banner/index.tsx b/app/src/atoms/Banner/index.tsx index a74fcf829ba..e7d2008521a 100644 --- a/app/src/atoms/Banner/index.tsx +++ b/app/src/atoms/Banner/index.tsx @@ -8,13 +8,12 @@ import { DIRECTION_ROW, Flex, Icon, - IconProps, JUSTIFY_SPACE_BETWEEN, RESPONSIVENESS, SPACING, TYPOGRAPHY, } from '@opentrons/components' -import type { StyleProps } from '@opentrons/components' +import type { IconProps, StyleProps } from '@opentrons/components' export type BannerType = | 'success' From e6769f3f2489642a9c521fc67fb19eeeb770b7e7 Mon Sep 17 00:00:00 2001 From: Seth Foster Date: Mon, 15 Apr 2024 15:27:06 -0400 Subject: [PATCH 124/194] fix(shared-data): correctly apply loadname regex (#14887) For some reason I changed this to be pattern= when updating pydantic, but that's still wrong and still needs to be regex. Closes EXEC-397 --- .../labware/labware_definition.py | 4 ++-- .../python/tests/labware/test_validations.py | 21 +++++++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) create mode 100644 shared-data/python/tests/labware/test_validations.py diff --git a/shared-data/python/opentrons_shared_data/labware/labware_definition.py b/shared-data/python/opentrons_shared_data/labware/labware_definition.py index 203dba1455d..1b2e68040de 100644 --- a/shared-data/python/opentrons_shared_data/labware/labware_definition.py +++ b/shared-data/python/opentrons_shared_data/labware/labware_definition.py @@ -160,7 +160,7 @@ class Parameters(BaseModel): loadName: str = Field( ..., description="Name used to reference a labware definition", - pattern=SAFE_STRING_REGEX, + regex=SAFE_STRING_REGEX, ) isMagneticModuleCompatible: bool = Field( ..., @@ -262,7 +262,7 @@ class LabwareDefinition(BaseModel): "(eg myPlate v1/v2/v3). An incrementing integer", ge=1.0, ) - namespace: str = Field(..., pattern=SAFE_STRING_REGEX) + namespace: str = Field(..., regex=SAFE_STRING_REGEX) metadata: Metadata = Field( ..., description="Properties used for search and display" ) diff --git a/shared-data/python/tests/labware/test_validations.py b/shared-data/python/tests/labware/test_validations.py new file mode 100644 index 00000000000..39052e5d150 --- /dev/null +++ b/shared-data/python/tests/labware/test_validations.py @@ -0,0 +1,21 @@ +import pytest + +from pydantic import ValidationError +from opentrons_shared_data.labware import load_definition +from opentrons_shared_data.labware.labware_definition import LabwareDefinition + +from . import get_ot_defs + + +def test_loadname_regex_applied() -> None: + defdict = load_definition(*get_ot_defs()[0]) + defdict["parameters"]["loadName"] = "ALSJHDAKJLA" + with pytest.raises(ValidationError): + LabwareDefinition.parse_obj(defdict) + + +def test_namespace_regex_applied() -> None: + defdict = load_definition(*get_ot_defs()[0]) + defdict["namespace"] = "ALSJHDAKJLA" + with pytest.raises(ValidationError): + LabwareDefinition.parse_obj(defdict) From 26e063b4626acb7088f75cf4be654760877569eb Mon Sep 17 00:00:00 2001 From: koji Date: Mon, 15 Apr 2024 15:55:11 -0400 Subject: [PATCH 125/194] feat(opentrons-ai-client): add prompt guide component (#14892) * feat(opentrons-ai-client): add prompt guide component --- .../localization/en/protocol_generator.json | 10 +- .../src/atoms/GlobalStyle/index.ts | 37 ++++++ .../PromptGuide/PromptGuide.stories.tsx | 21 ++++ .../__tests__/PromptGuide.test.tsx | 46 ++++++++ .../src/molecules/PromptGuide/index.tsx | 109 ++++++++++++++++++ .../src/molecules/SidePanel/index.tsx | 3 +- 6 files changed, 221 insertions(+), 5 deletions(-) create mode 100644 opentrons-ai-client/src/atoms/GlobalStyle/index.ts create mode 100644 opentrons-ai-client/src/molecules/PromptGuide/PromptGuide.stories.tsx create mode 100644 opentrons-ai-client/src/molecules/PromptGuide/__tests__/PromptGuide.test.tsx create mode 100644 opentrons-ai-client/src/molecules/PromptGuide/index.tsx diff --git a/opentrons-ai-client/src/assets/localization/en/protocol_generator.json b/opentrons-ai-client/src/assets/localization/en/protocol_generator.json index f19455ad47e..80d273abffe 100644 --- a/opentrons-ai-client/src/assets/localization/en/protocol_generator.json +++ b/opentrons-ai-client/src/assets/localization/en/protocol_generator.json @@ -6,7 +6,8 @@ "make_sure_your_prompt": "Make sure your prompt includes the following:", "metadata": "Metadata: Three pieces of information.", "modules": "Modules: Thermocycler or Temperature Module.", - "opentronsai_asks_you": "OpentronsAI asks you to provide it!", + "opentronsai_asks": "OpentronsAI asks you to provide it!", + "opentronsai": "OpentronsAI", "ot2_pipettes": "OT-2 pipettes: Include volume, number of channels, and generation.", "prc_flex": "PCR (Flex)", "prc": "PCR", @@ -16,10 +17,11 @@ "share_your_thoughts": "Share your thoughts here", "side_panel_body": "Write a prompt in natural language to generate a Reagent Transfer or a PCR protocol for the OT-2 or Opentrons Flex using the Opentrons Python Protocol API.", "side_panel_header": "Use natural language to generate protocols with OpentronsAI powered by OpenAI", - "tipracks_and_labware": "Tip racks and labware: Use names from the Opentrons Labware Library.", + "tipracks_and_labware": "Tip racks and labware: Use names from the Opentrons Labware Library.", "try_example_prompts": "Stuck? Try these example prompts to get started.", "type_your_prompt": "Type your prompt...", "well_allocations": "Well allocations: Describe where liquids should go in labware.", - "what_if_you": "What if you don’t provide all of those pieces of information?", - "what_typeof_protocol": "What type of protocol do you need?" + "what_if_you": "What if you don’t provide all of those pieces of information? OpentronsAI asks you to provide it!", + "what_typeof_protocol": "What type of protocol do you need?", + "you": "You" } diff --git a/opentrons-ai-client/src/atoms/GlobalStyle/index.ts b/opentrons-ai-client/src/atoms/GlobalStyle/index.ts new file mode 100644 index 00000000000..1319d297779 --- /dev/null +++ b/opentrons-ai-client/src/atoms/GlobalStyle/index.ts @@ -0,0 +1,37 @@ +import { createGlobalStyle } from 'styled-components' +import { COLORS } from '@opentrons/components' +import '@fontsource/public-sans' +import '@fontsource/public-sans/600.css' +import '@fontsource/public-sans/700.css' + +export const GlobalStyle = createGlobalStyle<{ isOnDevice?: boolean }>` + * { + box-sizing: border-box; + margin: 0; + padding: 0; + font-family: ${props => + props.isOnDevice ?? false + ? 'Public Sans, DejaVu Sans' + : 'Open Sans'}, sans-serif; + } + + html, + body { + width: 100%; + height: 100%; + color: ${COLORS.black90}; + } + + a { + text-decoration: none; + } + + button { + border: none; + + &:focus, + &:active { + outline: 0; + } + } +` diff --git a/opentrons-ai-client/src/molecules/PromptGuide/PromptGuide.stories.tsx b/opentrons-ai-client/src/molecules/PromptGuide/PromptGuide.stories.tsx new file mode 100644 index 00000000000..1a29b80c709 --- /dev/null +++ b/opentrons-ai-client/src/molecules/PromptGuide/PromptGuide.stories.tsx @@ -0,0 +1,21 @@ +import React from 'react' +import { I18nextProvider } from 'react-i18next' +import { i18n } from '../../i18n' +import { PromptGuide as PromptGuideComponent } from './index' + +import type { Meta, StoryObj } from '@storybook/react' + +const meta: Meta = { + title: 'AI/molecules/PromptGuide', + component: PromptGuideComponent, + decorators: [ + Story => ( + + + + ), + ], +} +export default meta +type Story = StoryObj +export const PromptGuide: Story = {} diff --git a/opentrons-ai-client/src/molecules/PromptGuide/__tests__/PromptGuide.test.tsx b/opentrons-ai-client/src/molecules/PromptGuide/__tests__/PromptGuide.test.tsx new file mode 100644 index 00000000000..babe9f271f8 --- /dev/null +++ b/opentrons-ai-client/src/molecules/PromptGuide/__tests__/PromptGuide.test.tsx @@ -0,0 +1,46 @@ +import React from 'react' +import { describe, it, expect } from 'vitest' +import { screen } from '@testing-library/react' +import { renderWithProviders } from '../../../__testing-utils__' +import { i18n } from '../../../i18n' + +import { PromptGuide } from '../index' + +const LABWARE_LIBRARY_URL = 'https://labware.opentrons.com/' + +const render = () => { + return renderWithProviders(, { i18nInstance: i18n }) +} + +describe('PromptGuide', () => { + it('should render text', () => { + render() + screen.getByText('What type of protocol do you need?') + screen.getByText('Make sure your prompt includes the following:') + screen.getByText('Metadata: Three pieces of information.') + screen.getByText( + "Application: Your protocol's name, describing what it does." + ) + screen.getByText('Robot: OT-2.') + screen.getByText('API: An API level is 2.15') + screen.getByText( + 'OT-2 pipettes: Include volume, number of channels, and generation.' + ) + screen.getByText('Modules: Thermocycler or Temperature Module.') + screen.getByText( + 'Well allocations: Describe where liquids should go in labware.' + ) + screen.getByText( + "Commands: List the protocol's steps, specifying quantities in microliters and giving exact source and destination locations." + ) + screen.getByText( + 'What if you don’t provide all of those pieces of information?' + ) + screen.getByText('OpentronsAI asks you to provide it!') + }) + it('should have the right url', () => { + render() + const link = screen.getByRole('link', { name: 'Opentrons Labware Library' }) + expect(link).toHaveAttribute('href', LABWARE_LIBRARY_URL) + }) +}) diff --git a/opentrons-ai-client/src/molecules/PromptGuide/index.tsx b/opentrons-ai-client/src/molecules/PromptGuide/index.tsx new file mode 100644 index 00000000000..fb65c615ea3 --- /dev/null +++ b/opentrons-ai-client/src/molecules/PromptGuide/index.tsx @@ -0,0 +1,109 @@ +import React from 'react' +import { Trans, useTranslation } from 'react-i18next' +import styled, { css } from 'styled-components' +import { + BORDERS, + COLORS, + DIRECTION_COLUMN, + Flex, + Link, + SPACING, + StyledText, + TYPOGRAPHY, +} from '@opentrons/components' + +const LABWARE_LIBRARY_URL = 'https://labware.opentrons.com/' + +export function PromptGuide(): JSX.Element { + const { t } = useTranslation('protocol_generator') + + return ( + + + {t('what_typeof_protocol')} + + + {t('make_sure_your_prompt')} + + +
      +
    • + {t('metadata')} +
        +
      • + {t('application')} +
      • +
      • + {t('robot')} +
      • +
      • + {t('api')} +
      • +
      +
    • +
    • + {t('ot2_pipettes')} +
    • +
    • + {t('modules')} +
    • +
    • + {t('well_allocations')} +
    • +
    • + , + span: , + }} + /> +
    • +
    • + {t('commands')} +
    • +
    +
    + , + span: , + }} + /> +
    + ) +} + +const HEADER_TEXT_STYLE = css` + font-size: ${TYPOGRAPHY.fontSize28}; + line-height: ${TYPOGRAPHY.lineHeight36}; + font-weight: ${TYPOGRAPHY.fontWeightSemiBold}; +` +const BODY_TEXT_STYLE = css` + font-size: ${TYPOGRAPHY.fontSize20}; + line-height: ${TYPOGRAPHY.lineHeight24}; +` +const NESTED_ITEM_STYLE = css` + padding-left: ${SPACING.spacing16}; + list-style-type: disc; +` +const ExternalLink = styled(Link)` + font-size: ${TYPOGRAPHY.fontSize20}; + line-height: ${TYPOGRAPHY.lineHeight24}; + color: ${COLORS.black90}; + text-decoration: ${TYPOGRAPHY.textDecorationUnderline}; +` diff --git a/opentrons-ai-client/src/molecules/SidePanel/index.tsx b/opentrons-ai-client/src/molecules/SidePanel/index.tsx index 536c0709a8b..a53927c0293 100644 --- a/opentrons-ai-client/src/molecules/SidePanel/index.tsx +++ b/opentrons-ai-client/src/molecules/SidePanel/index.tsx @@ -2,6 +2,7 @@ import React from 'react' import styled, { css } from 'styled-components' import { useTranslation } from 'react-i18next' import { + BORDERS, COLORS, DIRECTION_COLUMN, Flex, @@ -90,7 +91,7 @@ const BUTTON_GUIDE_TEXT_STYLE = css` ` const PromptButton = styled(PrimaryButton)` - border-radius: 2rem; + border-radius: ${BORDERS.borderRadiusFull}; white-space: nowrap; ` From 611978c6787fb594d5b60d4242ff40bae4f1ed72 Mon Sep 17 00:00:00 2001 From: Max Marrone Date: Mon, 15 Apr 2024 16:05:22 -0400 Subject: [PATCH 126/194] refactor(robot-server): Delete unused models for maintenance runs, and document `actions` list as empty (#14905) --- .../maintenance_action_models.py | 44 ------------------- .../maintenance_run_models.py | 11 +++-- 2 files changed, 8 insertions(+), 47 deletions(-) delete mode 100644 robot-server/robot_server/maintenance_runs/maintenance_action_models.py diff --git a/robot-server/robot_server/maintenance_runs/maintenance_action_models.py b/robot-server/robot_server/maintenance_runs/maintenance_action_models.py deleted file mode 100644 index 1eb34809dd5..00000000000 --- a/robot-server/robot_server/maintenance_runs/maintenance_action_models.py +++ /dev/null @@ -1,44 +0,0 @@ -"""Request and response models for controlling maintenance runs with actions.""" -from datetime import datetime -from enum import Enum -from pydantic import BaseModel, Field - -from robot_server.service.json_api import ResourceModel - - -class MaintenanceRunActionType(str, Enum): - """Types of run control actions. - - Args: - PLAY: Start or resume a protocol run. - PAUSE: Pause a run. - STOP: Stop (cancel) a run. - """ - - PLAY = "play" - PAUSE = "pause" - STOP = "stop" - - -class MaintenanceRunActionCreate(BaseModel): - """Request model for new control action creation.""" - - actionType: MaintenanceRunActionType - - -class MaintenanceRunAction(ResourceModel): - """Maintenance Run control action model. - - A MaintenanceRunAction resource represents a client-provided command to - the run in order to control the execution of the run itself. - - This is different than a run command, which represents an individual - robotic procedure to be executed. - """ - - id: str = Field(..., description="A unique identifier to reference the command.") - createdAt: datetime = Field(..., description="When the command was created.") - actionType: MaintenanceRunActionType = Field( - ..., - description="Specific type of action, which determines behavior.", - ) diff --git a/robot-server/robot_server/maintenance_runs/maintenance_run_models.py b/robot-server/robot_server/maintenance_runs/maintenance_run_models.py index f4d1a19dc61..00379034d9b 100644 --- a/robot-server/robot_server/maintenance_runs/maintenance_run_models.py +++ b/robot-server/robot_server/maintenance_runs/maintenance_run_models.py @@ -17,7 +17,6 @@ LabwareOffsetCreate, Liquid, ) -from robot_server.maintenance_runs.maintenance_action_models import MaintenanceRunAction from robot_server.service.json_api import ResourceModel @@ -70,9 +69,15 @@ class MaintenanceRun(ResourceModel): " There can be, at most, one current run." ), ) - actions: List[MaintenanceRunAction] = Field( + actions: List[object] = Field( ..., - description="Client-initiated run control actions.", + description=( + " This is currently always an empty list," + " and is provided for symmetry with non-maintenance runs." + " Non-maintenance runs let you issue actions with" + " `POST /runs/{id}/actions`, but there is currently no equivalent" + " endpoint for maintenance runs." + ), ) errors: List[ErrorOccurrence] = Field( ..., From 5b84b345da682277b017181ce4b269b465a18d28 Mon Sep 17 00:00:00 2001 From: Sarah Breen Date: Mon, 15 Apr 2024 16:38:34 -0400 Subject: [PATCH 127/194] feat(app): orchestration component for new quick transfer flow (#14808) fix PLAT-173, PLAT-228, PLAT-271 --- app/src/App/OnDeviceDisplayApp.tsx | 4 + app/src/assets/localization/en/index.ts | 2 + .../localization/en/quick_transfer.json | 18 ++ .../ChildNavigation.stories.tsx | 19 ++ .../__tests__/ChildNavigation.test.tsx | 22 +++ app/src/organisms/ChildNavigation/index.tsx | 26 ++- .../QuickTransferFlow/CreateNewTransfer.tsx | 74 ++++++++ .../__tests__/CreateNewTransfer.test.tsx | 62 +++++++ .../organisms/QuickTransferFlow/constants.ts | 9 + app/src/organisms/QuickTransferFlow/index.tsx | 167 ++++++++++++++++++ app/src/organisms/QuickTransferFlow/types.ts | 51 ++++++ app/src/pages/ProtocolDashboard/index.tsx | 7 +- .../DeckConfigurator.stories.tsx | 15 ++ .../DeckConfigurator/StaticFixture.tsx | 56 ++++++ .../DeckConfigurator/constants.ts | 2 + .../hardware-sim/DeckConfigurator/index.tsx | 11 ++ 16 files changed, 535 insertions(+), 10 deletions(-) create mode 100644 app/src/assets/localization/en/quick_transfer.json create mode 100644 app/src/organisms/QuickTransferFlow/CreateNewTransfer.tsx create mode 100644 app/src/organisms/QuickTransferFlow/__tests__/CreateNewTransfer.test.tsx create mode 100644 app/src/organisms/QuickTransferFlow/constants.ts create mode 100644 app/src/organisms/QuickTransferFlow/index.tsx create mode 100644 app/src/organisms/QuickTransferFlow/types.ts create mode 100644 components/src/hardware-sim/DeckConfigurator/StaticFixture.tsx diff --git a/app/src/App/OnDeviceDisplayApp.tsx b/app/src/App/OnDeviceDisplayApp.tsx index 835e005d256..1459ff5071f 100644 --- a/app/src/App/OnDeviceDisplayApp.tsx +++ b/app/src/App/OnDeviceDisplayApp.tsx @@ -32,6 +32,7 @@ import { RobotDashboard } from '../pages/RobotDashboard' import { RobotSettingsDashboard } from '../pages/RobotSettingsDashboard' import { ProtocolDashboard } from '../pages/ProtocolDashboard' import { ProtocolDetails } from '../pages/ProtocolDetails' +import { QuickTransferFlow } from '../organisms/QuickTransferFlow' import { RunningProtocol } from '../pages/RunningProtocol' import { RunSummary } from '../pages/RunSummary' import { UpdateRobot } from '../pages/UpdateRobot/UpdateRobot' @@ -73,6 +74,7 @@ export const ON_DEVICE_DISPLAY_PATHS = [ '/network-setup/wifi', '/protocols', '/protocols/:protocolId', + '/quick-transfer', '/robot-settings', '/robot-settings/rename-robot', '/robot-settings/update-robot', @@ -109,6 +111,8 @@ function getPathComponent( return case '/protocols/:protocolId': return + case `/quick-transfer`: + return case '/robot-settings': return case '/robot-settings/rename-robot': diff --git a/app/src/assets/localization/en/index.ts b/app/src/assets/localization/en/index.ts index c74aab09de5..51acf92db53 100644 --- a/app/src/assets/localization/en/index.ts +++ b/app/src/assets/localization/en/index.ts @@ -21,6 +21,7 @@ import protocol_details from './protocol_details.json' import protocol_info from './protocol_info.json' import protocol_list from './protocol_list.json' import protocol_setup from './protocol_setup.json' +import quick_transfer from './quick_transfer.json' import robot_calibration from './robot_calibration.json' import robot_controls from './robot_controls.json' import run_details from './run_details.json' @@ -51,6 +52,7 @@ export const en = { protocol_info, protocol_list, protocol_setup, + quick_transfer, robot_calibration, robot_controls, run_details, diff --git a/app/src/assets/localization/en/quick_transfer.json b/app/src/assets/localization/en/quick_transfer.json new file mode 100644 index 00000000000..45732a28114 --- /dev/null +++ b/app/src/assets/localization/en/quick_transfer.json @@ -0,0 +1,18 @@ +{ + "create_new_transfer": "Create new quick transfer", + "select_attached_pipette": "Select attached pipette", + "select_dest_labware": "Select destination labware", + "select_dest_wells": "Select destination wells", + "select_source_labware": "Select source labware", + "select_source_wells": "Select source wells", + "select_tip_rack": "Select tip rack", + "set_aspirate_volume": "Set aspirate volume", + "set_dispense_volume": "Set dispense volume", + "set_transfer_volume": "Set transfer volume", + "use_deck_slots": "Quick transfers use deck slots B2-D2. These slots hold a tip rack, a source labware, and a destination labware.Make sure that your deck configuration is up to date to avoid collisions.", + "tip_rack": "Tip rack", + "labware": "Labware", + "pipette_currently_attached": "Quick transfer options depend on the pipettes currently attached to your robot.", + "well_selection": "Well selection", + "well_ratio": "Quick transfers with multiple source wells can either be one-to-one (select {{wells}} for this transfer) or consolidate (select 1 destination well)." +} diff --git a/app/src/organisms/ChildNavigation/ChildNavigation.stories.tsx b/app/src/organisms/ChildNavigation/ChildNavigation.stories.tsx index da15b3af90e..cddbb2cd7a3 100644 --- a/app/src/organisms/ChildNavigation/ChildNavigation.stories.tsx +++ b/app/src/organisms/ChildNavigation/ChildNavigation.stories.tsx @@ -15,6 +15,13 @@ const Template: Story> = args => ( export const Default = Template.bind({}) Default.args = { header: 'Header', + onClickBack: () => {}, +} + +export const TitleNoBackButton = Template.bind({}) +TitleNoBackButton.args = { + header: 'Header', + onClickBack: undefined, } export const TitleWithNormalSmallButton = Template.bind({}) @@ -22,6 +29,16 @@ TitleWithNormalSmallButton.args = { header: 'Header', buttonText: 'ButtonText', onClickButton: () => {}, + onClickBack: () => {}, +} + +export const TitleWithNormalSmallButtonDisabled = Template.bind({}) +TitleWithNormalSmallButtonDisabled.args = { + header: 'Header', + buttonText: 'ButtonText', + onClickButton: () => {}, + onClickBack: () => {}, + buttonIsDisabled: true, } export const TitleWithLinkButton = Template.bind({}) @@ -32,6 +49,7 @@ TitleWithLinkButton.args = { iconName: 'information', iconPlacement: 'startIcon', onClickButton: () => {}, + onClickBack: () => {}, } export const TitleWithTwoButtons = Template.bind({}) @@ -47,4 +65,5 @@ TitleWithTwoButtons.args = { buttonText: 'ButtonText', onClickButton: () => {}, secondaryButtonProps, + onClickBack: () => {}, } diff --git a/app/src/organisms/ChildNavigation/__tests__/ChildNavigation.test.tsx b/app/src/organisms/ChildNavigation/__tests__/ChildNavigation.test.tsx index 8f53b640187..8e2a1c7ec0e 100644 --- a/app/src/organisms/ChildNavigation/__tests__/ChildNavigation.test.tsx +++ b/app/src/organisms/ChildNavigation/__tests__/ChildNavigation.test.tsx @@ -72,4 +72,26 @@ describe('ChildNavigation', () => { fireEvent.click(secondaryButton) expect(mockOnClickSecondaryButton).toHaveBeenCalled() }) + it.fails( + 'should not render back button if onClickBack does not exist', + () => { + props = { + ...props, + onClickBack: undefined, + } + render(props) + screen.getByTestId('ChildNavigation_Back_Button') + } + ) + it('should render button as disabled', () => { + props = { + ...props, + buttonText: 'mock button', + onClickButton: mockOnClickButton, + buttonIsDisabled: true, + } + render(props) + const button = screen.getByTestId('ChildNavigation_Primary_Button') + expect(button).toBeDisabled() + }) }) diff --git a/app/src/organisms/ChildNavigation/index.tsx b/app/src/organisms/ChildNavigation/index.tsx index afe3c1f7508..e076f7191af 100644 --- a/app/src/organisms/ChildNavigation/index.tsx +++ b/app/src/organisms/ChildNavigation/index.tsx @@ -20,20 +20,21 @@ import { ODD_FOCUS_VISIBLE } from '../../atoms/buttons/constants' import { SmallButton } from '../../atoms/buttons' import { InlineNotification } from '../../atoms/InlineNotification' -import type { IconName } from '@opentrons/components' +import type { IconName, StyleProps } from '@opentrons/components' import type { InlineNotificationProps } from '../../atoms/InlineNotification' import type { IconPlacement, SmallButtonTypes, } from '../../atoms/buttons/SmallButton' -interface ChildNavigationProps { +interface ChildNavigationProps extends StyleProps { header: string - onClickBack: React.MouseEventHandler + onClickBack?: React.MouseEventHandler buttonText?: React.ReactNode inlineNotification?: InlineNotificationProps onClickButton?: React.MouseEventHandler buttonType?: SmallButtonTypes + buttonIsDisabled?: boolean iconName?: IconName iconPlacement?: IconPlacement secondaryButtonProps?: React.ComponentProps @@ -49,6 +50,8 @@ export function ChildNavigation({ iconName, iconPlacement, secondaryButtonProps, + buttonIsDisabled, + ...styleProps }: ChildNavigationProps): JSX.Element { return ( - - - + {onClickBack != null ? ( + + + + ) : null} {header} @@ -87,6 +93,8 @@ export function ChildNavigation({ onClick={onClickButton} iconName={iconName} iconPlacement={iconPlacement} + disabled={buttonIsDisabled} + data-testid="ChildNavigation_Primary_Button" /> ) : null} diff --git a/app/src/organisms/QuickTransferFlow/CreateNewTransfer.tsx b/app/src/organisms/QuickTransferFlow/CreateNewTransfer.tsx new file mode 100644 index 00000000000..f1b795e5fc3 --- /dev/null +++ b/app/src/organisms/QuickTransferFlow/CreateNewTransfer.tsx @@ -0,0 +1,74 @@ +import * as React from 'react' +import { useTranslation, Trans } from 'react-i18next' +import { + Flex, + SPACING, + StyledText, + DeckConfigurator, + TYPOGRAPHY, + DIRECTION_COLUMN, +} from '@opentrons/components' +import { SmallButton } from '../../atoms/buttons' +import { useDeckConfigurationQuery } from '@opentrons/react-api-client' +import { ChildNavigation } from '../ChildNavigation' + +interface CreateNewTransferProps { + onNext: () => void + exitButtonProps: React.ComponentProps +} + +export function CreateNewTransfer(props: CreateNewTransferProps): JSX.Element { + const { i18n, t } = useTranslation(['quick_transfer', 'shared']) + const deckConfig = useDeckConfigurationQuery().data ?? [] + return ( + + + + + + + ), + }} + /> + + + {}} + handleClickRemove={() => {}} + additionalStaticFixtures={[ + { location: 'cutoutB2', label: t('tip_rack') }, + { location: 'cutoutC2', label: t('labware') }, + { location: 'cutoutD2', label: t('labware') }, + ]} + /> + + + + + ) +} diff --git a/app/src/organisms/QuickTransferFlow/__tests__/CreateNewTransfer.test.tsx b/app/src/organisms/QuickTransferFlow/__tests__/CreateNewTransfer.test.tsx new file mode 100644 index 00000000000..abeba9a2b1d --- /dev/null +++ b/app/src/organisms/QuickTransferFlow/__tests__/CreateNewTransfer.test.tsx @@ -0,0 +1,62 @@ +import * as React from 'react' +import { fireEvent, screen } from '@testing-library/react' +import { describe, it, expect, afterEach, vi, beforeEach } from 'vitest' +import { DeckConfigurator } from '@opentrons/components' + +import { renderWithProviders } from '../../../__testing-utils__' +import { i18n } from '../../../i18n' +import { CreateNewTransfer } from '../CreateNewTransfer' + +import type * as OpentronsComponents from '@opentrons/components' + +vi.mock('@opentrons/components', async importOriginal => { + const actual = await importOriginal() + return { + ...actual, + DeckConfigurator: vi.fn(), + } +}) +const render = (props: React.ComponentProps) => { + return renderWithProviders(, { + i18nInstance: i18n, + }) +} + +describe('CreateNewTransfer', () => { + let props: React.ComponentProps + + beforeEach(() => { + props = { + onNext: vi.fn(), + exitButtonProps: { + buttonType: 'tertiaryLowLight', + buttonText: 'Exit', + onClick: vi.fn(), + }, + } + }) + afterEach(() => { + vi.resetAllMocks() + }) + + it('renders the create new transfer screen and header', () => { + render(props) + screen.getByText('Create new quick transfer') + screen.getByText( + 'Quick transfers use deck slots B2-D2. These slots hold a tip rack, a source labware, and a destination labware.' + ) + screen.getByText( + 'Make sure that your deck configuration is up to date to avoid collisions.' + ) + expect(vi.mocked(DeckConfigurator)).toHaveBeenCalled() + }) + it('renders exit and continue buttons and they work as expected', () => { + render(props) + const exitBtn = screen.getByText('Exit') + fireEvent.click(exitBtn) + expect(props.exitButtonProps.onClick).toHaveBeenCalled() + const continueBtn = screen.getByText('Continue') + fireEvent.click(continueBtn) + expect(props.onNext).toHaveBeenCalled() + }) +}) diff --git a/app/src/organisms/QuickTransferFlow/constants.ts b/app/src/organisms/QuickTransferFlow/constants.ts new file mode 100644 index 00000000000..3241759a044 --- /dev/null +++ b/app/src/organisms/QuickTransferFlow/constants.ts @@ -0,0 +1,9 @@ +export const ACTIONS = { + SELECT_PIPETTE: 'SELECT_PIPETTE', + SELECT_TIP_RACK: 'SELECT_TIP_RACK', + SET_SOURCE_LABWARE: 'SET_SOURCE_LABWARE', + SET_SOURCE_WELLS: 'SET_SOURCE_WELLS', + SET_DEST_LABWARE: 'SET_DEST_LABWARE', + SET_DEST_WELLS: 'SET_DEST_WELLS', + SET_VOLUME: 'SET_VOLUME', +} as const diff --git a/app/src/organisms/QuickTransferFlow/index.tsx b/app/src/organisms/QuickTransferFlow/index.tsx new file mode 100644 index 00000000000..4031c7aa7bf --- /dev/null +++ b/app/src/organisms/QuickTransferFlow/index.tsx @@ -0,0 +1,167 @@ +import * as React from 'react' +import { useHistory } from 'react-router-dom' +import { useTranslation } from 'react-i18next' +import { Flex, StepMeter, SPACING } from '@opentrons/components' +import { SmallButton } from '../../atoms/buttons' +import { ChildNavigation } from '../ChildNavigation' +import { CreateNewTransfer } from './CreateNewTransfer' + +import type { + QuickTransferSetupState, + QuickTransferWizardAction, +} from './types' + +const QUICK_TRANSFER_WIZARD_STEPS = 8 + +// const initialQuickTransferState: QuickTransferSetupState = {} +export function reducer( + state: QuickTransferSetupState, + action: QuickTransferWizardAction +): QuickTransferSetupState { + switch (action.type) { + case 'SELECT_PIPETTE': { + return { + pipette: action.pipette, + } + } + case 'SELECT_TIP_RACK': { + return { + pipette: state.pipette, + tipRack: action.tipRack, + } + } + case 'SET_SOURCE_LABWARE': { + return { + pipette: state.pipette, + tipRack: state.tipRack, + source: action.labware, + } + } + case 'SET_SOURCE_WELLS': { + return { + pipette: state.pipette, + tipRack: state.tipRack, + source: state.source, + sourceWells: action.wells, + } + } + case 'SET_DEST_LABWARE': { + return { + pipette: state.pipette, + tipRack: state.tipRack, + source: state.source, + sourceWells: state.sourceWells, + destination: action.labware, + } + } + case 'SET_DEST_WELLS': { + return { + pipette: state.pipette, + tipRack: state.tipRack, + source: state.source, + sourceWells: state.sourceWells, + destination: state.destination, + destinationWells: action.wells, + } + } + case 'SET_VOLUME': { + return { + pipette: state.pipette, + tipRack: state.tipRack, + source: state.source, + sourceWells: state.sourceWells, + destination: state.destination, + destinationWells: state.destinationWells, + volume: action.volume, + } + } + } +} + +export const QuickTransferFlow = (): JSX.Element => { + const history = useHistory() + const { i18n, t } = useTranslation(['quick_transfer', 'shared']) + // const [state, dispatch] = React.useReducer(reducer, initialQuickTransferState) + const [currentStep, setCurrentStep] = React.useState(1) + const [continueIsDisabled] = React.useState(false) + + // every child component will take state as a prop, an anonymous + // dispatch function related to that step (except create new), + // and a function to disable the continue button + + const exitButtonProps: React.ComponentProps = { + buttonType: 'tertiaryLowLight', + buttonText: i18n.format(t('shared:exit'), 'capitalize'), + onClick: () => { + history.push('protocols') + }, + } + const ORDERED_STEP_HEADERS: string[] = [ + t('create_new_transfer'), + t('select_attached_pipette'), + t('select_tip_rack'), + t('select_source_labware'), + t('select_source_wells'), + t('select_dest_labware'), + t('select_dest_wells'), + t('set_transfer_volume'), + ] + + const header = ORDERED_STEP_HEADERS[currentStep - 1] + let modalContent: JSX.Element | null = null + if (currentStep === 1) { + modalContent = ( + setCurrentStep(prevStep => prevStep + 1)} + exitButtonProps={exitButtonProps} + /> + ) + } else { + modalContent = null + } + + // until each page is wired up, show header title with empty screen + + return ( + <> + + {modalContent == null ? ( + + { + setCurrentStep(prevStep => prevStep - 1) + } + } + buttonText={i18n.format(t('shared:continue'), 'capitalize')} + onClickButton={() => { + if (currentStep === 8) { + history.push('protocols') + } else { + setCurrentStep(prevStep => prevStep + 1) + } + }} + buttonIsDisabled={continueIsDisabled} + secondaryButtonProps={{ + buttonType: 'tertiaryLowLight', + buttonText: i18n.format(t('shared:exit'), 'capitalize'), + onClick: () => { + history.push('protocols') + }, + }} + top={SPACING.spacing8} + /> + {modalContent} + + ) : ( + modalContent + )} + + ) +} diff --git a/app/src/organisms/QuickTransferFlow/types.ts b/app/src/organisms/QuickTransferFlow/types.ts new file mode 100644 index 00000000000..2087a98be37 --- /dev/null +++ b/app/src/organisms/QuickTransferFlow/types.ts @@ -0,0 +1,51 @@ +import { ACTIONS } from './constants' +import type { PipetteData } from '@opentrons/api-client' +import type { LabwareDefinition1 } from '@opentrons/shared-data' + +export interface QuickTransferSetupState { + pipette?: PipetteData + tipRack?: LabwareDefinition1 + source?: LabwareDefinition1 + sourceWells?: string[] + destination?: LabwareDefinition1 + destinationWells?: string[] + volume?: number +} + +export type QuickTransferWizardAction = + | SelectPipetteAction + | SelectTipRackAction + | SetSourceLabwareAction + | SetSourceWellsAction + | SetDestLabwareAction + | SetDestWellsAction + | SetVolumeAction + +interface SelectPipetteAction { + type: typeof ACTIONS.SELECT_PIPETTE + pipette: PipetteData +} +interface SelectTipRackAction { + type: typeof ACTIONS.SELECT_TIP_RACK + tipRack: LabwareDefinition1 +} +interface SetSourceLabwareAction { + type: typeof ACTIONS.SET_SOURCE_LABWARE + labware: LabwareDefinition1 +} +interface SetSourceWellsAction { + type: typeof ACTIONS.SET_SOURCE_WELLS + wells: string[] +} +interface SetDestLabwareAction { + type: typeof ACTIONS.SET_DEST_LABWARE + labware: LabwareDefinition1 +} +interface SetDestWellsAction { + type: typeof ACTIONS.SET_DEST_WELLS + wells: string[] +} +interface SetVolumeAction { + type: typeof ACTIONS.SET_VOLUME + volume: number +} diff --git a/app/src/pages/ProtocolDashboard/index.tsx b/app/src/pages/ProtocolDashboard/index.tsx index e326ab7176c..dd4aa89b1ac 100644 --- a/app/src/pages/ProtocolDashboard/index.tsx +++ b/app/src/pages/ProtocolDashboard/index.tsx @@ -1,4 +1,5 @@ import * as React from 'react' +import { useHistory } from 'react-router-dom' import { useDispatch, useSelector } from 'react-redux' import { useTranslation } from 'react-i18next' @@ -38,6 +39,7 @@ import type { ProtocolResource } from '@opentrons/shared-data' export function ProtocolDashboard(): JSX.Element { const protocols = useAllProtocolsQuery() const runs = useNotifyAllRunsQuery() + const history = useHistory() const { t } = useTranslation('protocol_info') const dispatch = useDispatch() const [navMenuIsOpened, setNavMenuIsOpened] = React.useState(false) @@ -58,6 +60,9 @@ export function ProtocolDashboard(): JSX.Element { const pinnedProtocolIds = useSelector(getPinnedProtocolIds) ?? [] const pinnedProtocols: ProtocolResource[] = [] + // TODO(sb, 4/15/24): The quick transfer button is going to be moved to a new quick transfer + // tab before the feature is released. Because of this, we're not adding test cov + // for this button in ProtocolDashboard const enableQuickTransferFF = useFeatureFlag('enableQuickTransfer') // We only need to grab out the pinned protocol data once all the protocols load @@ -280,7 +285,7 @@ export function ProtocolDashboard(): JSX.Element { buttonText={t('quick_transfer')} iconName="plus" onClick={() => { - console.log('launch quick transfer flow') + history.push('/quick-transfer') }} /> )} diff --git a/components/src/hardware-sim/DeckConfigurator/DeckConfigurator.stories.tsx b/components/src/hardware-sim/DeckConfigurator/DeckConfigurator.stories.tsx index dc900541fd6..f29b2cffc02 100644 --- a/components/src/hardware-sim/DeckConfigurator/DeckConfigurator.stories.tsx +++ b/components/src/hardware-sim/DeckConfigurator/DeckConfigurator.stories.tsx @@ -71,6 +71,12 @@ const deckConfig: CutoutConfig[] = [ }, ] +const staticFixtures = [ + { location: 'cutoutB2', label: 'Tip rack' }, + { location: 'cutoutC2', label: 'Labware' }, + { location: 'cutoutD2', label: 'Labware' }, +] + export const Default = Template.bind({}) Default.args = { deckConfig, @@ -85,3 +91,12 @@ ReadOnly.args = { handleClickRemove: cutoutId => console.log(`remove at ${cutoutId}`), readOnly: true, } + +export const ReadOnlyWithStaticFixtures = Template.bind({}) +ReadOnlyWithStaticFixtures.args = { + deckConfig, + handleClickAdd: () => {}, + handleClickRemove: () => {}, + readOnly: true, + additionalStaticFixtures: staticFixtures, +} diff --git a/components/src/hardware-sim/DeckConfigurator/StaticFixture.tsx b/components/src/hardware-sim/DeckConfigurator/StaticFixture.tsx new file mode 100644 index 00000000000..a3722d51269 --- /dev/null +++ b/components/src/hardware-sim/DeckConfigurator/StaticFixture.tsx @@ -0,0 +1,56 @@ +import * as React from 'react' + +import { Btn, Text } from '../../primitives' +import { TYPOGRAPHY } from '../../ui-style-constants' +import { RobotCoordsForeignObject } from '../Deck/RobotCoordsForeignObject' +import { + CONFIG_STYLE_READ_ONLY, + FIXTURE_HEIGHT, + MIDDLE_SLOT_FIXTURE_WIDTH, + Y_ADJUSTMENT, + COLUMN_2_X_ADJUSTMENT, +} from './constants' + +import type { CutoutId, DeckDefinition } from '@opentrons/shared-data' + +interface StaticFixtureProps { + deckDefinition: DeckDefinition + fixtureLocation: CutoutId + label: string +} + +/** + * this component allows us to add static labeled fixtures to the center column of a deck + * config map + */ + +export function StaticFixture(props: StaticFixtureProps): JSX.Element { + const { deckDefinition, fixtureLocation, label } = props + + const staticCutout = deckDefinition.locations.cutouts.find( + cutout => cutout.id === fixtureLocation + ) + + /** + * deck definition cutout position is the position of the single slot located within that cutout + * so, to get the position of the cutout itself we must add an adjustment to the slot position + */ + const [xSlotPosition = 0, ySlotPosition = 0] = staticCutout?.position ?? [] + const y = ySlotPosition + Y_ADJUSTMENT + const x = xSlotPosition + COLUMN_2_X_ADJUSTMENT + + return ( + + {}}> + {label} + + + ) +} diff --git a/components/src/hardware-sim/DeckConfigurator/constants.ts b/components/src/hardware-sim/DeckConfigurator/constants.ts index 53faef10b7e..388dcdedecc 100644 --- a/components/src/hardware-sim/DeckConfigurator/constants.ts +++ b/components/src/hardware-sim/DeckConfigurator/constants.ts @@ -9,10 +9,12 @@ import { RESPONSIVENESS, SPACING } from '../../ui-style-constants' * Position is relative to deck definition slot positions and a custom stroke applied to the single slot fixture SVG */ export const FIXTURE_HEIGHT = 102.0 +export const MIDDLE_SLOT_FIXTURE_WIDTH = 158.5 export const SINGLE_SLOT_FIXTURE_WIDTH = 243.5 export const STAGING_AREA_FIXTURE_WIDTH = 314.5 export const COLUMN_1_X_ADJUSTMENT = -100 +export const COLUMN_2_X_ADJUSTMENT = -15.5 export const COLUMN_3_X_ADJUSTMENT = -15.5 export const Y_ADJUSTMENT = -8 diff --git a/components/src/hardware-sim/DeckConfigurator/index.tsx b/components/src/hardware-sim/DeckConfigurator/index.tsx index 9378471d8e0..8477b20e875 100644 --- a/components/src/hardware-sim/DeckConfigurator/index.tsx +++ b/components/src/hardware-sim/DeckConfigurator/index.tsx @@ -18,6 +18,7 @@ import { EmptyConfigFixture } from './EmptyConfigFixture' import { StagingAreaConfigFixture } from './StagingAreaConfigFixture' import { TrashBinConfigFixture } from './TrashBinConfigFixture' import { WasteChuteConfigFixture } from './WasteChuteConfigFixture' +import { StaticFixture } from './StaticFixture' import type { CutoutId, DeckConfiguration } from '@opentrons/shared-data' @@ -30,6 +31,7 @@ interface DeckConfiguratorProps { readOnly?: boolean showExpansion?: boolean children?: React.ReactNode + additionalStaticFixtures?: Array<{ location: CutoutId; label: string }> } export function DeckConfigurator(props: DeckConfiguratorProps): JSX.Element { @@ -41,6 +43,7 @@ export function DeckConfigurator(props: DeckConfiguratorProps): JSX.Element { darkFill = COLORS.black90, readOnly = false, showExpansion = true, + additionalStaticFixtures, children, } = props const deckDef = getDeckDefFromRobotType(FLEX_ROBOT_TYPE) @@ -143,6 +146,14 @@ export function DeckConfigurator(props: DeckConfiguratorProps): JSX.Element { fixtureLocation={cutoutId} /> ))} + {additionalStaticFixtures?.map(staticFixture => ( + + ))} Date: Mon, 15 Apr 2024 16:41:37 -0400 Subject: [PATCH 128/194] feat(app): push recent RTP run card click to protocol details (#14906) --- .../RobotDashboard/RecentRunProtocolCard.tsx | 37 +++++++++++---- .../__tests__/RecentRunProtocolCard.test.tsx | 45 +++++++++++++++++-- 2 files changed, 70 insertions(+), 12 deletions(-) diff --git a/app/src/organisms/OnDeviceDisplay/RobotDashboard/RecentRunProtocolCard.tsx b/app/src/organisms/OnDeviceDisplay/RobotDashboard/RecentRunProtocolCard.tsx index df77e460792..21d293c7d5f 100644 --- a/app/src/organisms/OnDeviceDisplay/RobotDashboard/RecentRunProtocolCard.tsx +++ b/app/src/organisms/OnDeviceDisplay/RobotDashboard/RecentRunProtocolCard.tsx @@ -3,6 +3,7 @@ import { css } from 'styled-components' import { useTranslation } from 'react-i18next' import { useHistory } from 'react-router-dom' import { formatDistance } from 'date-fns' +import last from 'lodash/last' import { BORDERS, @@ -17,14 +18,14 @@ import { StyledText, TYPOGRAPHY, } from '@opentrons/components' -import { useProtocolQuery } from '@opentrons/react-api-client' +import { + useProtocolAnalysisAsDocumentQuery, + useProtocolQuery, +} from '@opentrons/react-api-client' import { RUN_STATUS_FAILED, RUN_STATUS_STOPPED, RUN_STATUS_SUCCEEDED, - Run, - RunData, - RunStatus, } from '@opentrons/api-client' import { ODD_FOCUS_VISIBLE } from '../../../atoms/buttons//constants' @@ -38,6 +39,7 @@ import { INIT_STATUS, } from '../../../resources/health/hooks' +import type { Run, RunData, RunStatus } from '@opentrons/api-client' import type { ProtocolResource } from '@opentrons/shared-data' interface RecentRunProtocolCardProps { @@ -98,6 +100,14 @@ export function ProtocolWithLastRun({ const protocolName = protocolData.metadata.protocolName ?? protocolData.files[0].name + const protocolId = protocolData.id + + const { data: analysis } = useProtocolAnalysisAsDocumentQuery( + protocolId, + last(protocolData?.analysisSummaries)?.id ?? null, + { enabled: protocolData != null } + ) + const PROTOCOL_CARD_STYLE = css` flex: 1 0 0; &:active { @@ -125,13 +135,22 @@ export function ProtocolWithLastRun({ height: max-content; ` + const hasRunTimeParameters = + analysis?.runTimeParameters != null + ? analysis?.runTimeParameters.length > 0 + : false + const handleCardClick = (): void => { setShowSpinner(true) - cloneRun() - trackEvent({ - name: 'proceedToRun', - properties: { sourceLocation: 'RecentRunProtocolCard' }, - }) + if (hasRunTimeParameters) { + history.push(`/protocols/${protocolId}`) + } else { + cloneRun() + trackEvent({ + name: 'proceedToRun', + properties: { sourceLocation: 'RecentRunProtocolCard' }, + }) + } // TODO(BC, 08/29/23): reintroduce this analytics event when we refactor the hook to fetch data lazily (performance concern) // trackProtocolRunEvent({ name: 'runAgain' }) } diff --git a/app/src/organisms/OnDeviceDisplay/RobotDashboard/__tests__/RecentRunProtocolCard.test.tsx b/app/src/organisms/OnDeviceDisplay/RobotDashboard/__tests__/RecentRunProtocolCard.test.tsx index 1584e3ce723..10de409948a 100644 --- a/app/src/organisms/OnDeviceDisplay/RobotDashboard/__tests__/RecentRunProtocolCard.test.tsx +++ b/app/src/organisms/OnDeviceDisplay/RobotDashboard/__tests__/RecentRunProtocolCard.test.tsx @@ -5,8 +5,14 @@ import { MemoryRouter } from 'react-router-dom' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { when } from 'vitest-when' -import { useProtocolQuery } from '@opentrons/react-api-client' -import { RUN_STATUS_FAILED } from '@opentrons/api-client' +import { + useProtocolQuery, + useProtocolAnalysisAsDocumentQuery, +} from '@opentrons/react-api-client' +import { + RUN_STATUS_FAILED, + simpleAnalysisFileFixture, +} from '@opentrons/api-client' import { COLORS } from '@opentrons/components' import { renderWithProviders } from '../../../../__testing-utils__' @@ -24,11 +30,23 @@ import { INIT_STATUS, } from '../../../../resources/health/hooks' +import type { useHistory } from 'react-router-dom' import type { ProtocolHardware } from '../../../../pages/Protocols/hooks' +const mockPush = vi.fn() + +vi.mock('react-router-dom', async importOriginal => { + const actual = await importOriginal() + return { + ...actual, + useHistory: () => ({ push: mockPush } as any), + } +}) + vi.mock('@opentrons/react-api-client') vi.mock('../../../../atoms/Skeleton') vi.mock('../../../../pages/Protocols/hooks') +vi.mock('../../../../pages/ProtocolDetails') vi.mock('../../../../organisms/Devices/hooks') vi.mock('../../../../organisms/RunTimeControl/hooks') vi.mock('../../../../organisms/ProtocolUpload/hooks') @@ -128,7 +146,18 @@ describe('RecentRunProtocolCard', () => { data: { data: [mockRunData] }, } as any) vi.mocked(useProtocolQuery).mockReturnValue({ - data: { data: { metadata: { protocolName: 'mockProtocol' } } }, + data: { + data: { + metadata: { protocolName: 'mockProtocol' }, + id: 'mockProtocolId', + }, + }, + } as any) + vi.mocked(useProtocolAnalysisAsDocumentQuery).mockReturnValue({ + data: { + ...simpleAnalysisFileFixture, + runTimeParameters: [], + }, } as any) vi.mocked(useRobotInitializationStatus).mockReturnValue( INIT_STATUS.SUCCEEDED @@ -252,4 +281,14 @@ describe('RecentRunProtocolCard', () => { const [{ getByText }] = render(props) getByText('mock Skeleton') }) + + it('should push to protocol details if protocol contains runtime parameters', () => { + vi.mocked(useProtocolAnalysisAsDocumentQuery).mockReturnValue({ + data: simpleAnalysisFileFixture, + } as any) + render(props) + const button = screen.getByLabelText('RecentRunProtocolCard') + fireEvent.click(button) + expect(mockPush).toBeCalledWith('/protocols/mockProtocolId') + }) }) From 4e895d4ed1d990bb804a2086214fa407e44905fb Mon Sep 17 00:00:00 2001 From: CaseyBatten Date: Mon, 15 Apr 2024 17:14:10 -0400 Subject: [PATCH 129/194] feat(app, api, shared-data, robot-server): Add module fixtures to deck configuration (#14684) Build out the backend and frontend to support loading modules into the deck configuration, including tracking modules by serial number in the persistent deck configuration directory. Closes RESC-209, PLAT-247, PLAT-248, PLAT-249, PLAT-250, PLAT-251, PLAT-252, PLAT-254 --------- Co-authored-by: Brian Cooper Co-authored-by: ahiuchingau <20424172+ahiuchingau@users.noreply.github.com> --- .../calibration_storage/deck_configuration.py | 9 +- .../opentrons/calibration_storage/types.py | 1 + .../hardware_control/modules/types.py | 16 + .../protocol_api/core/engine/protocol.py | 29 +- .../protocol_api/core/legacy/deck.py | 5 + .../core/legacy/legacy_protocol_core.py | 4 +- .../opentrons/protocol_api/core/protocol.py | 4 +- api/src/opentrons/protocol_api/validation.py | 4 + .../protocol_engine/commands/load_module.py | 22 +- .../protocol_engine/execution/equipment.py | 9 +- .../resources/deck_configuration_provider.py | 21 +- .../resources/deck_data_provider.py | 8 +- .../state/addressable_areas.py | 63 +- .../protocol_engine/state/geometry.py | 5 +- .../protocol_engine/state/labware.py | 8 +- .../protocol_engine/state/modules.py | 198 +++- .../opentrons/protocol_engine/state/state.py | 4 +- api/src/opentrons/protocol_engine/types.py | 9 +- .../test_deck_configuration.py | 8 +- api/tests/opentrons/conftest.py | 4 +- .../core/engine/test_protocol_core.py | 160 ++- api/tests/opentrons/protocol_api/test_deck.py | 8 +- .../opentrons/protocol_engine/conftest.py | 14 +- .../execution/test_equipment_handler.py | 1 + .../test_deck_configuration_provider.py | 49 +- .../resources/test_deck_data_provider.py | 10 +- .../state/test_addressable_area_store.py | 34 +- .../state/test_addressable_area_view.py | 14 +- .../state/test_geometry_view.py | 99 +- .../state/test_labware_store.py | 6 +- .../state/test_labware_view.py | 24 +- .../state/test_module_store.py | 71 +- .../protocol_engine/state/test_module_view.py | 110 +- .../protocol_engine/state/test_state_store.py | 4 +- .../test_create_protocol_engine.py | 31 +- .../multiple_modules_modal.png | Bin 85832 -> 0 bytes .../localization/en/device_details.json | 5 +- .../localization/en/protocol_setup.json | 2 + .../AddFixtureModal.tsx | 402 +++++-- .../__tests__/AddFixtureModal.test.tsx | 56 +- .../DeviceDetailsDeckConfiguration.test.tsx | 4 +- .../DeviceDetailsDeckConfiguration/index.tsx | 162 ++- .../ChooseModuleToConfigureModal.tsx | 154 +++ .../LocationConflictModal.tsx | 88 +- .../MultipleModulesModal.tsx | 120 -- .../OT2MultipleModulesHelp.tsx | 123 ++ .../SetupModuleAndDeck/SetupFixtureList.tsx | 74 +- .../SetupModuleAndDeck/SetupModulesList.tsx | 180 +-- .../__tests__/LocationConflictModal.test.tsx | 13 +- ...st.tsx => OT2MultipleModulesHelp.test.tsx} | 42 +- .../__tests__/SetupFixtureList.test.tsx | 5 +- .../__tests__/SetupModulesList.test.tsx | 36 +- .../ProtocolRun/SetupModuleAndDeck/index.tsx | 59 +- .../ProtocolRun/SetupModuleAndDeck/utils.ts | 15 + .../__tests__/getLabwareRenderInfo.test.ts | 4 +- .../__tests__/getProtocolModulesInfo.test.ts | 4 +- ...seModuleRenderInfoForProtocolById.test.tsx | 73 +- .../useModuleRenderInfoForProtocolById.ts | 67 +- .../InterventionModal/__tests__/utils.test.ts | 20 +- .../ModuleWizardFlows/AttachProbe.tsx | 35 +- .../ModuleWizardFlows/PlaceAdapter.tsx | 38 +- .../ModuleWizardFlows/SelectLocation.tsx | 86 +- app/src/organisms/ModuleWizardFlows/index.tsx | 35 +- app/src/organisms/ModuleWizardFlows/types.ts | 1 - .../ProtocolSetupDeckConfiguration.test.tsx | 5 + .../__tests__/ProtocolSetupLabware.test.tsx | 2 +- .../organisms/ProtocolSetupLabware/index.tsx | 1 + .../FixtureTable.tsx | 7 + .../ModuleTable.tsx | 68 +- .../ProtocolSetupModulesAndDeck/index.tsx | 12 - app/src/pages/DeckConfiguration/index.tsx | 65 +- .../__tests__/ProtocolSetup.test.tsx | 12 +- .../Protocols/hooks/__tests__/hooks.test.tsx | 36 +- app/src/pages/Protocols/hooks/index.ts | 67 +- app/src/resources/deck_configuration/hooks.ts | 22 +- app/src/resources/deck_configuration/utils.ts | 5 +- .../DeckConfigurator/EmptyConfigFixture.tsx | 45 +- .../DeckConfigurator/HeaterShakerFixture.tsx | 101 ++ .../DeckConfigurator/MagneticBlockFixture.tsx | 130 ++ .../StagingAreaConfigFixture.tsx | 21 +- .../DeckConfigurator/StaticFixture.tsx | 4 +- .../TemperatureModuleFixture.tsx | 101 ++ .../DeckConfigurator/ThermocyclerFixture.tsx | 88 ++ .../TrashBinConfigFixture.tsx | 25 +- .../WasteChuteConfigFixture.tsx | 21 +- .../DeckConfigurator/constants.ts | 6 +- .../hardware-sim/DeckConfigurator/index.tsx | 91 +- .../hardware-sim/DeckSlotLocation/index.tsx | 6 +- .../deck_configuration/defaults.py | 96 +- .../robot_server/deck_configuration/models.py | 7 + .../robot_server/deck_configuration/router.py | 4 +- .../robot_server/deck_configuration/store.py | 8 +- .../deck_configuration/validation.py | 83 +- .../deck_configuration/validation_mapping.py | 6 +- robot-server/robot_server/hardware.py | 4 +- .../tests/deck_configuration/test_defaults.py | 2 +- .../deck_configuration/test_validation.py | 318 +++-- .../test_v8_json_upload_flex.tavern.yaml | 30 +- .../runs/test_deck_slot_standardization.py | 24 + .../tests/integration/robot_client.py | 12 + .../deck/definitions/5/ot2_short_trash.json | 409 +++++++ .../deck/definitions/5/ot2_standard.json | 409 +++++++ .../deck/definitions/5/ot3_standard.json | 1042 +++++++++++++++++ shared-data/deck/index.ts | 16 +- shared-data/deck/schemas/5.json | 338 ++++++ shared-data/deck/types/schemaV5.ts | 141 +++ shared-data/js/constants.ts | 128 +- shared-data/js/deck/index.ts | 8 +- shared-data/js/fixtures.ts | 244 +++- .../getDeckDefFromLoadedLabware.test.ts | 4 +- .../helpers/getAddressableAreasInProtocol.ts | 76 +- .../js/helpers/getSimplestFlexDeckConfig.ts | 13 +- shared-data/js/helpers/index.ts | 4 +- shared-data/js/types.ts | 9 +- .../protocol/fixtures/8/simpleFlexV8.json | 6 +- .../opentrons_shared_data/deck/__init__.py | 9 +- .../opentrons_shared_data/deck/dev_types.py | 20 +- .../python/tests/deck/test_typechecks.py | 13 +- 118 files changed, 6032 insertions(+), 1361 deletions(-) delete mode 100644 app/src/assets/images/on-device-display/multiple_modules_modal.png create mode 100644 app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/ChooseModuleToConfigureModal.tsx delete mode 100644 app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/MultipleModulesModal.tsx create mode 100644 app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/OT2MultipleModulesHelp.tsx rename app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/{MultipleModuleModal.test.tsx => OT2MultipleModulesHelp.test.tsx} (60%) create mode 100644 components/src/hardware-sim/DeckConfigurator/HeaterShakerFixture.tsx create mode 100644 components/src/hardware-sim/DeckConfigurator/MagneticBlockFixture.tsx create mode 100644 components/src/hardware-sim/DeckConfigurator/TemperatureModuleFixture.tsx create mode 100644 components/src/hardware-sim/DeckConfigurator/ThermocyclerFixture.tsx create mode 100644 shared-data/deck/definitions/5/ot2_short_trash.json create mode 100644 shared-data/deck/definitions/5/ot2_standard.json create mode 100644 shared-data/deck/definitions/5/ot3_standard.json create mode 100644 shared-data/deck/schemas/5.json create mode 100644 shared-data/deck/types/schemaV5.ts diff --git a/api/src/opentrons/calibration_storage/deck_configuration.py b/api/src/opentrons/calibration_storage/deck_configuration.py index 31410403d35..a627fce73c9 100644 --- a/api/src/opentrons/calibration_storage/deck_configuration.py +++ b/api/src/opentrons/calibration_storage/deck_configuration.py @@ -10,6 +10,7 @@ class _CutoutFixturePlacementModel(pydantic.BaseModel): cutoutId: str cutoutFixtureId: str + opentronsModuleSerialNumber: Optional[str] class _DeckConfigurationModel(pydantic.BaseModel): @@ -26,7 +27,9 @@ def serialize_deck_configuration( data = _DeckConfigurationModel.construct( cutoutFixtures=[ _CutoutFixturePlacementModel.construct( - cutoutId=e.cutout_id, cutoutFixtureId=e.cutout_fixture_id + cutoutId=e.cutout_id, + cutoutFixtureId=e.cutout_fixture_id, + opentronsModuleSerialNumber=e.opentrons_module_serial_number, ) for e in cutout_fixture_placements ], @@ -50,7 +53,9 @@ def deserialize_deck_configuration( else: cutout_fixture_placements = [ CutoutFixturePlacement( - cutout_id=e.cutoutId, cutout_fixture_id=e.cutoutFixtureId + cutout_id=e.cutoutId, + cutout_fixture_id=e.cutoutFixtureId, + opentrons_module_serial_number=e.opentronsModuleSerialNumber, ) for e in parsed.cutoutFixtures ] diff --git a/api/src/opentrons/calibration_storage/types.py b/api/src/opentrons/calibration_storage/types.py index fd1bfbd5e2e..bd80af33719 100644 --- a/api/src/opentrons/calibration_storage/types.py +++ b/api/src/opentrons/calibration_storage/types.py @@ -42,3 +42,4 @@ class UriDetails: class CutoutFixturePlacement: cutout_fixture_id: str cutout_id: str + opentrons_module_serial_number: typing.Optional[str] diff --git a/api/src/opentrons/hardware_control/modules/types.py b/api/src/opentrons/hardware_control/modules/types.py index 1a87d60d35e..eb8054a87ee 100644 --- a/api/src/opentrons/hardware_control/modules/types.py +++ b/api/src/opentrons/hardware_control/modules/types.py @@ -64,6 +64,22 @@ def from_model(cls, model: ModuleModel) -> ModuleType: if isinstance(model, MagneticBlockModel): return cls.MAGNETIC_BLOCK + @classmethod + def to_module_fixture_id(cls, module_type: ModuleType) -> str: + if module_type == ModuleType.THERMOCYCLER: + # Thermocyclers are "loaded" in B1 only + return "thermocyclerModuleV2Front" + if module_type == ModuleType.TEMPERATURE: + return "temperatureModuleV2" + if module_type == ModuleType.HEATER_SHAKER: + return "heaterShakerModuleV1" + if module_type == ModuleType.MAGNETIC_BLOCK: + return "magneticBlockV1" + else: + raise ValueError( + f"Module Type {module_type} does not have a related fixture ID." + ) + class MagneticModuleModel(str, Enum): MAGNETIC_V1: str = "magneticModuleV1" diff --git a/api/src/opentrons/protocol_api/core/engine/protocol.py b/api/src/opentrons/protocol_api/core/engine/protocol.py index e3146a98a08..68b86cbfe34 100644 --- a/api/src/opentrons/protocol_api/core/engine/protocol.py +++ b/api/src/opentrons/protocol_api/core/engine/protocol.py @@ -3,7 +3,8 @@ from typing import Dict, Optional, Type, Union, List, Tuple, TYPE_CHECKING from opentrons.protocol_engine.commands import LoadModuleResult -from opentrons_shared_data.deck.dev_types import DeckDefinitionV4, SlotDefV3 +from opentrons_shared_data.deck.dev_types import DeckDefinitionV5, SlotDefV3 +from opentrons.protocol_engine.resources import deck_configuration_provider from opentrons_shared_data.labware.labware_definition import LabwareDefinition from opentrons_shared_data.labware.dev_types import LabwareDefinition as LabwareDefDict from opentrons_shared_data.pipette.dev_types import PipetteNameType @@ -602,7 +603,7 @@ def set_last_location( self._last_location = location self._last_mount = mount - def get_deck_definition(self) -> DeckDefinitionV4: + def get_deck_definition(self) -> DeckDefinitionV5: """Get the geometry definition of the robot's deck.""" return self._engine_client.state.labware.get_deck_definition() @@ -625,10 +626,26 @@ def get_staging_slot_definitions(self) -> Dict[str, SlotDefV3]: def _ensure_module_location( self, slot: DeckSlotName, module_type: ModuleType ) -> None: - slot_def = self.get_slot_definition(slot) - compatible_modules = slot_def["compatibleModuleTypes"] - if module_type.value not in compatible_modules: - raise ValueError(f"A {module_type.value} cannot be loaded into slot {slot}") + if self._engine_client.state.config.robot_type == "OT-2 Standard": + slot_def = self.get_slot_definition(slot) + compatible_modules = slot_def["compatibleModuleTypes"] + if module_type.value not in compatible_modules: + raise ValueError( + f"A {module_type.value} cannot be loaded into slot {slot}" + ) + else: + cutout_fixture_id = ModuleType.to_module_fixture_id(module_type) + module_fixture = deck_configuration_provider.get_cutout_fixture( + cutout_fixture_id, + self._engine_client.state.addressable_areas.state.deck_definition, + ) + cutout_id = self._engine_client.state.addressable_areas.get_cutout_id_by_deck_slot_name( + slot + ) + if cutout_id not in module_fixture["mayMountTo"]: + raise ValueError( + f"A {module_type.value} cannot be loaded into slot {slot}" + ) def get_slot_item( self, slot_name: Union[DeckSlotName, StagingSlotName] diff --git a/api/src/opentrons/protocol_api/core/legacy/deck.py b/api/src/opentrons/protocol_api/core/legacy/deck.py index 9a9092af5ae..685f0f5d553 100644 --- a/api/src/opentrons/protocol_api/core/legacy/deck.py +++ b/api/src/opentrons/protocol_api/core/legacy/deck.py @@ -280,6 +280,11 @@ def resolve_module_location( compatible_modules = slot_def["compatibleModuleTypes"] if module_type.value in compatible_modules: return location + elif ( + self._definition["robot"]["model"] == "OT-3 Standard" + and ModuleType.to_module_fixture_id(module_type) == slot_def["id"] + ): + return location else: raise ValueError( f"A {dn_from_type[module_type]} cannot be loaded" diff --git a/api/src/opentrons/protocol_api/core/legacy/legacy_protocol_core.py b/api/src/opentrons/protocol_api/core/legacy/legacy_protocol_core.py index d99c3032a71..02fc2003733 100644 --- a/api/src/opentrons/protocol_api/core/legacy/legacy_protocol_core.py +++ b/api/src/opentrons/protocol_api/core/legacy/legacy_protocol_core.py @@ -1,7 +1,7 @@ import logging from typing import Dict, List, Optional, Set, Union, cast, Tuple -from opentrons_shared_data.deck.dev_types import DeckDefinitionV4, SlotDefV3 +from opentrons_shared_data.deck.dev_types import DeckDefinitionV5, SlotDefV3 from opentrons_shared_data.labware.dev_types import LabwareDefinition from opentrons_shared_data.pipette.dev_types import PipetteNameType from opentrons_shared_data.robot.dev_types import RobotType @@ -491,7 +491,7 @@ def get_labware_on_labware( ) -> Optional[LegacyLabwareCore]: assert False, "get_labware_on_labware only supported on engine core" - def get_deck_definition(self) -> DeckDefinitionV4: + def get_deck_definition(self) -> DeckDefinitionV5: """Get the geometry definition of the robot's deck.""" assert False, "get_deck_definition only supported on engine core" diff --git a/api/src/opentrons/protocol_api/core/protocol.py b/api/src/opentrons/protocol_api/core/protocol.py index 8ed83388c07..a554c14e306 100644 --- a/api/src/opentrons/protocol_api/core/protocol.py +++ b/api/src/opentrons/protocol_api/core/protocol.py @@ -5,7 +5,7 @@ from abc import abstractmethod, ABC from typing import Generic, List, Optional, Union, Tuple, Dict, TYPE_CHECKING -from opentrons_shared_data.deck.dev_types import DeckDefinitionV4, SlotDefV3 +from opentrons_shared_data.deck.dev_types import DeckDefinitionV5, SlotDefV3 from opentrons_shared_data.pipette.dev_types import PipetteNameType from opentrons_shared_data.labware.dev_types import LabwareDefinition from opentrons_shared_data.robot.dev_types import RobotType @@ -188,7 +188,7 @@ def set_last_location( ... @abstractmethod - def get_deck_definition(self) -> DeckDefinitionV4: + def get_deck_definition(self) -> DeckDefinitionV5: """Get the geometry definition of the robot's deck.""" @abstractmethod diff --git a/api/src/opentrons/protocol_api/validation.py b/api/src/opentrons/protocol_api/validation.py index f714f35cecd..eb72c6b6dfd 100644 --- a/api/src/opentrons/protocol_api/validation.py +++ b/api/src/opentrons/protocol_api/validation.py @@ -87,6 +87,10 @@ class InvalidTrashBinLocationError(ValueError): """An error raised when attempting to load trash bins in invalid slots.""" +class InvalidFixtureLocationError(ValueError): + """An error raised when attempting to load a fixture in an invalid cutout.""" + + def ensure_mount_for_pipette( mount: Union[str, Mount, None], pipette: PipetteNameType ) -> Mount: diff --git a/api/src/opentrons/protocol_engine/commands/load_module.py b/api/src/opentrons/protocol_engine/commands/load_module.py index 1d877d08941..dcaa396a245 100644 --- a/api/src/opentrons/protocol_engine/commands/load_module.py +++ b/api/src/opentrons/protocol_engine/commands/load_module.py @@ -5,7 +5,11 @@ from pydantic import BaseModel, Field from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate -from ..types import DeckSlotLocation, ModuleModel, ModuleDefinition +from ..types import ( + DeckSlotLocation, + ModuleModel, + ModuleDefinition, +) if TYPE_CHECKING: from ..state import StateView @@ -104,9 +108,19 @@ def __init__( async def execute(self, params: LoadModuleParams) -> LoadModuleResult: """Check that the requested module is attached and assign its identifier.""" - self._state_view.addressable_areas.raise_if_area_not_in_deck_configuration( - params.location.slotName.id - ) + if self._state_view.config.robot_type == "OT-2 Standard": + self._state_view.addressable_areas.raise_if_area_not_in_deck_configuration( + params.location.slotName.id + ) + else: + addressable_area = self._state_view.geometry._modules.ensure_and_convert_module_fixture_location( + deck_slot=params.location.slotName, + deck_type=self._state_view.config.deck_type, + model=params.model, + ) + self._state_view.addressable_areas.raise_if_area_not_in_deck_configuration( + addressable_area + ) verified_location = self._state_view.geometry.ensure_location_not_occupied( params.location diff --git a/api/src/opentrons/protocol_engine/execution/equipment.py b/api/src/opentrons/protocol_engine/execution/equipment.py index 2487ad50aaa..cda39925945 100644 --- a/api/src/opentrons/protocol_engine/execution/equipment.py +++ b/api/src/opentrons/protocol_engine/execution/equipment.py @@ -1,6 +1,6 @@ """Equipment command side-effect logic.""" from dataclasses import dataclass -from typing import Optional, overload +from typing import Optional, overload, Union from opentrons_shared_data.pipette.dev_types import PipetteNameType @@ -44,6 +44,7 @@ LabwareOffsetLocation, ModuleModel, ModuleDefinition, + AddressableAreaLocation, ) @@ -252,7 +253,7 @@ async def load_pipette( async def load_magnetic_block( self, model: ModuleModel, - location: DeckSlotLocation, + location: Union[DeckSlotLocation, AddressableAreaLocation], module_id: Optional[str], ) -> LoadedModuleData: """Ensure the required magnetic block is attached. @@ -317,10 +318,14 @@ async def load_module( for hw_mod in self._hardware_api.attached_modules ] + serial_number_at_locaiton = self._state_store.geometry._addressable_areas.get_fixture_serial_from_deck_configuration_by_deck_slot( + location.slotName + ) attached_module = self._state_store.modules.select_hardware_module_to_load( model=model, location=location, attached_modules=attached_modules, + expected_serial_number=serial_number_at_locaiton, ) else: diff --git a/api/src/opentrons/protocol_engine/resources/deck_configuration_provider.py b/api/src/opentrons/protocol_engine/resources/deck_configuration_provider.py index 112be3663cd..648bd4f4484 100644 --- a/api/src/opentrons/protocol_engine/resources/deck_configuration_provider.py +++ b/api/src/opentrons/protocol_engine/resources/deck_configuration_provider.py @@ -1,7 +1,7 @@ """Deck configuration resource provider.""" from typing import List, Set, Tuple -from opentrons_shared_data.deck.dev_types import DeckDefinitionV4, CutoutFixture +from opentrons_shared_data.deck.dev_types import DeckDefinitionV5, CutoutFixture from opentrons.types import DeckSlotName @@ -17,11 +17,10 @@ CutoutDoesNotExistError, FixtureDoesNotExistError, AddressableAreaDoesNotExistError, - FixtureDoesNotProvideAreasError, ) -def get_cutout_position(cutout_id: str, deck_definition: DeckDefinitionV4) -> DeckPoint: +def get_cutout_position(cutout_id: str, deck_definition: DeckDefinitionV5) -> DeckPoint: """Get the base position of a cutout on the deck.""" for cutout in deck_definition["locations"]["cutouts"]: if cutout_id == cutout["id"]: @@ -32,7 +31,7 @@ def get_cutout_position(cutout_id: str, deck_definition: DeckDefinitionV4) -> De def get_cutout_fixture( - cutout_fixture_id: str, deck_definition: DeckDefinitionV4 + cutout_fixture_id: str, deck_definition: DeckDefinitionV5 ) -> CutoutFixture: """Gets cutout fixture from deck that matches the cutout fixture ID provided.""" for cutout_fixture in deck_definition["cutoutFixtures"]: @@ -44,20 +43,18 @@ def get_cutout_fixture( def get_provided_addressable_area_names( - cutout_fixture_id: str, cutout_id: str, deck_definition: DeckDefinitionV4 + cutout_fixture_id: str, cutout_id: str, deck_definition: DeckDefinitionV5 ) -> List[str]: """Gets a list of the addressable areas provided by the cutout fixture on the cutout.""" cutout_fixture = get_cutout_fixture(cutout_fixture_id, deck_definition) try: return cutout_fixture["providesAddressableAreas"][cutout_id] - except KeyError as exception: - raise FixtureDoesNotProvideAreasError( - f"Cutout fixture {cutout_fixture['id']} does not provide addressable areas for {cutout_id}" - ) from exception + except KeyError: + return [] def get_addressable_area_display_name( - addressable_area_name: str, deck_definition: DeckDefinitionV4 + addressable_area_name: str, deck_definition: DeckDefinitionV5 ) -> str: """Get the display name for an addressable area name.""" for addressable_area in deck_definition["locations"]["addressableAreas"]: @@ -69,7 +66,7 @@ def get_addressable_area_display_name( def get_potential_cutout_fixtures( - addressable_area_name: str, deck_definition: DeckDefinitionV4 + addressable_area_name: str, deck_definition: DeckDefinitionV5 ) -> Tuple[str, Set[PotentialCutoutFixture]]: """Given an addressable area name, gets the cutout ID associated with it and a set of potential fixtures.""" potential_fixtures = [] @@ -102,7 +99,7 @@ def get_addressable_area_from_name( addressable_area_name: str, cutout_position: DeckPoint, base_slot: DeckSlotName, - deck_definition: DeckDefinitionV4, + deck_definition: DeckDefinitionV5, ) -> AddressableArea: """Given a name and a cutout position, get an addressable area on the deck.""" for addressable_area in deck_definition["locations"]["addressableAreas"]: diff --git a/api/src/opentrons/protocol_engine/resources/deck_data_provider.py b/api/src/opentrons/protocol_engine/resources/deck_data_provider.py index 6098c2f4301..017fc58f552 100644 --- a/api/src/opentrons/protocol_engine/resources/deck_data_provider.py +++ b/api/src/opentrons/protocol_engine/resources/deck_data_provider.py @@ -9,7 +9,7 @@ load as load_deck, DEFAULT_DECK_DEFINITION_VERSION, ) -from opentrons_shared_data.deck.dev_types import DeckDefinitionV4 +from opentrons_shared_data.deck.dev_types import DeckDefinitionV5 from opentrons.protocols.models import LabwareDefinition from opentrons.types import DeckSlotName @@ -39,10 +39,10 @@ def __init__( self._deck_type = deck_type self._labware_data = labware_data or LabwareDataProvider() - async def get_deck_definition(self) -> DeckDefinitionV4: + async def get_deck_definition(self) -> DeckDefinitionV5: """Get a labware definition given the labware's identification.""" - def sync() -> DeckDefinitionV4: + def sync() -> DeckDefinitionV5: return load_deck( name=self._deck_type.value, version=DEFAULT_DECK_DEFINITION_VERSION ) @@ -51,7 +51,7 @@ def sync() -> DeckDefinitionV4: async def get_deck_fixed_labware( self, - deck_definition: DeckDefinitionV4, + deck_definition: DeckDefinitionV5, ) -> List[DeckFixedLabware]: """Get a list of all labware fixtures from a given deck definition.""" labware: List[DeckFixedLabware] = [] diff --git a/api/src/opentrons/protocol_engine/state/addressable_areas.py b/api/src/opentrons/protocol_engine/state/addressable_areas.py index 04894fe3338..909beffbe86 100644 --- a/api/src/opentrons/protocol_engine/state/addressable_areas.py +++ b/api/src/opentrons/protocol_engine/state/addressable_areas.py @@ -4,7 +4,7 @@ from opentrons_shared_data.robot.dev_types import RobotType from opentrons_shared_data.deck.dev_types import ( - DeckDefinitionV4, + DeckDefinitionV5, SlotDefV3, CutoutFixture, ) @@ -56,7 +56,7 @@ class AddressableAreaState: potential_cutout_fixtures_by_cutout_id: Dict[str, Set[PotentialCutoutFixture]] - deck_definition: DeckDefinitionV4 + deck_definition: DeckDefinitionV5 deck_configuration: Optional[DeckConfigurationType] """The host robot's full deck configuration. @@ -94,7 +94,7 @@ class AddressableAreaState: def _get_conflicting_addressable_areas_error_string( potential_cutout_fixtures: Set[PotentialCutoutFixture], loaded_addressable_areas: Dict[str, AddressableArea], - deck_definition: DeckDefinitionV4, + deck_definition: DeckDefinitionV5, ) -> str: loaded_areas_on_cutout = set() for fixture in potential_cutout_fixtures: @@ -158,7 +158,7 @@ def __init__( self, deck_configuration: DeckConfigurationType, config: Config, - deck_definition: DeckDefinitionV4, + deck_definition: DeckDefinitionV5, ) -> None: """Initialize an addressable area store and its state.""" if config.use_simulated_deck_config: @@ -224,11 +224,11 @@ def _handle_command(self, command: Command) -> None: @staticmethod def _get_addressable_areas_from_deck_configuration( - deck_config: DeckConfigurationType, deck_definition: DeckDefinitionV4 + deck_config: DeckConfigurationType, deck_definition: DeckDefinitionV5 ) -> Dict[str, AddressableArea]: """Return all addressable areas provided by the given deck configuration.""" addressable_areas = [] - for cutout_id, cutout_fixture_id in deck_config: + for cutout_id, cutout_fixture_id, opentrons_module_serial_number in deck_config: provided_addressable_areas = ( deck_configuration_provider.get_provided_addressable_area_names( cutout_fixture_id, cutout_id, deck_definition @@ -351,7 +351,7 @@ def get_all_cutout_fixtures(self) -> Optional[List[str]]: assert self._state.deck_configuration is not None return [ cutout_fixture_id - for _, cutout_fixture_id in self._state.deck_configuration + for _, cutout_fixture_id, _serial in self._state.deck_configuration ] def _get_loaded_addressable_area( @@ -453,11 +453,31 @@ def get_addressable_area_position( """ addressable_area = self._get_addressable_area_from_deck_data( addressable_area_name=addressable_area_name, - do_compatibility_check=do_compatibility_check, + do_compatibility_check=False, # This should probably not default to false ) position = addressable_area.position return Point(x=position.x, y=position.y, z=position.z) + def get_addressable_area_offsets_from_cutout( + self, + addressable_area_name: str, + ) -> Point: + """Get the offset form cutout fixture of an addressable area.""" + for addressable_area in self.state.deck_definition["locations"][ + "addressableAreas" + ]: + if addressable_area["id"] == addressable_area_name: + area_offset = addressable_area["offsetFromCutoutFixture"] + position = Point( + x=area_offset[0], + y=area_offset[1], + z=area_offset[2], + ) + return Point(x=position.x, y=position.y, z=position.z) + raise ValueError( + f"No matching addressable area named {addressable_area_name} identified." + ) + def get_addressable_area_bounding_box( self, addressable_area_name: str, @@ -499,6 +519,10 @@ def get_addressable_area_center(self, addressable_area_name: str) -> Point: z=position.z, ) + def get_cutout_id_by_deck_slot_name(self, slot_name: DeckSlotName) -> str: + """Get the Cutout ID of a given Deck Slot by Deck Slot Name.""" + return DECK_SLOT_TO_CUTOUT_MAP[slot_name] + def get_fixture_by_deck_slot_name( self, slot_name: DeckSlotName ) -> Optional[CutoutFixture]: @@ -508,7 +532,11 @@ def get_fixture_by_deck_slot_name( slot_cutout_id = DECK_SLOT_TO_CUTOUT_MAP[slot_name] slot_cutout_fixture = None # This will only ever be one under current assumptions - for cutout_id, cutout_fixture_id in deck_config: + for ( + cutout_id, + cutout_fixture_id, + opentrons_module_serial_number, + ) in deck_config: if cutout_id == slot_cutout_id: slot_cutout_fixture = ( deck_configuration_provider.get_cutout_fixture( @@ -532,6 +560,23 @@ def get_fixture_height(self, cutout_fixture_name: str) -> float: ) return cutout_fixture["height"] + def get_fixture_serial_from_deck_configuration_by_deck_slot( + self, slot_name: DeckSlotName + ) -> Optional[str]: + """Get the serial number provided by the deck configuration for a Fixture at a given location.""" + deck_config = self.state.deck_configuration + if deck_config: + slot_cutout_id = DECK_SLOT_TO_CUTOUT_MAP[slot_name] + # This will only ever be one under current assumptions + for ( + cutout_id, + cutout_fixture_id, + opentrons_module_serial_number, + ) in deck_config: + if cutout_id == slot_cutout_id: + return opentrons_module_serial_number + return None + def get_slot_definition(self, slot_id: str) -> SlotDefV3: """Get the definition of a slot in the deck. diff --git a/api/src/opentrons/protocol_engine/state/geometry.py b/api/src/opentrons/protocol_engine/state/geometry.py index 1822881eea2..4a37bf798c1 100644 --- a/api/src/opentrons/protocol_engine/state/geometry.py +++ b/api/src/opentrons/protocol_engine/state/geometry.py @@ -166,6 +166,7 @@ def get_highest_z_in_slot( except LabwareNotLoadedOnModuleError: return self._modules.get_module_highest_z( module_id=module_id, + addressable_areas=self._addressable_areas, ) else: return self.get_highest_z_of_labware_stack(labware_id) @@ -246,7 +247,9 @@ def _get_labware_position_offset( return LabwareOffsetVector(x=0, y=0, z=0) elif isinstance(labware_location, ModuleLocation): module_id = labware_location.moduleId - module_offset = self._modules.get_nominal_module_offset(module_id=module_id) + module_offset = self._modules.get_nominal_module_offset( + module_id=module_id, addressable_areas=self._addressable_areas + ) module_model = self._modules.get_connected_model(module_id) stacking_overlap = self._labware.get_module_overlap_offsets( labware_id, module_model diff --git a/api/src/opentrons/protocol_engine/state/labware.py b/api/src/opentrons/protocol_engine/state/labware.py index 7709410fd0f..a11f1a58e4a 100644 --- a/api/src/opentrons/protocol_engine/state/labware.py +++ b/api/src/opentrons/protocol_engine/state/labware.py @@ -15,7 +15,7 @@ Union, ) -from opentrons_shared_data.deck.dev_types import DeckDefinitionV4 +from opentrons_shared_data.deck.dev_types import DeckDefinitionV5 from opentrons_shared_data.gripper.constants import LABWARE_GRIP_FORCE from opentrons_shared_data.labware.labware_definition import LabwareRole from opentrons_shared_data.pipette.dev_types import LabwareUri @@ -106,7 +106,7 @@ class LabwareState: labware_offsets_by_id: Dict[str, LabwareOffset] definitions_by_uri: Dict[str, LabwareDefinition] - deck_definition: DeckDefinitionV4 + deck_definition: DeckDefinitionV5 class LabwareStore(HasState[LabwareState], HandlesActions): @@ -116,7 +116,7 @@ class LabwareStore(HasState[LabwareState], HandlesActions): def __init__( self, - deck_definition: DeckDefinitionV4, + deck_definition: DeckDefinitionV5, deck_fixed_labware: Sequence[DeckFixedLabware], ) -> None: """Initialize a labware store and its state.""" @@ -324,7 +324,7 @@ def get_display_name(self, labware_id: str) -> str: or self.get_definition(labware_id).metadata.displayName ) - def get_deck_definition(self) -> DeckDefinitionV4: + def get_deck_definition(self) -> DeckDefinitionV5: """Get the current deck definition.""" return self._state.deck_definition diff --git a/api/src/opentrons/protocol_engine/state/modules.py b/api/src/opentrons/protocol_engine/state/modules.py index 84093de0d4a..0e79dd53cf2 100644 --- a/api/src/opentrons/protocol_engine/state/modules.py +++ b/api/src/opentrons/protocol_engine/state/modules.py @@ -46,6 +46,7 @@ DeckType, LabwareMovementOffsetData, ) +from .addressable_areas import AddressableAreaView from .. import errors from ..commands import ( Command, @@ -210,11 +211,12 @@ def handle_action(self, action: Action) -> None: def _handle_command(self, command: Command) -> None: if isinstance(command.result, LoadModuleResult): + slot_name = command.params.location.slotName self._add_module_substate( module_id=command.result.moduleId, serial_number=command.result.serialNumber, definition=command.result.definition, - slot_name=command.params.location.slotName, + slot_name=slot_name, requested_model=command.params.model, module_live_data=None, ) @@ -707,35 +709,70 @@ def get_dimensions(self, module_id: str) -> ModuleDimensions: def get_nominal_module_offset( self, module_id: str, + addressable_areas: AddressableAreaView, ) -> LabwareOffsetVector: """Get the module's nominal offset vector computed with slot transform.""" - definition = self.get_definition(module_id) - slot = self.get_location(module_id).slotName.id - - pre_transform: NDArray[npdouble] = array( - ( - definition.labwareOffset.x, - definition.labwareOffset.y, - definition.labwareOffset.z, - 1, + if ( + self.state.deck_type == DeckType.OT2_STANDARD + or self.state.deck_type == DeckType.OT2_SHORT_TRASH + ): + definition = self.get_definition(module_id) + slot = self.get_location(module_id).slotName.id + + pre_transform: NDArray[npdouble] = array( + ( + definition.labwareOffset.x, + definition.labwareOffset.y, + definition.labwareOffset.z, + 1, + ) + ) + xforms_ser = definition.slotTransforms.get( + str(self._state.deck_type.value), {} + ).get( + slot, + { + "labwareOffset": [ + [1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1], + ] + }, + ) + xforms_ser_offset = xforms_ser["labwareOffset"] + + # Apply the slot transform, if any + xform: NDArray[npdouble] = array(xforms_ser_offset) + xformed = dot(xform, pre_transform) + return LabwareOffsetVector( + x=xformed[0], + y=xformed[1], + z=xformed[2], + ) + else: + module = self.get(module_id) + if isinstance(module.location, DeckSlotLocation): + location = module.location.slotName + elif module.model == ModuleModel.THERMOCYCLER_MODULE_V2: + location = DeckSlotName.SLOT_B1 + else: + raise ValueError( + "Module location invalid for nominal module offset calculation." + ) + module_addressable_area = self.ensure_and_convert_module_fixture_location( + location, self.state.deck_type, module.model + ) + module_addressable_area_position = ( + addressable_areas.get_addressable_area_offsets_from_cutout( + module_addressable_area + ) + ) + return LabwareOffsetVector( + x=module_addressable_area_position.x, + y=module_addressable_area_position.y, + z=module_addressable_area_position.z, ) - ) - xforms_ser = definition.slotTransforms.get( - str(self._state.deck_type.value), {} - ).get( - slot, - {"labwareOffset": [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]]}, - ) - xforms_ser_offset = xforms_ser["labwareOffset"] - - # Apply the slot transform, if any - xform: NDArray[npdouble] = array(xforms_ser_offset) - xformed = dot(xform, pre_transform) - return LabwareOffsetVector( - x=xformed[0], - y=xformed[1], - z=xformed[2], - ) def get_module_calibration_offset( self, module_id: str @@ -755,7 +792,9 @@ def get_height_over_labware(self, module_id: str) -> float: """Get the height of module parts above module labware base.""" return self.get_dimensions(module_id).overLabwareHeight - def get_module_highest_z(self, module_id: str) -> float: + def get_module_highest_z( + self, module_id: str, addressable_areas: AddressableAreaView + ) -> float: """Get the highest z point of the module, as placed on the robot. The highest Z of a module, unlike the bare overall height, depends on @@ -781,7 +820,7 @@ def get_module_highest_z(self, module_id: str) -> float: z_difference = module_height - default_lw_offset_point nominal_transformed_lw_offset_z = self.get_nominal_module_offset( - module_id=module_id + module_id=module_id, addressable_areas=addressable_areas ).z calibration_offset = self.get_module_calibration_offset(module_id) return ( @@ -943,11 +982,12 @@ def is_edge_move_unsafe(self, mount: MountType, target_slot: DeckSlotName) -> bo return neighbor_slot in self._state.slot_by_module_id.values() - def select_hardware_module_to_load( + def select_hardware_module_to_load( # noqa: C901 self, model: ModuleModel, location: DeckSlotLocation, attached_modules: Sequence[HardwareModule], + expected_serial_number: Optional[str] = None, ) -> HardwareModule: """Get the next matching hardware module for the given model and location. @@ -963,6 +1003,8 @@ def select_hardware_module_to_load( location: The location the module will be assigned to. attached_modules: All attached modules as reported by the HardwareAPI, in the order in which they should be used. + expected_serial_number: An optional variable containing the serial number + expected of the module identified. Raises: ModuleNotAttachedError: A not-yet-assigned module matching the requested @@ -976,7 +1018,6 @@ def select_hardware_module_to_load( if slot == location.slotName: existing_mod_in_slot = self._state.hardware_by_module_id.get(mod_id) break - if existing_mod_in_slot: existing_def = existing_mod_in_slot.definition @@ -992,7 +1033,11 @@ def select_hardware_module_to_load( for m in attached_modules: if m not in self._state.hardware_by_module_id.values(): if model == m.definition.model or model in m.definition.compatibleWith: - return m + if expected_serial_number is not None: + if m.serial_number == expected_serial_number: + return m + else: + return m raise errors.ModuleNotAttachedError(f"No available {model.value} found.") @@ -1063,3 +1108,92 @@ def is_flex_deck_with_thermocycler(self) -> bool: return True else: return False + + def ensure_and_convert_module_fixture_location( + self, + deck_slot: DeckSlotName, + deck_type: DeckType, + model: ModuleModel, + ) -> str: + """Ensure module fixture load location is valid. + + Also, convert the deck slot to a valid module fixture addressable area. + """ + if deck_type == DeckType.OT2_STANDARD or deck_type == DeckType.OT2_SHORT_TRASH: + raise ValueError( + f"Invalid Deck Type: {deck_type.name} - Does not support modules as fixtures." + ) + + if model == ModuleModel.MAGNETIC_BLOCK_V1: + valid_slots = [ + slot + for slot in [ + "A1", + "B1", + "C1", + "D1", + "A2", + "B2", + "C2", + "D2", + "A3", + "B3", + "C3", + "D3", + ] + ] + addressable_areas = [ + "magneticBlockV1A1", + "magneticBlockV1B1", + "magneticBlockV1C1", + "magneticBlockV1D1", + "magneticBlockV1A2", + "magneticBlockV1B2", + "magneticBlockV1C2", + "magneticBlockV1D2", + "magneticBlockV1A3", + "magneticBlockV1B3", + "magneticBlockV1C3", + "magneticBlockV1D3", + ] + + elif model == ModuleModel.HEATER_SHAKER_MODULE_V1: + valid_slots = [ + slot for slot in ["A1", "B1", "C1", "D1", "A3", "B3", "C3", "D3"] + ] + addressable_areas = [ + "heaterShakerV1A1", + "heaterShakerV1B1", + "heaterShakerV1C1", + "heaterShakerV1D1", + "heaterShakerV1A3", + "heaterShakerV1B3", + "heaterShakerV1C3", + "heaterShakerV1D3", + ] + elif model == ModuleModel.TEMPERATURE_MODULE_V2: + valid_slots = [ + slot for slot in ["A1", "B1", "C1", "D1", "A3", "B3", "C3", "D3"] + ] + addressable_areas = [ + "temperatureModuleV2A1", + "temperatureModuleV2B1", + "temperatureModuleV2C1", + "temperatureModuleV2D1", + "temperatureModuleV2A3", + "temperatureModuleV2B3", + "temperatureModuleV2C3", + "temperatureModuleV2D3", + ] + elif model == ModuleModel.THERMOCYCLER_MODULE_V2: + return "thermocyclerModuleV2" + else: + raise ValueError( + f"Unknown module {model.name} has no addressable areas to provide." + ) + + map_addressable_area = { + slot: addressable_area + for slot, addressable_area in zip(valid_slots, addressable_areas) + } + return map_addressable_area[deck_slot.value] diff --git a/api/src/opentrons/protocol_engine/state/state.py b/api/src/opentrons/protocol_engine/state/state.py index 6e08bf759c6..dcde17a7894 100644 --- a/api/src/opentrons/protocol_engine/state/state.py +++ b/api/src/opentrons/protocol_engine/state/state.py @@ -5,7 +5,7 @@ from typing import Callable, Dict, List, Optional, Sequence, TypeVar from typing_extensions import ParamSpec -from opentrons_shared_data.deck.dev_types import DeckDefinitionV4 +from opentrons_shared_data.deck.dev_types import DeckDefinitionV5 from opentrons.protocol_engine.types import ModuleOffsetData @@ -142,7 +142,7 @@ def __init__( self, *, config: Config, - deck_definition: DeckDefinitionV4, + deck_definition: DeckDefinitionV5, deck_fixed_labware: Sequence[DeckFixedLabware], is_door_open: bool, change_notifier: Optional[ChangeNotifier] = None, diff --git a/api/src/opentrons/protocol_engine/types.py b/api/src/opentrons/protocol_engine/types.py index 3d833a65042..d7b0e981b2a 100644 --- a/api/src/opentrons/protocol_engine/types.py +++ b/api/src/opentrons/protocol_engine/types.py @@ -714,6 +714,10 @@ class AreaType(Enum): MOVABLE_TRASH = "movableTrash" FIXED_TRASH = "fixedTrash" WASTE_CHUTE = "wasteChute" + THERMOCYCLER = "thermocycler" + HEATER_SHAKER = "heaterShaker" + TEMPERATURE = "temperatureModule" + MAGNETICBLOCK = "magneticBlock" @dataclass(frozen=True) @@ -820,7 +824,10 @@ class QuadrantNozzleLayoutConfiguration(BaseModel): ] # TODO make the below some sort of better type -DeckConfigurationType = List[Tuple[str, str]] # cutout_id, cutout_fixture_id +# TODO This should instead contain a proper cutout fixture type +DeckConfigurationType = List[ + Tuple[str, str, Optional[str]] +] # cutout_id, cutout_fixture_id, opentrons_module_serial_number class TipPresenceStatus(str, Enum): diff --git a/api/tests/opentrons/calibration_storage/test_deck_configuration.py b/api/tests/opentrons/calibration_storage/test_deck_configuration.py index 3cb8d59535f..afdd4449eb4 100644 --- a/api/tests/opentrons/calibration_storage/test_deck_configuration.py +++ b/api/tests/opentrons/calibration_storage/test_deck_configuration.py @@ -10,8 +10,12 @@ def test_deck_configuration_serdes() -> None: """Test that deck configuration serialization/deserialization survives a round trip.""" dummy_cutout_fixture_placements = [ - CutoutFixturePlacement(cutout_fixture_id="a", cutout_id="b"), - CutoutFixturePlacement(cutout_fixture_id="c", cutout_id="d"), + CutoutFixturePlacement( + cutout_fixture_id="a", cutout_id="b", opentrons_module_serial_number="1" + ), + CutoutFixturePlacement( + cutout_fixture_id="c", cutout_id="d", opentrons_module_serial_number="2" + ), ] dummy_datetime = datetime(year=1961, month=5, day=6, tzinfo=timezone.utc) diff --git a/api/tests/opentrons/conftest.py b/api/tests/opentrons/conftest.py index dcf6b6c4e37..de731268bce 100755 --- a/api/tests/opentrons/conftest.py +++ b/api/tests/opentrons/conftest.py @@ -40,7 +40,7 @@ from opentrons_shared_data.deck.dev_types import ( RobotModel, DeckDefinitionV3, - DeckDefinitionV4, + DeckDefinitionV5, ) from opentrons_shared_data.deck import ( load as load_deck, @@ -256,7 +256,7 @@ def deck_definition_name(robot_model: RobotModel) -> str: @pytest.fixture -def deck_definition(deck_definition_name: str) -> DeckDefinitionV4: +def deck_definition(deck_definition_name: str) -> DeckDefinitionV5: return load_deck(deck_definition_name, DEFAULT_DECK_DEFINITION_VERSION) diff --git a/api/tests/opentrons/protocol_api/core/engine/test_protocol_core.py b/api/tests/opentrons/protocol_api/core/engine/test_protocol_core.py index fdf12f1e51b..d5e71f56f46 100644 --- a/api/tests/opentrons/protocol_api/core/engine/test_protocol_core.py +++ b/api/tests/opentrons/protocol_api/core/engine/test_protocol_core.py @@ -7,7 +7,10 @@ from decoy import Decoy from opentrons_shared_data.deck import load as load_deck -from opentrons_shared_data.deck.dev_types import DeckDefinitionV4, SlotDefV3 +from opentrons_shared_data.deck.dev_types import ( + DeckDefinitionV5, + SlotDefV3, +) from opentrons_shared_data.pipette.dev_types import PipetteNameType from opentrons_shared_data.labware.dev_types import ( LabwareDefinition as LabwareDefDict, @@ -85,15 +88,15 @@ @pytest.fixture(scope="session") -def ot2_standard_deck_def() -> DeckDefinitionV4: +def ot2_standard_deck_def() -> DeckDefinitionV5: """Get the OT-2 standard deck definition.""" - return load_deck(STANDARD_OT2_DECK, 4) + return load_deck(STANDARD_OT2_DECK, 5) @pytest.fixture(scope="session") -def ot3_standard_deck_def() -> DeckDefinitionV4: +def ot3_standard_deck_def() -> DeckDefinitionV5: """Get the OT-2 standard deck definition.""" - return load_deck(STANDARD_OT3_DECK, 4) + return load_deck(STANDARD_OT3_DECK, 5) @pytest.fixture(autouse=True) @@ -180,7 +183,7 @@ def test_api_version( def test_get_slot_definition( - ot2_standard_deck_def: DeckDefinitionV4, subject: ProtocolCore, decoy: Decoy + ot2_standard_deck_def: DeckDefinitionV5, subject: ProtocolCore, decoy: Decoy ) -> None: """It should return a deck slot's definition.""" expected_slot_def = cast( @@ -1154,7 +1157,7 @@ def test_add_labware_definition( EngineModuleModel.THERMOCYCLER_MODULE_V2, ThermocyclerModuleCore, lazy_fixture("ot3_standard_deck_def"), - DeckSlotName.SLOT_A1, + DeckSlotName.SLOT_B1, "OT-3 Standard", ), ( @@ -1177,7 +1180,7 @@ def test_load_module( engine_model: EngineModuleModel, expected_core_cls: Type[ModuleCore], subject: ProtocolCore, - deck_def: DeckDefinitionV4, + deck_def: DeckDefinitionV5, slot_name: DeckSlotName, robot_type: RobotType, ) -> None: @@ -1193,12 +1196,22 @@ def test_load_module( [mock_hw_mod_1, mock_hw_mod_2] ) - decoy.when(subject.get_slot_definition(slot_name)).then_return( - cast( - SlotDefV3, - {"compatibleModuleTypes": [ModuleType.from_model(requested_model)]}, + if robot_type == "OT-2 Standard": + decoy.when(subject.get_slot_definition(slot_name)).then_return( + cast( + SlotDefV3, + {"compatibleModuleTypes": [ModuleType.from_model(requested_model)]}, + ) ) - ) + else: + decoy.when( + mock_engine_client.state.addressable_areas.state.deck_definition + ).then_return(deck_def) + decoy.when( + mock_engine_client.state.addressable_areas.get_cutout_id_by_deck_slot_name( + slot_name + ) + ).then_return("cutout" + slot_name.value) decoy.when(mock_engine_client.state.config.robot_type).then_return(robot_type) @@ -1269,14 +1282,6 @@ def test_load_module( DeckSlotName.SLOT_D2, "OT-3 Standard", ), - ( - MagneticModuleModel.MAGNETIC_V2, - EngineModuleModel.MAGNETIC_MODULE_V2, - MagneticModuleCore, - lazy_fixture("ot3_standard_deck_def"), - DeckSlotName.SLOT_A2, - "OT-3 Standard", - ), ( ThermocyclerModuleModel.THERMOCYCLER_V1, EngineModuleModel.THERMOCYCLER_MODULE_V1, @@ -1311,7 +1316,7 @@ def test_load_module_raises_wrong_location( engine_model: EngineModuleModel, expected_core_cls: Type[ModuleCore], subject: ProtocolCore, - deck_def: DeckDefinitionV4, + deck_def: DeckDefinitionV5, slot_name: DeckSlotName, robot_type: RobotType, ) -> None: @@ -1327,9 +1332,19 @@ def test_load_module_raises_wrong_location( decoy.when(mock_engine_client.state.config.robot_type).then_return(robot_type) - decoy.when(subject.get_slot_definition(slot_name)).then_return( - cast(SlotDefV3, {"compatibleModuleTypes": []}) - ) + if robot_type == "OT-2 Standard": + decoy.when(subject.get_slot_definition(slot_name)).then_return( + cast(SlotDefV3, {"compatibleModuleTypes": []}) + ) + else: + decoy.when( + mock_engine_client.state.addressable_areas.state.deck_definition + ).then_return(deck_def) + decoy.when( + mock_engine_client.state.addressable_areas.get_cutout_id_by_deck_slot_name( + slot_name + ) + ).then_return("cutout" + slot_name.value) with pytest.raises( ValueError, @@ -1342,6 +1357,75 @@ def test_load_module_raises_wrong_location( ) +@pytest.mark.parametrize( + ( + "requested_model", + "engine_model", + "expected_core_cls", + "deck_def", + "slot_name", + "robot_type", + ), + [ + ( + MagneticModuleModel.MAGNETIC_V2, + EngineModuleModel.MAGNETIC_MODULE_V2, + MagneticModuleCore, + lazy_fixture("ot3_standard_deck_def"), + DeckSlotName.SLOT_A2, + "OT-3 Standard", + ), + ], +) +def test_load_module_raises_module_fixture_id_does_not_exist( + decoy: Decoy, + mock_engine_client: EngineClient, + mock_sync_hardware_api: SyncHardwareAPI, + requested_model: ModuleModel, + engine_model: EngineModuleModel, + expected_core_cls: Type[ModuleCore], + subject: ProtocolCore, + deck_def: DeckDefinitionV5, + slot_name: DeckSlotName, + robot_type: RobotType, +) -> None: + """It should issue a load module engine command and raise an error for unmatched fixtures.""" + mock_hw_mod_1 = decoy.mock(cls=AbstractModule) + mock_hw_mod_2 = decoy.mock(cls=AbstractModule) + + decoy.when(mock_hw_mod_1.device_info).then_return({"serial": "abc123"}) + decoy.when(mock_hw_mod_2.device_info).then_return({"serial": "xyz789"}) + decoy.when(mock_sync_hardware_api.attached_modules).then_return( + [mock_hw_mod_1, mock_hw_mod_2] + ) + + decoy.when(mock_engine_client.state.config.robot_type).then_return(robot_type) + + if robot_type == "OT-2 Standard": + decoy.when(subject.get_slot_definition(slot_name)).then_return( + cast(SlotDefV3, {"compatibleModuleTypes": []}) + ) + else: + decoy.when( + mock_engine_client.state.addressable_areas.state.deck_definition + ).then_return(deck_def) + decoy.when( + mock_engine_client.state.addressable_areas.get_cutout_id_by_deck_slot_name( + slot_name + ) + ).then_return("cutout" + slot_name.value) + + with pytest.raises( + ValueError, + match=f"Module Type {ModuleType.from_model(requested_model).value} does not have a related fixture ID.", + ): + subject.load_module( + model=requested_model, + deck_slot=slot_name, + configuration=None, + ) + + # APIv2.15 because we're expecting a fixed trash. @pytest.mark.parametrize("api_version", [APIVersion(2, 15)]) def test_load_mag_block( @@ -1349,7 +1433,7 @@ def test_load_mag_block( mock_engine_client: EngineClient, mock_sync_hardware_api: SyncHardwareAPI, subject: ProtocolCore, - ot3_standard_deck_def: DeckDefinitionV4, + ot3_standard_deck_def: DeckDefinitionV5, ) -> None: """It should issue a load module engine command.""" definition = ModuleDefinition.construct() # type: ignore[call-arg] @@ -1366,6 +1450,14 @@ def test_load_mag_block( }, ) ) + decoy.when( + mock_engine_client.state.addressable_areas.state.deck_definition + ).then_return(ot3_standard_deck_def) + decoy.when( + mock_engine_client.state.addressable_areas.get_cutout_id_by_deck_slot_name( + DeckSlotName.SLOT_A2 + ) + ).then_return("cutout" + DeckSlotName.SLOT_A2.value) decoy.when( mock_engine_client.load_module( @@ -1440,7 +1532,7 @@ def test_load_module_thermocycler_with_no_location( requested_model: ModuleModel, engine_model: EngineModuleModel, subject: ProtocolCore, - deck_def: DeckDefinitionV4, + deck_def: DeckDefinitionV5, expected_slot: DeckSlotName, ) -> None: """It should issue a load module engine command with location at 7.""" @@ -1450,12 +1542,14 @@ def test_load_module_thermocycler_with_no_location( decoy.when(mock_hw_mod.device_info).then_return({"serial": "xyz789"}) decoy.when(mock_sync_hardware_api.attached_modules).then_return([mock_hw_mod]) decoy.when(mock_engine_client.state.config.robot_type).then_return("OT-3 Standard") - decoy.when(subject.get_slot_definition(expected_slot)).then_return( - cast( - SlotDefV3, - {"compatibleModuleTypes": [ModuleType.from_model(requested_model)]}, + decoy.when( + mock_engine_client.state.addressable_areas.state.deck_definition + ).then_return(deck_def) + decoy.when( + mock_engine_client.state.addressable_areas.get_cutout_id_by_deck_slot_name( + expected_slot ) - ) + ).then_return("cutout" + expected_slot.value) decoy.when( mock_engine_client.load_module( @@ -1590,7 +1684,7 @@ def test_get_deck_definition( decoy: Decoy, mock_engine_client: EngineClient, subject: ProtocolCore ) -> None: """It should return the loaded deck definition from engine state.""" - deck_definition = cast(DeckDefinitionV4, {"schemaVersion": "4"}) + deck_definition = cast(DeckDefinitionV5, {"schemaVersion": "5"}) decoy.when(mock_engine_client.state.labware.get_deck_definition()).then_return( deck_definition diff --git a/api/tests/opentrons/protocol_api/test_deck.py b/api/tests/opentrons/protocol_api/test_deck.py index b3dc4716449..f471cb936e1 100644 --- a/api/tests/opentrons/protocol_api/test_deck.py +++ b/api/tests/opentrons/protocol_api/test_deck.py @@ -5,7 +5,7 @@ import pytest from decoy import Decoy -from opentrons_shared_data.deck.dev_types import DeckDefinitionV4, SlotDefV3 +from opentrons_shared_data.deck.dev_types import DeckDefinitionV5, SlotDefV3 from opentrons.motion_planning import adjacent_slots_getters as mock_adjacent_slots from opentrons.protocols.api_support.types import APIVersion @@ -23,10 +23,10 @@ @pytest.fixture -def deck_definition() -> DeckDefinitionV4: +def deck_definition() -> DeckDefinitionV5: """Get a deck definition value object.""" return cast( - DeckDefinitionV4, + DeckDefinitionV5, { "locations": {"addressableAreas": [], "calibrationPoints": []}, "cutoutFixtures": {}, @@ -81,7 +81,7 @@ def staging_slot_definitions_by_name() -> Dict[str, SlotDefV3]: @pytest.fixture def subject( decoy: Decoy, - deck_definition: DeckDefinitionV4, + deck_definition: DeckDefinitionV5, mock_protocol_core: ProtocolCore, mock_core_map: LoadedCoreMap, api_version: APIVersion, diff --git a/api/tests/opentrons/protocol_engine/conftest.py b/api/tests/opentrons/protocol_engine/conftest.py index dfd59089c2d..ab23f7e9e08 100644 --- a/api/tests/opentrons/protocol_engine/conftest.py +++ b/api/tests/opentrons/protocol_engine/conftest.py @@ -7,7 +7,7 @@ from opentrons_shared_data import load_shared_data from opentrons_shared_data.deck import load as load_deck -from opentrons_shared_data.deck.dev_types import DeckDefinitionV4 +from opentrons_shared_data.deck.dev_types import DeckDefinitionV5 from opentrons_shared_data.labware import load_definition from opentrons_shared_data.pipette import pipette_definition from opentrons.protocols.models import LabwareDefinition @@ -57,21 +57,21 @@ def ot3_hardware_api(decoy: Decoy) -> OT3API: @pytest.fixture(scope="session") -def ot2_standard_deck_def() -> DeckDefinitionV4: +def ot2_standard_deck_def() -> DeckDefinitionV5: """Get the OT-2 standard deck definition.""" - return load_deck(STANDARD_OT2_DECK, 4) + return load_deck(STANDARD_OT2_DECK, 5) @pytest.fixture(scope="session") -def ot2_short_trash_deck_def() -> DeckDefinitionV4: +def ot2_short_trash_deck_def() -> DeckDefinitionV5: """Get the OT-2 short-trash deck definition.""" - return load_deck(SHORT_TRASH_DECK, 4) + return load_deck(SHORT_TRASH_DECK, 5) @pytest.fixture(scope="session") -def ot3_standard_deck_def() -> DeckDefinitionV4: +def ot3_standard_deck_def() -> DeckDefinitionV5: """Get the OT-2 standard deck definition.""" - return load_deck(STANDARD_OT3_DECK, 4) + return load_deck(STANDARD_OT3_DECK, 5) @pytest.fixture(scope="session") diff --git a/api/tests/opentrons/protocol_engine/execution/test_equipment_handler.py b/api/tests/opentrons/protocol_engine/execution/test_equipment_handler.py index 69a249ebfc2..b2d97aff7d5 100644 --- a/api/tests/opentrons/protocol_engine/execution/test_equipment_handler.py +++ b/api/tests/opentrons/protocol_engine/execution/test_equipment_handler.py @@ -837,6 +837,7 @@ async def test_load_module( HardwareModule(serial_number="serial-1", definition=tempdeck_v1_def), HardwareModule(serial_number="serial-2", definition=tempdeck_v2_def), ], + expected_serial_number=None, ) ).then_return(HardwareModule(serial_number="serial-1", definition=tempdeck_v1_def)) diff --git a/api/tests/opentrons/protocol_engine/resources/test_deck_configuration_provider.py b/api/tests/opentrons/protocol_engine/resources/test_deck_configuration_provider.py index 8071cc98a66..12b324955be 100644 --- a/api/tests/opentrons/protocol_engine/resources/test_deck_configuration_provider.py +++ b/api/tests/opentrons/protocol_engine/resources/test_deck_configuration_provider.py @@ -5,7 +5,7 @@ from pytest_lazyfixture import lazy_fixture # type: ignore[import-untyped] from opentrons_shared_data.deck import load as load_deck -from opentrons_shared_data.deck.dev_types import DeckDefinitionV4 +from opentrons_shared_data.deck.dev_types import DeckDefinitionV5 from opentrons.types import DeckSlotName @@ -13,7 +13,6 @@ FixtureDoesNotExistError, CutoutDoesNotExistError, AddressableAreaDoesNotExistError, - FixtureDoesNotProvideAreasError, ) from opentrons.protocol_engine.types import ( AddressableArea, @@ -33,21 +32,21 @@ @pytest.fixture(scope="session") -def ot2_standard_deck_def() -> DeckDefinitionV4: +def ot2_standard_deck_def() -> DeckDefinitionV5: """Get the OT-2 standard deck definition.""" - return load_deck(STANDARD_OT2_DECK, 4) + return load_deck(STANDARD_OT2_DECK, 5) @pytest.fixture(scope="session") -def ot2_short_trash_deck_def() -> DeckDefinitionV4: +def ot2_short_trash_deck_def() -> DeckDefinitionV5: """Get the OT-2 standard deck definition.""" - return load_deck(SHORT_TRASH_DECK, 4) + return load_deck(SHORT_TRASH_DECK, 5) @pytest.fixture(scope="session") -def ot3_standard_deck_def() -> DeckDefinitionV4: +def ot3_standard_deck_def() -> DeckDefinitionV5: """Get the OT-2 standard deck definition.""" - return load_deck(STANDARD_OT3_DECK, 4) + return load_deck(STANDARD_OT3_DECK, 5) @pytest.mark.parametrize( @@ -73,7 +72,7 @@ def ot3_standard_deck_def() -> DeckDefinitionV4: def test_get_cutout_position( cutout_id: str, expected_deck_point: DeckPoint, - deck_def: DeckDefinitionV4, + deck_def: DeckDefinitionV5, ) -> None: """It should get the deck position for the requested cutout id.""" cutout_position = subject.get_cutout_position(cutout_id, deck_def) @@ -81,7 +80,7 @@ def test_get_cutout_position( def test_get_cutout_position_raises( - ot3_standard_deck_def: DeckDefinitionV4, + ot3_standard_deck_def: DeckDefinitionV5, ) -> None: """It should raise if there is no cutout with that ID in the deck definition.""" with pytest.raises(CutoutDoesNotExistError): @@ -107,7 +106,7 @@ def test_get_cutout_position_raises( def test_get_cutout_fixture( cutout_fixture_id: str, expected_display_name: str, - deck_def: DeckDefinitionV4, + deck_def: DeckDefinitionV5, ) -> None: """It should get the cutout fixture given the cutout fixture id.""" cutout_fixture = subject.get_cutout_fixture(cutout_fixture_id, deck_def) @@ -115,7 +114,7 @@ def test_get_cutout_fixture( def test_get_cutout_fixture_raises( - ot3_standard_deck_def: DeckDefinitionV4, + ot3_standard_deck_def: DeckDefinitionV5, ) -> None: """It should raise if the given cutout fixture id does not exist.""" with pytest.raises(FixtureDoesNotExistError): @@ -149,7 +148,7 @@ def test_get_provided_addressable_area_names( cutout_fixture_id: str, cutout_id: str, expected_areas: List[str], - deck_def: DeckDefinitionV4, + deck_def: DeckDefinitionV5, ) -> None: """It should get the provided addressable area for the cutout fixture and cutout.""" provided_addressable_areas = subject.get_provided_addressable_area_names( @@ -158,16 +157,6 @@ def test_get_provided_addressable_area_names( assert provided_addressable_areas == expected_areas -def test_get_provided_addressable_area_raises( - ot3_standard_deck_def: DeckDefinitionV4, -) -> None: - """It should raise if the cutout fixture does not provide areas for the given cutout id.""" - with pytest.raises(FixtureDoesNotProvideAreasError): - subject.get_provided_addressable_area_names( - "singleRightSlot", "theFunCutout", ot3_standard_deck_def - ) - - @pytest.mark.parametrize( ( "addressable_area_name", @@ -223,7 +212,7 @@ def test_get_potential_cutout_fixtures( addressable_area_name: str, expected_cutout_id: str, expected_potential_fixtures: Set[PotentialCutoutFixture], - deck_def: DeckDefinitionV4, + deck_def: DeckDefinitionV5, ) -> None: """It should get a cutout id and a set of potential cutout fixtures for an addressable area name.""" cutout_id, potential_fixtures = subject.get_potential_cutout_fixtures( @@ -234,7 +223,7 @@ def test_get_potential_cutout_fixtures( def test_get_potential_cutout_fixtures_raises( - ot3_standard_deck_def: DeckDefinitionV4, + ot3_standard_deck_def: DeckDefinitionV5, ) -> None: """It should raise if there is no fixtures that provide the requested area.""" with pytest.raises(AddressableAreaDoesNotExistError): @@ -288,11 +277,7 @@ def test_get_potential_cutout_fixtures_raises( display_name="Slot D1", bounding_box=Dimensions(x=128.0, y=86.0, z=0), position=AddressableOffsetVector(x=1, y=2, z=3), - compatible_module_types=[ - "temperatureModuleType", - "heaterShakerModuleType", - "magneticBlockType", - ], + compatible_module_types=[], ), lazy_fixture("ot3_standard_deck_def"), ), @@ -327,7 +312,7 @@ def test_get_potential_cutout_fixtures_raises( def test_get_addressable_area_from_name( addressable_area_name: str, expected_addressable_area: AddressableArea, - deck_def: DeckDefinitionV4, + deck_def: DeckDefinitionV5, ) -> None: """It should get the deck position for the requested cutout id.""" addressable_area = subject.get_addressable_area_from_name( @@ -337,7 +322,7 @@ def test_get_addressable_area_from_name( def test_get_addressable_area_from_name_raises( - ot3_standard_deck_def: DeckDefinitionV4, + ot3_standard_deck_def: DeckDefinitionV5, ) -> None: """It should raise if there is no addressable area by that name in the deck.""" with pytest.raises(AddressableAreaDoesNotExistError): diff --git a/api/tests/opentrons/protocol_engine/resources/test_deck_data_provider.py b/api/tests/opentrons/protocol_engine/resources/test_deck_data_provider.py index f587d7ce5dd..bd720777ed6 100644 --- a/api/tests/opentrons/protocol_engine/resources/test_deck_data_provider.py +++ b/api/tests/opentrons/protocol_engine/resources/test_deck_data_provider.py @@ -3,7 +3,7 @@ from pytest_lazyfixture import lazy_fixture # type: ignore[import-untyped] from decoy import Decoy -from opentrons_shared_data.deck.dev_types import DeckDefinitionV4 +from opentrons_shared_data.deck.dev_types import DeckDefinitionV5 from opentrons.protocols.models import LabwareDefinition from opentrons.types import DeckSlotName @@ -31,7 +31,7 @@ def mock_labware_data_provider(decoy: Decoy) -> LabwareDataProvider: ) async def test_get_deck_definition( deck_type: DeckType, - expected_definition: DeckDefinitionV4, + expected_definition: DeckDefinitionV5, mock_labware_data_provider: LabwareDataProvider, ) -> None: """It should be able to load the correct deck definition.""" @@ -44,7 +44,7 @@ async def test_get_deck_definition( async def test_get_deck_labware_fixtures_ot2_standard( decoy: Decoy, - ot2_standard_deck_def: DeckDefinitionV4, + ot2_standard_deck_def: DeckDefinitionV5, ot2_fixed_trash_def: LabwareDefinition, mock_labware_data_provider: LabwareDataProvider, ) -> None: @@ -74,7 +74,7 @@ async def test_get_deck_labware_fixtures_ot2_standard( async def test_get_deck_labware_fixtures_ot2_short_trash( decoy: Decoy, - ot2_short_trash_deck_def: DeckDefinitionV4, + ot2_short_trash_deck_def: DeckDefinitionV5, ot2_short_fixed_trash_def: LabwareDefinition, mock_labware_data_provider: LabwareDataProvider, ) -> None: @@ -104,7 +104,7 @@ async def test_get_deck_labware_fixtures_ot2_short_trash( async def test_get_deck_labware_fixtures_ot3_standard( decoy: Decoy, - ot3_standard_deck_def: DeckDefinitionV4, + ot3_standard_deck_def: DeckDefinitionV5, ot3_fixed_trash_def: LabwareDefinition, mock_labware_data_provider: LabwareDataProvider, ) -> None: diff --git a/api/tests/opentrons/protocol_engine/state/test_addressable_area_store.py b/api/tests/opentrons/protocol_engine/state/test_addressable_area_store.py index 63e9cea2925..8a79d31ce92 100644 --- a/api/tests/opentrons/protocol_engine/state/test_addressable_area_store.py +++ b/api/tests/opentrons/protocol_engine/state/test_addressable_area_store.py @@ -1,7 +1,7 @@ """Addressable area state store tests.""" import pytest -from opentrons_shared_data.deck.dev_types import DeckDefinitionV4 +from opentrons_shared_data.deck.dev_types import DeckDefinitionV5 from opentrons_shared_data.labware.labware_definition import Parameters from opentrons.protocols.models import LabwareDefinition from opentrons.types import DeckSlotName @@ -35,24 +35,24 @@ def _make_deck_config() -> DeckConfigurationType: return [ - ("cutoutA1", "singleLeftSlot"), - ("cutoutB1", "singleLeftSlot"), - ("cutoutC1", "singleLeftSlot"), - ("cutoutD1", "singleLeftSlot"), - ("cutoutA2", "singleCenterSlot"), - ("cutoutB2", "singleCenterSlot"), - ("cutoutC2", "singleCenterSlot"), - ("cutoutD2", "singleCenterSlot"), - ("cutoutA3", "trashBinAdapter"), - ("cutoutB3", "singleRightSlot"), - ("cutoutC3", "stagingAreaRightSlot"), - ("cutoutD3", "wasteChuteRightAdapterNoCover"), + ("cutoutA1", "singleLeftSlot", None), + ("cutoutB1", "singleLeftSlot", None), + ("cutoutC1", "singleLeftSlot", None), + ("cutoutD1", "singleLeftSlot", None), + ("cutoutA2", "singleCenterSlot", None), + ("cutoutB2", "singleCenterSlot", None), + ("cutoutC2", "singleCenterSlot", None), + ("cutoutD2", "singleCenterSlot", None), + ("cutoutA3", "trashBinAdapter", None), + ("cutoutB3", "singleRightSlot", None), + ("cutoutC3", "stagingAreaRightSlot", None), + ("cutoutD3", "wasteChuteRightAdapterNoCover", None), ] @pytest.fixture def simulated_subject( - ot3_standard_deck_def: DeckDefinitionV4, + ot3_standard_deck_def: DeckDefinitionV5, ) -> AddressableAreaStore: """Get an AddressableAreaStore test subject, under simulated deck conditions.""" return AddressableAreaStore( @@ -68,7 +68,7 @@ def simulated_subject( @pytest.fixture def subject( - ot3_standard_deck_def: DeckDefinitionV4, + ot3_standard_deck_def: DeckDefinitionV5, ) -> AddressableAreaStore: """Get an AddressableAreaStore test subject.""" return AddressableAreaStore( @@ -83,7 +83,7 @@ def subject( def test_initial_state_simulated( - ot3_standard_deck_def: DeckDefinitionV4, + ot3_standard_deck_def: DeckDefinitionV5, simulated_subject: AddressableAreaStore, ) -> None: """It should create the Addressable Area store with no loaded addressable areas.""" @@ -98,7 +98,7 @@ def test_initial_state_simulated( def test_initial_state( - ot3_standard_deck_def: DeckDefinitionV4, + ot3_standard_deck_def: DeckDefinitionV5, subject: AddressableAreaStore, ) -> None: """It should create the Addressable Area store with loaded addressable areas.""" diff --git a/api/tests/opentrons/protocol_engine/state/test_addressable_area_view.py b/api/tests/opentrons/protocol_engine/state/test_addressable_area_view.py index 34ddcaa37fa..e903c59a45d 100644 --- a/api/tests/opentrons/protocol_engine/state/test_addressable_area_view.py +++ b/api/tests/opentrons/protocol_engine/state/test_addressable_area_view.py @@ -6,7 +6,7 @@ from typing import Dict, Set, Optional, cast from opentrons_shared_data.robot.dev_types import RobotType -from opentrons_shared_data.deck.dev_types import DeckDefinitionV4 +from opentrons_shared_data.deck.dev_types import DeckDefinitionV5 from opentrons.types import Point, DeckSlotName from opentrons.protocol_engine.errors import ( @@ -47,7 +47,7 @@ def get_addressable_area_view( potential_cutout_fixtures_by_cutout_id: Optional[ Dict[str, Set[PotentialCutoutFixture]] ] = None, - deck_definition: Optional[DeckDefinitionV4] = None, + deck_definition: Optional[DeckDefinitionV5] = None, deck_configuration: Optional[DeckConfigurationType] = None, robot_type: RobotType = "OT-3 Standard", use_simulated_deck_config: bool = False, @@ -57,7 +57,7 @@ def get_addressable_area_view( loaded_addressable_areas_by_name=loaded_addressable_areas_by_name or {}, potential_cutout_fixtures_by_cutout_id=potential_cutout_fixtures_by_cutout_id or {}, - deck_definition=deck_definition or cast(DeckDefinitionV4, {"otId": "fake"}), + deck_definition=deck_definition or cast(DeckDefinitionV5, {"otId": "fake"}), deck_configuration=deck_configuration or [], robot_type=robot_type, use_simulated_deck_config=use_simulated_deck_config, @@ -79,8 +79,8 @@ def test_get_all_cutout_fixtures_non_simulated_deck_config() -> None: """It should return the cutout fixtures from the deck config, if it's not simulated.""" subject = get_addressable_area_view( deck_configuration=[ - ("cutout-id-1", "cutout-fixture-id-1"), - ("cutout-id-2", "cutout-fixture-id-2"), + ("cutout-id-1", "cutout-fixture-id-1", None), + ("cutout-id-2", "cutout-fixture-id-2", None), ], use_simulated_deck_config=False, ) @@ -309,6 +309,8 @@ def test_get_fixture_height(decoy: Decoy) -> None: "height": 10, # These values don't matter: "id": "id", + "expectOpentronsModuleSerialNumber": False, + "fixtureGroup": {}, "mayMountTo": [], "displayName": "", "providesAddressableAreas": {}, @@ -324,6 +326,8 @@ def test_get_fixture_height(decoy: Decoy) -> None: "height": 9000.1, # These values don't matter: "id": "id", + "expectOpentronsModuleSerialNumber": False, + "fixtureGroup": {}, "mayMountTo": [], "displayName": "", "providesAddressableAreas": {}, diff --git a/api/tests/opentrons/protocol_engine/state/test_geometry_view.py b/api/tests/opentrons/protocol_engine/state/test_geometry_view.py index 731bcfb9a0e..a390036bdcf 100644 --- a/api/tests/opentrons/protocol_engine/state/test_geometry_view.py +++ b/api/tests/opentrons/protocol_engine/state/test_geometry_view.py @@ -3,9 +3,10 @@ import pytest from decoy import Decoy -from typing import cast, List, Tuple, Optional, NamedTuple +from typing import cast, List, Tuple, Optional, NamedTuple, Dict, Set -from opentrons_shared_data.deck.dev_types import DeckDefinitionV4 +from opentrons_shared_data.deck.dev_types import DeckDefinitionV5 +from opentrons_shared_data.robot.dev_types import RobotType from opentrons_shared_data.labware.dev_types import LabwareUri from opentrons_shared_data.pipette import pipette_definition from opentrons.calibration_storage.helpers import uri_from_details @@ -26,6 +27,7 @@ ModuleLocation, OnLabwareLocation, AddressableAreaLocation, + AddressableArea, ModuleOffsetVector, ModuleOffsetData, LoadedLabware, @@ -45,6 +47,8 @@ LabwareMovementOffsetData, LoadedPipette, TipGeometry, + PotentialCutoutFixture, + DeckConfigurationType, ) from opentrons.protocol_engine.state import move_types from opentrons.protocol_engine.state.config import Config @@ -56,7 +60,10 @@ BoundingNozzlesOffsets, PipetteBoundingBoxOffsets, ) -from opentrons.protocol_engine.state.addressable_areas import AddressableAreaView +from opentrons.protocol_engine.state.addressable_areas import ( + AddressableAreaView, + AddressableAreaState, +) from opentrons.protocol_engine.state.geometry import GeometryView, _GripperMoveType from ..pipette_fixtures import get_default_nozzle_map @@ -112,6 +119,30 @@ def subject( ) +def get_addressable_area_view( + loaded_addressable_areas_by_name: Optional[Dict[str, AddressableArea]] = None, + potential_cutout_fixtures_by_cutout_id: Optional[ + Dict[str, Set[PotentialCutoutFixture]] + ] = None, + deck_definition: Optional[DeckDefinitionV5] = None, + deck_configuration: Optional[DeckConfigurationType] = None, + robot_type: RobotType = "OT-3 Standard", + use_simulated_deck_config: bool = False, +) -> AddressableAreaView: + """Get a labware view test subject.""" + state = AddressableAreaState( + loaded_addressable_areas_by_name=loaded_addressable_areas_by_name or {}, + potential_cutout_fixtures_by_cutout_id=potential_cutout_fixtures_by_cutout_id + or {}, + deck_definition=deck_definition or cast(DeckDefinitionV5, {"otId": "fake"}), + deck_configuration=deck_configuration or [], + robot_type=robot_type, + use_simulated_deck_config=use_simulated_deck_config, + ) + + return AddressableAreaView(state=state) + + def test_get_labware_parent_position( decoy: Decoy, labware_view: LabwareView, @@ -159,7 +190,7 @@ def test_get_labware_parent_position_on_module( labware_view: LabwareView, module_view: ModuleView, addressable_area_view: AddressableAreaView, - ot2_standard_deck_def: DeckDefinitionV4, + ot2_standard_deck_def: DeckDefinitionV5, subject: GeometryView, ) -> None: """It should return a module position for labware on a module.""" @@ -178,10 +209,16 @@ def test_get_labware_parent_position_on_module( decoy.when( addressable_area_view.get_addressable_area_position(DeckSlotName.SLOT_3.id) ).then_return(Point(1, 2, 3)) + decoy.when(labware_view.get_deck_definition()).then_return(ot2_standard_deck_def) + decoy.when( - module_view.get_nominal_module_offset(module_id="module-id") + module_view.get_nominal_module_offset( + module_id="module-id", + addressable_areas=addressable_area_view, + ) ).then_return(LabwareOffsetVector(x=4, y=5, z=6)) + decoy.when(module_view.get_connected_model("module-id")).then_return( ModuleModel.THERMOCYCLER_MODULE_V2 ) @@ -207,7 +244,7 @@ def test_get_labware_parent_position_on_labware( labware_view: LabwareView, module_view: ModuleView, addressable_area_view: AddressableAreaView, - ot2_standard_deck_def: DeckDefinitionV4, + ot2_standard_deck_def: DeckDefinitionV5, subject: GeometryView, ) -> None: """It should return a labware position for labware on a labware on a module.""" @@ -242,7 +279,10 @@ def test_get_labware_parent_position_on_labware( decoy.when(labware_view.get_deck_definition()).then_return(ot2_standard_deck_def) decoy.when( - module_view.get_nominal_module_offset(module_id="module-id") + module_view.get_nominal_module_offset( + module_id="module-id", + addressable_areas=addressable_area_view, + ) ).then_return(LabwareOffsetVector(x=1, y=2, z=3)) decoy.when(module_view.get_connected_model("module-id")).then_return( @@ -270,7 +310,7 @@ def test_module_calibration_offset_rotation( decoy: Decoy, labware_view: LabwareView, module_view: ModuleView, - ot2_standard_deck_def: DeckDefinitionV4, + ot2_standard_deck_def: DeckDefinitionV5, subject: GeometryView, ) -> None: """Return the rotated module calibration offset if the module was moved from one side of the deck to the other.""" @@ -395,7 +435,7 @@ def test_get_module_labware_highest_z( labware_view: LabwareView, module_view: ModuleView, addressable_area_view: AddressableAreaView, - ot2_standard_deck_def: DeckDefinitionV4, + ot2_standard_deck_def: DeckDefinitionV5, subject: GeometryView, ) -> None: """It should get the absolute location of a labware's highest Z point.""" @@ -422,7 +462,10 @@ def test_get_module_labware_highest_z( ) decoy.when(labware_view.get_deck_definition()).then_return(ot2_standard_deck_def) decoy.when( - module_view.get_nominal_module_offset(module_id="module-id") + module_view.get_nominal_module_offset( + module_id="module-id", + addressable_areas=addressable_area_view, + ) ).then_return(LabwareOffsetVector(x=4, y=5, z=6)) decoy.when(module_view.get_height_over_labware("module-id")).then_return(0.5) decoy.when(module_view.get_module_calibration_offset("module-id")).then_return( @@ -692,7 +735,7 @@ def test_get_highest_z_in_slot_with_single_module( module_view: ModuleView, addressable_area_view: AddressableAreaView, subject: GeometryView, - ot2_standard_deck_def: DeckDefinitionV4, + ot2_standard_deck_def: DeckDefinitionV5, ) -> None: """It should get the highest Z in slot with just a single module.""" # Case: Slot has a module that doesn't have any labware on it. Highest z is equal to module height. @@ -707,9 +750,12 @@ def test_get_highest_z_in_slot_with_single_module( errors.LabwareNotLoadedOnModuleError("only module") ) decoy.when(labware_view.get_deck_definition()).then_return(ot2_standard_deck_def) - decoy.when(module_view.get_module_highest_z(module_id="only-module")).then_return( - 12345 - ) + decoy.when( + module_view.get_module_highest_z( + module_id="only-module", + addressable_areas=addressable_area_view, + ) + ).then_return(12345) assert ( subject.get_highest_z_in_slot(DeckSlotLocation(slotName=DeckSlotName.SLOT_3)) @@ -820,7 +866,7 @@ def test_get_highest_z_in_slot_with_labware_stack_on_module( addressable_area_view: AddressableAreaView, subject: GeometryView, well_plate_def: LabwareDefinition, - ot2_standard_deck_def: DeckDefinitionV4, + ot2_standard_deck_def: DeckDefinitionV5, ) -> None: """It should get the highest z in slot of labware on module. @@ -883,7 +929,10 @@ def test_get_highest_z_in_slot_with_labware_stack_on_module( DeckSlotLocation(slotName=DeckSlotName.SLOT_3) ) decoy.when( - module_view.get_nominal_module_offset(module_id="module-id") + module_view.get_nominal_module_offset( + module_id="module-id", + addressable_areas=addressable_area_view, + ) ).then_return(LabwareOffsetVector(x=40, y=50, z=60)) decoy.when(module_view.get_connected_model("module-id")).then_return( ModuleModel.TEMPERATURE_MODULE_V2 @@ -1056,7 +1105,7 @@ def test_get_module_labware_well_position( labware_view: LabwareView, module_view: ModuleView, addressable_area_view: AddressableAreaView, - ot2_standard_deck_def: DeckDefinitionV4, + ot2_standard_deck_def: DeckDefinitionV5, subject: GeometryView, ) -> None: """It should be able to get the position of a well top in a labware on module.""" @@ -1087,7 +1136,10 @@ def test_get_module_labware_well_position( ) decoy.when(labware_view.get_deck_definition()).then_return(ot2_standard_deck_def) decoy.when( - module_view.get_nominal_module_offset(module_id="module-id") + module_view.get_nominal_module_offset( + module_id="module-id", + addressable_areas=addressable_area_view, + ) ).then_return(LabwareOffsetVector(x=4, y=5, z=6)) decoy.when(module_view.get_module_calibration_offset("module-id")).then_return( ModuleOffsetData( @@ -1544,7 +1596,7 @@ def test_get_labware_grip_point( labware_view: LabwareView, module_view: ModuleView, addressable_area_view: AddressableAreaView, - ot2_standard_deck_def: DeckDefinitionV4, + ot2_standard_deck_def: DeckDefinitionV5, subject: GeometryView, ) -> None: """It should get the grip point of the labware at the specified location.""" @@ -1567,7 +1619,7 @@ def test_get_labware_grip_point_on_labware( labware_view: LabwareView, module_view: ModuleView, addressable_area_view: AddressableAreaView, - ot2_standard_deck_def: DeckDefinitionV4, + ot2_standard_deck_def: DeckDefinitionV5, subject: GeometryView, ) -> None: """It should get the grip point of a labware on another labware.""" @@ -1614,7 +1666,7 @@ def test_get_labware_grip_point_for_labware_on_module( labware_view: LabwareView, module_view: ModuleView, addressable_area_view: AddressableAreaView, - ot2_standard_deck_def: DeckDefinitionV4, + ot2_standard_deck_def: DeckDefinitionV5, subject: GeometryView, ) -> None: """It should return the grip point for labware directly on a module.""" @@ -1626,7 +1678,10 @@ def test_get_labware_grip_point_for_labware_on_module( ) decoy.when(labware_view.get_deck_definition()).then_return(ot2_standard_deck_def) decoy.when( - module_view.get_nominal_module_offset(module_id="module-id") + module_view.get_nominal_module_offset( + module_id="module-id", + addressable_areas=addressable_area_view, + ) ).then_return(LabwareOffsetVector(x=1, y=2, z=3)) decoy.when(module_view.get_connected_model("module-id")).then_return( ModuleModel.MAGNETIC_MODULE_V2 diff --git a/api/tests/opentrons/protocol_engine/state/test_labware_store.py b/api/tests/opentrons/protocol_engine/state/test_labware_store.py index 2c0c8cdefd9..9d926583fb0 100644 --- a/api/tests/opentrons/protocol_engine/state/test_labware_store.py +++ b/api/tests/opentrons/protocol_engine/state/test_labware_store.py @@ -4,7 +4,7 @@ from datetime import datetime from opentrons.calibration_storage.helpers import uri_from_details -from opentrons_shared_data.deck.dev_types import DeckDefinitionV4 +from opentrons_shared_data.deck.dev_types import DeckDefinitionV5 from opentrons.protocols.models import LabwareDefinition from opentrons.types import DeckSlotName @@ -33,7 +33,7 @@ @pytest.fixture def subject( - ot2_standard_deck_def: DeckDefinitionV4, + ot2_standard_deck_def: DeckDefinitionV5, ) -> LabwareStore: """Get a LabwareStore test subject.""" return LabwareStore( @@ -43,7 +43,7 @@ def subject( def test_initial_state( - ot2_standard_deck_def: DeckDefinitionV4, + ot2_standard_deck_def: DeckDefinitionV5, subject: LabwareStore, ) -> None: """It should create the labware store with preloaded fixed labware.""" diff --git a/api/tests/opentrons/protocol_engine/state/test_labware_view.py b/api/tests/opentrons/protocol_engine/state/test_labware_view.py index 5e7e96412fa..0f8086de606 100644 --- a/api/tests/opentrons/protocol_engine/state/test_labware_view.py +++ b/api/tests/opentrons/protocol_engine/state/test_labware_view.py @@ -5,7 +5,7 @@ from contextlib import nullcontext as does_not_raise from opentrons_shared_data.deck import load as load_deck -from opentrons_shared_data.deck.dev_types import DeckDefinitionV4 +from opentrons_shared_data.deck.dev_types import DeckDefinitionV5 from opentrons_shared_data.pipette.dev_types import LabwareUri from opentrons_shared_data.labware import load_definition from opentrons_shared_data.labware.labware_definition import ( @@ -110,14 +110,14 @@ def get_labware_view( labware_by_id: Optional[Dict[str, LoadedLabware]] = None, labware_offsets_by_id: Optional[Dict[str, LabwareOffset]] = None, definitions_by_uri: Optional[Dict[str, LabwareDefinition]] = None, - deck_definition: Optional[DeckDefinitionV4] = None, + deck_definition: Optional[DeckDefinitionV5] = None, ) -> LabwareView: """Get a labware view test subject.""" state = LabwareState( labware_by_id=labware_by_id or {}, labware_offsets_by_id=labware_offsets_by_id or {}, definitions_by_uri=definitions_by_uri or {}, - deck_definition=deck_definition or cast(DeckDefinitionV4, {"fake": True}), + deck_definition=deck_definition or cast(DeckDefinitionV5, {"fake": True}), ) return LabwareView(state=state) @@ -696,7 +696,7 @@ def test_get_labware_overlap_offsets() -> None: class ModuleOverlapSpec(NamedTuple): """Spec data to test LabwareView.get_module_overlap_offsets.""" - spec_deck_definition: DeckDefinitionV4 + spec_deck_definition: DeckDefinitionV5 module_model: ModuleModel stacking_offset_with_module: Dict[str, SharedDataOverlapOffset] expected_offset: OverlapOffset @@ -705,7 +705,7 @@ class ModuleOverlapSpec(NamedTuple): module_overlap_specs: List[ModuleOverlapSpec] = [ ModuleOverlapSpec( # Labware on temp module on OT2, with stacking overlap for temp module - spec_deck_definition=load_deck(STANDARD_OT2_DECK, 4), + spec_deck_definition=load_deck(STANDARD_OT2_DECK, 5), module_model=ModuleModel.TEMPERATURE_MODULE_V2, stacking_offset_with_module={ str(ModuleModel.TEMPERATURE_MODULE_V2.value): SharedDataOverlapOffset( @@ -716,7 +716,7 @@ class ModuleOverlapSpec(NamedTuple): ), ModuleOverlapSpec( # Labware on TC Gen1 on OT2, with stacking overlap for TC Gen1 - spec_deck_definition=load_deck(STANDARD_OT2_DECK, 4), + spec_deck_definition=load_deck(STANDARD_OT2_DECK, 5), module_model=ModuleModel.THERMOCYCLER_MODULE_V1, stacking_offset_with_module={ str(ModuleModel.THERMOCYCLER_MODULE_V1.value): SharedDataOverlapOffset( @@ -727,21 +727,21 @@ class ModuleOverlapSpec(NamedTuple): ), ModuleOverlapSpec( # Labware on TC Gen2 on OT2, with no stacking overlap - spec_deck_definition=load_deck(STANDARD_OT2_DECK, 4), + spec_deck_definition=load_deck(STANDARD_OT2_DECK, 5), module_model=ModuleModel.THERMOCYCLER_MODULE_V2, stacking_offset_with_module={}, expected_offset=OverlapOffset(x=0, y=0, z=10.7), ), ModuleOverlapSpec( # Labware on TC Gen2 on Flex, with no stacking overlap - spec_deck_definition=load_deck(STANDARD_OT3_DECK, 4), + spec_deck_definition=load_deck(STANDARD_OT3_DECK, 5), module_model=ModuleModel.THERMOCYCLER_MODULE_V2, stacking_offset_with_module={}, expected_offset=OverlapOffset(x=0, y=0, z=0), ), ModuleOverlapSpec( # Labware on TC Gen2 on Flex, with stacking overlap for TC Gen2 - spec_deck_definition=load_deck(STANDARD_OT3_DECK, 4), + spec_deck_definition=load_deck(STANDARD_OT3_DECK, 5), module_model=ModuleModel.THERMOCYCLER_MODULE_V2, stacking_offset_with_module={ str(ModuleModel.THERMOCYCLER_MODULE_V2.value): SharedDataOverlapOffset( @@ -758,7 +758,7 @@ class ModuleOverlapSpec(NamedTuple): argvalues=module_overlap_specs, ) def test_get_module_overlap_offsets( - spec_deck_definition: DeckDefinitionV4, + spec_deck_definition: DeckDefinitionV5, module_model: ModuleModel, stacking_offset_with_module: Dict[str, SharedDataOverlapOffset], expected_offset: OverlapOffset, @@ -800,7 +800,7 @@ def test_get_default_magnet_height( assert subject.get_default_magnet_height(module_id="module-id", offset=2) == 12.0 -def test_get_deck_definition(ot2_standard_deck_def: DeckDefinitionV4) -> None: +def test_get_deck_definition(ot2_standard_deck_def: DeckDefinitionV5) -> None: """It should get the deck definition from the state.""" subject = get_labware_view(deck_definition=ot2_standard_deck_def) @@ -1404,7 +1404,7 @@ def test_raise_if_labware_cannot_be_stacked_on_labware_on_adapter() -> None: ) -def test_get_deck_gripper_offsets(ot3_standard_deck_def: DeckDefinitionV4) -> None: +def test_get_deck_gripper_offsets(ot3_standard_deck_def: DeckDefinitionV5) -> None: """It should get the deck's gripper offsets.""" subject = get_labware_view(deck_definition=ot3_standard_deck_def) diff --git a/api/tests/opentrons/protocol_engine/state/test_module_store.py b/api/tests/opentrons/protocol_engine/state/test_module_store.py index 1d0d7003496..e6de0a96ac0 100644 --- a/api/tests/opentrons/protocol_engine/state/test_module_store.py +++ b/api/tests/opentrons/protocol_engine/state/test_module_store.py @@ -1,8 +1,9 @@ """Module state store tests.""" -from typing import List +from typing import List, Set, cast, Dict, Optional import pytest from opentrons_shared_data.robot.dev_types import RobotType +from opentrons_shared_data.deck.dev_types import DeckDefinitionV5 from pytest_lazyfixture import lazy_fixture # type: ignore[import-untyped] from opentrons.types import DeckSlotName @@ -18,6 +19,9 @@ ModuleModel, HeaterShakerLatchStatus, DeckType, + AddressableArea, + DeckConfigurationType, + PotentialCutoutFixture, ) from opentrons.protocol_engine.state.modules import ( @@ -37,6 +41,11 @@ ThermocyclerModuleSubState, ModuleSubStateType, ) + +from opentrons.protocol_engine.state.addressable_areas import ( + AddressableAreaView, + AddressableAreaState, +) from opentrons.protocol_engine.state.config import Config from opentrons.hardware_control.modules.types import LiveData @@ -48,9 +57,35 @@ ) +def get_addressable_area_view( + loaded_addressable_areas_by_name: Optional[Dict[str, AddressableArea]] = None, + potential_cutout_fixtures_by_cutout_id: Optional[ + Dict[str, Set[PotentialCutoutFixture]] + ] = None, + deck_definition: Optional[DeckDefinitionV5] = None, + deck_configuration: Optional[DeckConfigurationType] = None, + robot_type: RobotType = "OT-3 Standard", + use_simulated_deck_config: bool = False, +) -> AddressableAreaView: + """Get a labware view test subject.""" + state = AddressableAreaState( + loaded_addressable_areas_by_name=loaded_addressable_areas_by_name or {}, + potential_cutout_fixtures_by_cutout_id=potential_cutout_fixtures_by_cutout_id + or {}, + deck_definition=deck_definition or cast(DeckDefinitionV5, {"otId": "fake"}), + deck_configuration=deck_configuration or [], + robot_type=robot_type, + use_simulated_deck_config=use_simulated_deck_config, + ) + + return AddressableAreaView(state=state) + + def test_initial_state() -> None: """It should initialize the module state.""" - subject = ModuleStore(config=_OT2_STANDARD_CONFIG) + subject = ModuleStore( + config=_OT2_STANDARD_CONFIG, + ) assert subject.state == ModuleState( deck_type=DeckType.OT2_STANDARD, @@ -158,7 +193,9 @@ def test_load_module( ), ) - subject = ModuleStore(config=_OT2_STANDARD_CONFIG) + subject = ModuleStore( + config=_OT2_STANDARD_CONFIG, + ) subject.handle_action(action) assert subject.state == ModuleState( @@ -223,7 +260,7 @@ def test_load_thermocycler_in_thermocycler_slot( use_simulated_deck_config=False, robot_type=robot_type, deck_type=deck_type, - ) + ), ) subject.handle_action(action) @@ -302,7 +339,9 @@ def test_add_module_action( module_live_data=live_data, ) - subject = ModuleStore(_OT2_STANDARD_CONFIG) + subject = ModuleStore( + config=_OT2_STANDARD_CONFIG, + ) subject.handle_action(action) assert subject.state == ModuleState( @@ -343,7 +382,9 @@ def test_handle_hs_temperature_commands(heater_shaker_v1_def: ModuleDefinition) params=hs_commands.DeactivateHeaterParams(moduleId="module-id"), result=hs_commands.DeactivateHeaterResult(), ) - subject = ModuleStore(_OT2_STANDARD_CONFIG) + subject = ModuleStore( + config=_OT2_STANDARD_CONFIG, + ) subject.handle_action( actions.SucceedCommandAction(private_result=None, command=load_module_cmd) @@ -394,7 +435,9 @@ def test_handle_hs_shake_commands(heater_shaker_v1_def: ModuleDefinition) -> Non params=hs_commands.DeactivateShakerParams(moduleId="module-id"), result=hs_commands.DeactivateShakerResult(), ) - subject = ModuleStore(_OT2_STANDARD_CONFIG) + subject = ModuleStore( + config=_OT2_STANDARD_CONFIG, + ) subject.handle_action( actions.SucceedCommandAction(private_result=None, command=load_module_cmd) @@ -447,7 +490,9 @@ def test_handle_hs_labware_latch_commands( params=hs_commands.OpenLabwareLatchParams(moduleId="module-id"), result=hs_commands.OpenLabwareLatchResult(pipetteRetracted=False), ) - subject = ModuleStore(_OT2_STANDARD_CONFIG) + subject = ModuleStore( + config=_OT2_STANDARD_CONFIG, + ) subject.handle_action( actions.SucceedCommandAction(private_result=None, command=load_module_cmd) @@ -511,7 +556,9 @@ def test_handle_tempdeck_temperature_commands( params=temp_commands.DeactivateTemperatureParams(moduleId="module-id"), result=temp_commands.DeactivateTemperatureResult(), ) - subject = ModuleStore(_OT2_STANDARD_CONFIG) + subject = ModuleStore( + config=_OT2_STANDARD_CONFIG, + ) subject.handle_action( actions.SucceedCommandAction(private_result=None, command=load_module_cmd) @@ -570,7 +617,9 @@ def test_handle_thermocycler_temperature_commands( params=tc_commands.DeactivateLidParams(moduleId="module-id"), result=tc_commands.DeactivateLidResult(), ) - subject = ModuleStore(_OT2_STANDARD_CONFIG) + subject = ModuleStore( + config=_OT2_STANDARD_CONFIG, + ) subject.handle_action( actions.SucceedCommandAction(private_result=None, command=load_module_cmd) @@ -652,7 +701,7 @@ def test_handle_thermocycler_lid_commands( use_simulated_deck_config=False, robot_type="OT-3 Standard", deck_type=DeckType.OT3_STANDARD, - ) + ), ) subject.handle_action( diff --git a/api/tests/opentrons/protocol_engine/state/test_module_view.py b/api/tests/opentrons/protocol_engine/state/test_module_view.py index 77ab24bb336..b840673f2e8 100644 --- a/api/tests/opentrons/protocol_engine/state/test_module_view.py +++ b/api/tests/opentrons/protocol_engine/state/test_module_view.py @@ -4,7 +4,21 @@ from pytest_lazyfixture import lazy_fixture # type: ignore[import-untyped] from contextlib import nullcontext as does_not_raise -from typing import ContextManager, Dict, NamedTuple, Optional, Type, Union, Any, List +from typing import ( + ContextManager, + Dict, + NamedTuple, + Optional, + Type, + Union, + Any, + List, + Set, + cast, +) + +from opentrons_shared_data.robot.dev_types import RobotType +from opentrons_shared_data.deck.dev_types import DeckDefinitionV5 from opentrons_shared_data import load_shared_data from opentrons.types import DeckSlotName, MountType @@ -19,12 +33,19 @@ ModuleOffsetData, HeaterShakerLatchStatus, LabwareMovementOffsetData, + AddressableArea, + DeckConfigurationType, + PotentialCutoutFixture, ) from opentrons.protocol_engine.state.modules import ( ModuleView, ModuleState, HardwareModule, ) +from opentrons.protocol_engine.state.addressable_areas import ( + AddressableAreaView, + AddressableAreaState, +) from opentrons.protocol_engine.state.module_substates import ( HeaterShakerModuleSubState, @@ -37,6 +58,40 @@ ThermocyclerModuleId, ModuleSubStateType, ) +from opentrons_shared_data.deck import load as load_deck +from opentrons.protocols.api_support.deck_type import ( + STANDARD_OT3_DECK, +) + + +@pytest.fixture(scope="session") +def ot3_standard_deck_def() -> DeckDefinitionV5: + """Get the OT-2 standard deck definition.""" + return load_deck(STANDARD_OT3_DECK, 5) + + +def get_addressable_area_view( + loaded_addressable_areas_by_name: Optional[Dict[str, AddressableArea]] = None, + potential_cutout_fixtures_by_cutout_id: Optional[ + Dict[str, Set[PotentialCutoutFixture]] + ] = None, + deck_definition: Optional[DeckDefinitionV5] = None, + deck_configuration: Optional[DeckConfigurationType] = None, + robot_type: RobotType = "OT-3 Standard", + use_simulated_deck_config: bool = False, +) -> AddressableAreaView: + """Get a labware view test subject.""" + state = AddressableAreaState( + loaded_addressable_areas_by_name=loaded_addressable_areas_by_name or {}, + potential_cutout_fixtures_by_cutout_id=potential_cutout_fixtures_by_cutout_id + or {}, + deck_definition=deck_definition or cast(DeckDefinitionV5, {"otId": "fake"}), + deck_configuration=deck_configuration or [], + robot_type=robot_type, + use_simulated_deck_config=use_simulated_deck_config, + ) + + return AddressableAreaView(state=state) def make_module_view( @@ -332,41 +387,50 @@ def test_get_module_offset_for_ot2_standard( ) }, ) - assert subject.get_nominal_module_offset("module-id") == expected_offset + assert ( + subject.get_nominal_module_offset("module-id", get_addressable_area_view()) + == expected_offset + ) @pytest.mark.parametrize( - argnames=["module_def", "slot", "expected_offset"], + argnames=["module_def", "slot", "expected_offset", "deck_definition"], argvalues=[ ( lazy_fixture("tempdeck_v2_def"), DeckSlotName.SLOT_1.to_ot3_equivalent(), LabwareOffsetVector(x=0, y=0, z=9), + lazy_fixture("ot3_standard_deck_def"), ), ( lazy_fixture("tempdeck_v2_def"), DeckSlotName.SLOT_3.to_ot3_equivalent(), LabwareOffsetVector(x=0, y=0, z=9), + lazy_fixture("ot3_standard_deck_def"), ), ( lazy_fixture("thermocycler_v2_def"), DeckSlotName.SLOT_7.to_ot3_equivalent(), LabwareOffsetVector(x=-20.005, y=67.96, z=10.96), + lazy_fixture("ot3_standard_deck_def"), ), ( lazy_fixture("heater_shaker_v1_def"), DeckSlotName.SLOT_1.to_ot3_equivalent(), LabwareOffsetVector(x=0, y=0, z=18.95), + lazy_fixture("ot3_standard_deck_def"), ), ( lazy_fixture("heater_shaker_v1_def"), DeckSlotName.SLOT_3.to_ot3_equivalent(), LabwareOffsetVector(x=0, y=0, z=18.95), + lazy_fixture("ot3_standard_deck_def"), ), ( lazy_fixture("mag_block_v1_def"), - DeckSlotName.SLOT_2, + DeckSlotName.SLOT_2.to_ot3_equivalent(), LabwareOffsetVector(x=0, y=0, z=38.0), + lazy_fixture("ot3_standard_deck_def"), ), ], ) @@ -374,6 +438,7 @@ def test_get_module_offset_for_ot3_standard( module_def: ModuleDefinition, slot: DeckSlotName, expected_offset: LabwareOffsetVector, + deck_definition: DeckDefinitionV5, ) -> None: """It should return the correct labware offset for module in specified slot.""" subject = make_module_view( @@ -386,7 +451,16 @@ def test_get_module_offset_for_ot3_standard( ) }, ) - result_offset = subject.get_nominal_module_offset("module-id") + + result_offset = subject.get_nominal_module_offset( + "module-id", + get_addressable_area_view( + deck_configuration=None, + deck_definition=deck_definition, + use_simulated_deck_config=True, + ), + ) + assert (result_offset.x, result_offset.y, result_offset.z) == pytest.approx( (expected_offset.x, expected_offset.y, expected_offset.z) ) @@ -1767,10 +1841,20 @@ def test_get_default_gripper_offsets( @pytest.mark.parametrize( - argnames=["deck_type", "slot_name", "expected_highest_z"], + argnames=["deck_type", "slot_name", "expected_highest_z", "deck_definition"], argvalues=[ - (DeckType.OT2_STANDARD, DeckSlotName.SLOT_1, 84), - (DeckType.OT3_STANDARD, DeckSlotName.SLOT_D1, 12.91), + ( + DeckType.OT2_STANDARD, + DeckSlotName.SLOT_1, + 84, + lazy_fixture("ot3_standard_deck_def"), + ), + ( + DeckType.OT3_STANDARD, + DeckSlotName.SLOT_D1, + 12.91, + lazy_fixture("ot3_standard_deck_def"), + ), ], ) def test_get_module_highest_z( @@ -1778,6 +1862,7 @@ def test_get_module_highest_z( deck_type: DeckType, slot_name: DeckSlotName, expected_highest_z: float, + deck_definition: DeckDefinitionV5, ) -> None: """It should get the highest z point of the module.""" subject = make_module_view( @@ -1794,7 +1879,14 @@ def test_get_module_highest_z( }, ) assert isclose( - subject.get_module_highest_z(module_id="module-id"), + subject.get_module_highest_z( + module_id="module-id", + addressable_areas=get_addressable_area_view( + deck_configuration=None, + deck_definition=deck_definition, + use_simulated_deck_config=True, + ), + ), expected_highest_z, ) diff --git a/api/tests/opentrons/protocol_engine/state/test_state_store.py b/api/tests/opentrons/protocol_engine/state/test_state_store.py index 170f05bb4b9..515cbbd81e1 100644 --- a/api/tests/opentrons/protocol_engine/state/test_state_store.py +++ b/api/tests/opentrons/protocol_engine/state/test_state_store.py @@ -5,7 +5,7 @@ import pytest from decoy import Decoy -from opentrons_shared_data.deck.dev_types import DeckDefinitionV4 +from opentrons_shared_data.deck.dev_types import DeckDefinitionV5 from opentrons.protocol_engine.actions import PlayAction from opentrons.protocol_engine.state import State, StateStore, Config @@ -32,7 +32,7 @@ def engine_config() -> Config: @pytest.fixture def subject( change_notifier: ChangeNotifier, - ot2_standard_deck_def: DeckDefinitionV4, + ot2_standard_deck_def: DeckDefinitionV5, engine_config: Config, ) -> StateStore: """Get a StateStore test subject.""" diff --git a/api/tests/opentrons/protocol_engine/test_create_protocol_engine.py b/api/tests/opentrons/protocol_engine/test_create_protocol_engine.py index b509946de75..2f7a0cae441 100644 --- a/api/tests/opentrons/protocol_engine/test_create_protocol_engine.py +++ b/api/tests/opentrons/protocol_engine/test_create_protocol_engine.py @@ -2,8 +2,9 @@ import pytest from pytest_lazyfixture import lazy_fixture # type: ignore[import-untyped] -from opentrons_shared_data.deck.dev_types import DeckDefinitionV4 +from opentrons_shared_data.deck.dev_types import DeckDefinitionV5 from opentrons_shared_data.robot.dev_types import RobotType +from opentrons_shared_data.deck import load as load_deck from opentrons.calibration_storage.helpers import uri_from_details from opentrons.hardware_control import API as HardwareAPI @@ -18,6 +19,30 @@ from opentrons.protocol_engine.types import DeckSlotLocation, LoadedLabware from opentrons.types import DeckSlotName +from opentrons.protocols.api_support.deck_type import ( + STANDARD_OT2_DECK, + SHORT_TRASH_DECK, + STANDARD_OT3_DECK, +) + + +@pytest.fixture(scope="session") +def ot2_standard_deck_def() -> DeckDefinitionV5: + """Get the OT-2 standard deck definition.""" + return load_deck(STANDARD_OT2_DECK, 5) + + +@pytest.fixture(scope="session") +def ot2_short_trash_deck_def() -> DeckDefinitionV5: + """Get the OT-2 with short trash standard deck definition.""" + return load_deck(SHORT_TRASH_DECK, 5) + + +@pytest.fixture(scope="session") +def ot3_standard_deck_def() -> DeckDefinitionV5: + """Get the OT-2 standard deck definition.""" + return load_deck(STANDARD_OT3_DECK, 5) + @pytest.mark.parametrize( ( @@ -47,7 +72,7 @@ async def test_create_engine_initializes_state_with_no_fixed_trash( hardware_api: HardwareAPI, robot_type: RobotType, deck_type: DeckType, - expected_deck_def: DeckDefinitionV4, + expected_deck_def: DeckDefinitionV5, ) -> None: """It should load deck geometry data into the store on create.""" engine = await create_protocol_engine( @@ -102,7 +127,7 @@ async def test_create_engine_initializes_state_with_fixed_trash( hardware_api: HardwareAPI, robot_type: RobotType, deck_type: DeckType, - expected_deck_def: DeckDefinitionV4, + expected_deck_def: DeckDefinitionV5, expected_fixed_trash_def: LabwareDefinition, expected_fixed_trash_slot: DeckSlotName, ) -> None: diff --git a/app/src/assets/images/on-device-display/multiple_modules_modal.png b/app/src/assets/images/on-device-display/multiple_modules_modal.png deleted file mode 100644 index 721c7cb11f7f8e069540db1bc1a7d2d4e1428c31..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 85832 zcmd3t_fr$j^Z!v)L=;p6l@e5>Hw7VtP((yJNblt(y$hj-BA_B5M7ngTp(8C2AVfqu zB+^^xCDa56EfC1Z_k90}@6FuY-2SvTv$J#i+{Z@0)mFK6gY5e^MT+*IZd#z{?aB2ITKEPtam+!PH1XFtH)9;s>S5&_UM%-2lk1R=Na~p{i2JIzJ zs>!Y(c2+d-zKJvL{)w5mYA`2Q4gItc!i=8_RcmRn1!W(7n{oGgK4q zz1HJ+bp7e2caOR*yGT2t8~pMl^wgO<5j9w;M|CD zi#7RZbuh)EmN5kAI@wJ&9A;ADMD)UU@e;dKl5-U^>Rxy z)14_&vY%xU>IzR)NLzKaXT*;}jtDKd6l=LJe-J-4xL#&B-PMRHy0m$~R{k|0L8JPf-ZKjDnzcc1>tBSB+P;lh9kO6@sx#sN2dF zyYFGikO-da8NVG&W+V9CbyxKXejT7-QN{^PQ+Rz@W*e?h1Cy*QICQ&0&T*%N9ieIs z5n*%r`uDA)qhlgXjE%r_p5$z|>6m#P-nmF`rSy)N#j)g*rzS<8uedVbcsq8Mk{?CF z9w%Rj7^=;~LuLNGvGt|a2PJERgp9OZ@8S2!eP1|t&)^O7uIpDQlQdtVB6V31avtAMK**Cnj1j-;h|GWkEP zT)e6qcu->0yA!fZRLCsZ_yJ826oCn}HTjPbb5pBBR3zLdI2p=xKs4 z{d2NHKwwH={F8d+J8#7JvN{sr{83m}QdeJjaWpe!I5!J4QVW2nqPnP?PiS`I;^X6O zJcpfs4inB&>`|H6nANK1sc{LxF|Nv@xr-QzZbIdYDS&Rn6ZgG`Mr&?71rx_fdMyob zO#ElWO=ipVC6esr4_k~XLNq^%PZoZtB>88+t_L}H9`J7*IN7|T5pt?|$v;+A@9{_G zC(XqK7o1UL2rLaeUAcV!;e;92L`GXTxBh);7t9~^onG8S*C^%F`S;>uzZ{kER)#=n z*7Ju{+dtE&v-QcTI@&8V6P$O2X-v#hM^+D8llA?bFPXBPj&g6+0Mp173;3zS#$Dc>k-UzmBxTqSQ^AA|r zqz5kCyz+vFxk|DwK)GixLO@v&T$wXA9a6`;RjEFt1Zo@%F}c@34tDL!VBIwtRcrvftHgf!v>nxu!>hg>+7>uXLTjW)}xO?l`aA{!g==c}#bl@kcr}oI_!$z|{ z4Yz4dMpF?8wjEQTx5tQZ(}($2WE1eBG-&QKk-k~GwKju>!$tX`(t?AuYXu1 z>)O~VH4crQCnd|v1k}`(RO&vZcb!J+kod=yvJ5zjakkPLzWnYxm2sRgH~l$PBCfvA z&O2kK`LX@cocyRfmFCxk8+f?g*$9${_KG-+6VRHiziGB5-?lU_IJ_JPWL+h((Z0CD zP18hp7IAf}>R(1P^iVFhFSnE`ol!N_)!9<_;CK}?T8MBmac0|>C3>sFb2!sE+SJd_ zmxYkkU0v}d#_1jnvUr+fzIy&5Hdzhh_e5)vMuRm{bqd?h7je+nAvqfNgg$pQA6Uy8lO0G%zgb!7Rg2RdJ}QCF;*lDM-h!Oy<~v84W&curR6Qyw*G}a~ zHa()qa!XvxKpy?U0F8zu0Z_leP@9 z{HBk?oliDWQu}Eecd4E+%gZ0}NNkHot!q!_12bp0den(%R0ZRNpWi@q-0QS@glJH5 z^YlSz>7|EX$e|=?+9=E0m`7{7E}=zrvl^fsgL+tg@+J9kg#C ztl9{uS?0PAsp%L?y3?=6206N8cRP)(#g0T*k%5AN-zi-29=&lvYV~41Ul`^{_YMt-uuK=TYvBp@~R_O zw_hzJTd&kL7>T>mzl62;sb)x6q+QA+Pt4-J0~T0)6}VpP;xnZe8=kB|evCcHedFE_ z?k0A7Vf|#-{Rn~aUDw2zZr;>C2>@btX*6wbM6&r;uD`Nt3=H2H+gj z8>=Ja_^Oj{^#=oeszPrcogXRwNUmzBsV6u0lRUU@L@9V9bVjf89%Gg^{NU4cHqd{3 zv$w6Ps;cxIp&}(I-ixnUG9WLk1rg!D8&0syDkiR&;Ci0tnFXG@~Wn?zxVZ+FD zo77u-Ce=VGmR0^yrrAgEyhUb9}L=YO@=_8O8Jd;N4x1Hk49&i|&a3b70@@a>D*C z!hcJ5_>?^tbsOdv05b)+a`G*Cs#q)%y$xBuesrnraC`&Yhf3q;a(PW|n~}ag*N6KL={_>KmIlpa1un z<~@T6?|Urke&J*Y@jGd0%?|Uxn|P~wr+vs51)NiVKO0le5)^EYF<;D9^bXBYm{;fq z&Oc|q1E&J<01@q02WlwVeF_zvd=~d|@E877S1` zq&ul*a@OF&HwDxim7^=-I)h@@U;aGt){T9|wGv;PyEM<$;~H6~5J}N74R7!`g&vHo zNe&&sMGOVpFA|B!qI>=~GfmjDQND6421m-kd*84p#9d<4T0`$z7$dqESLGzvi7H1; zkA#_Uxt#1hvNRQcGZFVRbNj>LC65k;7jkqkPw`{$J9+$np|{wbPf2df*=x&rOpsR( zUxzldeY0tAc5(rqgcriY5+c`UPA2}=R#l9Os+l3?0v31y7^)7f96Vk$U@A3L^^>9L z{lZ^q4Qf6Hw3Rf#)+q9IN0sHc#;;n}Dpb0$oLkoGwYO?$J=Fxy0gNuWKk{=jm!vEp zFy?a&j~eawk3thuOGEJ@{-!x+G{9H6D_1JD2eIt;^U9INm61$qp*n#EL#=hoXQ=z& zT<(Qd;|0g?ab_2vPPTVnYtg3IwfFCrgevzrzpsd) zrd2n>h^`DG%+~N|CQ*ex*xp?8WVJgEgm0M8KDEk7XwDji0q(kf6Vz2)aA#cC|7Uzax*YkYQ- zMuV{Qn}H7A;jvL=2qNmaYCC#GXl?*jpPm}6Fp+zpI&ahnp#q~sb57aKcmb;^%W-r~ z+>6Bd`2%RlrVk=)9&Z-&E-?}-#d zMh)zJ8+IJL&eI;G7;)8A`|fSVM=DJPtBggZK^QJc-;IF?&xu0$bgBsHrSnb!Hz{-I> zNNu>PP0(fWiLEw2|F9 z5JnuUHLcRS>0zbGR@R%SFzr@_R8&6R{?d2QSq zaeO&jtR0IGWOrW1@}gtbsw|$l!oRR50nnnAYqPiY1QnL`&Af3fgq3xysl33lh4q*< z18Wh`4A^HLQxEKwMb9hAHvb)Ld@g$&h?Gk<63Xbf?kcp@Ofl6zCi&MZ zEg5<|UBIft+=jJAxUb6mliT*4(=q_`8NBu%nYKP~dDk#DJD!HWLbE@LnaZq1#f8x=b zrJ3z)-jl1cUGps-@o|w;ll<(*f;&44-QM?s*TzP$Sn-bpCCdE`YX%X?k!S%jYR+b& z%wro-LK1YMXO}lFg;qkzfw{}rI||x6yROWuel00sh|ECKzIr(2q2UbK$N$CciHPK&iOVy|(!%AEYkxdKJu<5L@Nq=h_Uu?G=j!2Lj2&Tk z+v}&Mq4ZOpwOcjk_<|P~ZZ-CeYkL||RjqF_GENTF_s_rfB-3fTtxP_ak<*ibcle*S zWuEvV;l!%} z7RtkF-cgdk|HNw&6rf_eSli$q4)P2!xa(o^NAKZ1ndDyw@f&yYw%^^8T_R|K(j&{$ zoG*e|G(J_ZUR;EI5$85ezZ&}G>1RJLK5TjL`@k0|F&e$k0^2{hBTLl*JI%Rb2UQ!x zZRy=Tx$f!k!ym4$WQ#U!h_QJuF0YcXO1ubdd%wJmly&&c6|7OtaQ6%p2kjgg7ud&n zX_y}JN+qV|P|t&Zq3wf);_|2Nxvfp?O9?PwLEv7P_S#`+Cb~gv&wo8wO3JMC%K&X6 zvR1bZ!$~31Xx;bTJKh+~@PcL5>V?#1yXiqtPa3muj`aYoZ z#-9@zChr!Kb|Rw&H|c_e*D`trYDZ@orP{NyJP{kyt_^?*bL6$IX=y zUNiR?hNd->0eY;Rp*~UD z(rhdrw?{NE$6j@93Ca`^+GThFL?#wF=s*sF3(;Gna0W}?AnlQ`g%9Fjc(f+drnz+4 z#}hxaOz?`TPN)t3THYtx6EMjWt52vdY+Wc_BrJd0EC(a$wv$&k=*UM3U?_tA!x+cX zjM=o6tpDn(aHJmVF^B6W|NG1p=krCieQh0!bzy_@i>cGew)q<2ARticctrfG{=qZ4 z{@LX8rp&+xvfiB=JxpiKm!Q8f?Nx$9xbfii;z&Q~h;xTZ#ED*@3ywh20 z5wq>T-@bHi)ckIWK8tfdM=Qv@?f1?Ycl~(%L*-}0qF{M)7@uq{tT#NtsJBU-lvm05 zMUd}rM>IvVx^i`hHu&v9DroglR=t^B*ojMpAI9;Z?-RrR!^Dwec3Zlpw2aX7LO|H( ziOs2}*$mVX3;86IJ^>Q)DfrWNG2Qu3?dF-%)guX?)y_c9 zN2zf6F15akRL*r(3$ZH=XZIlnPkr~1{720l2r!rs@zthFyR?I z()EJ*86;=G)gchgFT$3am}SvP4(e9Q`{S%LhW@>G>P)z|WucvDZ~k9;(t$Y{52=7o z>d&QJYgse=r1~zb{<$CCSXiARJ5P1|;;{C&7gx{`k02}}PE13+n)f|{Hdj|k-mtIh z19NF?t1C+t#iiCo(Tfn;z;as0w);stte?;WL}fRJ!p})BE(UJ)oF!AOMp5G<6|^i_ zH%I84FOtqQwB~@UlYfx!*((gV{Z%N5JB3_pw%?^iZ12Sq-zUT90BeoM4CgO`WsaMN z^L}%;-2xzW^oo}sWkBTGbfy)m3Zlp$05&9kf7Ul?PF_18On^GqB$76SRJjKV8=moP z6(VBiQ*`@p2SSUkJReb%%gkz@?vOY6>5w6S+x~X=JYxndr0Dr2a@#95^6&47s`RHb zXIR{W+))x=wv@~?+2#;g4q<%8Bd|Oq6h%t{wZWFxUY|NoNxTla#?-6WeYxUGKOGzE z@ltxNs2Fk2Lx;Bm0__U;0~cpa$|*U&x`}F8ksn#%Kue6*FQ}WVPt7k35Oc+kksNCm zYc~%edZ0gwDA$dxp!wwqd?qDBUJ6n>T3Ubq%P`we@#BPHu0UKr*lYN2#f!=*TcKSi z_zG(I&GB*lDn$jGSKWJ760vC*+KKH!3(aok;IMbmM=TEY5^4E%-(Awa>n!eX5kvWo zQ46d|==(i;3;5ds>R}O*2bx zH}lrnl^`C#2k>ut)7jCn!Ar!kSdNcv1eXv8ETZfxu^&;XE#CyK|2u!u_8WY;7sOra z{m7JkOZrdatV|t8(9wUHv|fje`?Dv9)>QJVf?=~nCBWE;jP=QLGpRI{JFPvqGXoUG z!rF9O-8)w{V_#XDrY(yaQh4`Uz#ZLQ23fWPfzaHXk$zKIC5`l~Ku)H;_m~zIQ)-66 zI{HwjL`-U~i8#@?-5f~O1L|_k4EE-$;WE52IpV#tjdd)gLrDyljnD70KLLlt%rpna zUfbIW%H1)AAU=IVh7L>ZpUeG;?$5fCu0X_kbuWh%s4n0VxK-8$;hRJpP#|pi`>9Uw zIi^(s*zX9PvweS=xJ>8GPjn;UR-guIdspwn_$RNq)~+5h7LG;UYR-?0d1~JOy?daV zPdO1^vb79JoDVt6DAvq;d8$Juvk3auuaMYlQJ|uY#ln$37{qw-%z%of0a1A%Eu>z2 zRm8Am`kBSlJF}nO585LnmjznhIBT%RekXyJtp<(BFBewJNl*)i13TWj9M{PyuNalf z9zSr23Opo}^_=&=PFrwG!~p4Z1gbA~r3jt6gRZa7SmMS_l=I66`y@nKnx>SNp8xvd zjfB6=G@NrV38twOk>gt*^XgjNjB%&nHu0lMNgL017Hi_W?d!Ll?u$76iuadnS!BTd zv|gBzR#jfQCY6;g^le6?JU`fFsUVa8@VGx*qdk!|R217HI18^2dAHU)eA{$G(){<# z!MI>UzKN|f=eSTS!P{#&cjY6Ywx2sB6_0xM*SE~3fJfzxrDYwObg5=AZYzuDJy`<4 z>8mn5Ju`z9tG_9KTy`Xd@a0$qF7yo?Ho?u-^zOLX`Stm^s!l}}jm3RJ`TCrE*JcB&TwbUyZbCFn!G-nl9=|kL^&7}Mo*R>D){jF zO<`8{GZ)$kvp39Xvj-Gp{&elOsH{?N2HVET7x>xyZmY{pbZ z%L6rs15>7etP&njddp~F{lb|Cfz*=_ep=P9;?7CAPf=7AJ2j0={LOI7^^G$OdQ^!b z8Nv2Py|4wCB@ZQ0xaZP0;EV>mL9-qu;;w_4qO}X^7=*)hAUB7H4P*xQMx$LD4%ESU z^g`cnf`-A4_nUaMO}63A0>KMA2w#h}e)JzYuri+s!$Q}HjD@-B{9AKVfF<7m5IV@?@S5csL$)9qlu_<@{dA^FAGX7dKIE~WU-!)_I50K5>@SXVQ;HTa2by4jrbA2^?Lb07) zpN#Y@-MuBZ%Bzyofn5X&*j|ToSz9`N{^+?l!&Ze&vel9IQ_~vl+0JYq?N{#P%l_xB zR=fEtv+1U?@3cAVe9AZro>;^+%QfsRynRNLc%)GV;d9U+1pYU%`1CC?oBCkj-D7yQ z{qzszqU}FS6+WMy8iO6Nhd~Tdjb@w+c5U_qh`wdkf{3LN#{C)J_-qv-^p7#xmM~b> zB0Y8Sm?avlQsuO-E#eEBsz5s9FUl!%I;)~2OgCq$mQkcV9iHIn)ZhA^B@+VJs82=O z_n~A_cGTa}y-UlvBXM9R#(jUqH)HQ{Xzkrs9U1@QG$PZoVtU1YlY3(jt;>(0wUZ%% z@tM)HNW7g9G}akt(M=~r+c~N_(a>lN-LT25td!Cyb@BL?k1%CX!zL%~2L7WTO8kN@ zZ{{}wAJ*n8PT`+~6@g$FfmPolzwGV&Crw69J?brD<@RnR=}*qs6?ZUG%eFy=H64mK zEV3S*)pSj^Ef=pO6vy)oC(Rbf$xT2E>`a2H1?o)x)bgvVfi}cl7K|~8*DQ)+)G3fa zJ%UTQQMzL)#IL33z`7(OX5OuAj(Qo`s<4>w$+76q`vwx|M{6%2Bxjt{D15aBsnQI- zowp=qU_AE_kPgCFC*o=arnc6;9VIGr{WR{9$v|!eXBK2}a-6uxRW{M~$C`jvLqv;Q zV=cF2SBmrPw#)nTxJ$3SjA%pM~?*+o6fY4mHz7QO~mihxn>nikt!7 zS?I7^HSx?D+qR-b@pQb`=uSy%mn#2p40wAx3vC!_W8|=g7MfBt(_1&~u0EIju&5@Z zK0m*2#7>b9Fni*o-oh;_v;Upr7q;cApN!_zAx`(`F#sSSx=7g8^!}?ZwIlneM#$0E z%5c!D4%yJ)3)RtqnM(gTD!~la$3Z+I6iC>8$D_k{1dryr1IP$GLY&l~x8s1x&Hy*6 zCV&eCOh+LaB{5bSe1Qhh7lXK&A4c*=PN}w08{PWqdIAzv26v(*6% zAa&D6M%>IPa`2yUt@LBj!5&H1P%CqC{lD@8pSV<($)>aKOre|)tCr}NS7e{1YutWp z7G!(7JCV%TJeRz=D*qz3xE>y({ZtsMK`L*kVA=o7-5X$wlkcTF=q~xS`6>8=muf$S zPG`2dV&Uy1{n2O7iVgkg9siUN_43ga1DR0Y*+fxgvA`A123hD-Z>+mHcNM#|&ejL( zy5#Yr74K^Nw`1DNtTtt1p*9lUMBM zC})dr@}4j@#w6a7uK@`O2>L*abqZ5f&M#pXpP?xkLdIsLPqOGb`1Uc%U(Z2bf931y zYWbnevzEiua-cbh>A=Ol>K>o5?uGx`ciai-QFnWgQ~7+|uJGS|J{IYf&W_mQivQx5 znLFp=mBy2|f@u5G1w>)T3uWw~InZIw54Qb$s zAn80=uAi+8#-ct|TNW&7+g39}wvrIB(4m;8*v~9dq`WyTr^KJ&^6?nYYv{#jWkZ-0LOw?Z?~BfrnN< z^aPGSlVLj6OH7KrMiN>&T2tyOPvQ-2a+$T3higm*cA9z@n%v>dr7!vT_@u&reNVSG z-@H;D;J{d0mXm4S8#(4>Kc!slv0*xXk14ToP`&-_?f=D*z>SX}QVIX$*2YHkkX>!b z=ke&LD$GfjOaq-RxJ1fsU|dhaFdlBQW{Pr#!wcWOFC9KxZLj=NZTfIP{3IdE{=2{kLn)m&lw%5iuL#znc&6Eb zk>eYY83N+QcsQ9{5kveIVXS-#)4p5DDQL4sw#IrO4^LnxE?(v69t`k@6ogn!`{CXC z-j?p_8Q4)J&R*#AMU&dezvhM@EV-nExGtR1G+FHM_Z*Irc&w58#PgTm0aCXrre-C1Q4sDDP?e^jZfC*>iCrGB< zgj3LzI`>Yg{$Wl0Nbl=Y$|RyJ;oW@f*muBCsy-pqoE7%>H9x@3*sR-*WFPt~lJbDZ zxi$Rhf5U5XnSpU+E;@3>jEikeHH4#=od;;HYA_%NW7gJqGn>{Jv(tK^LQq>xAWx~^ z?#za%4+_=choi#NaYDg%hG==w-Nj>meK3_#UmI?&xIi@0YOb-y;%NW;E)%d}zeB8dGcz8A z%8csN@w`r7=?nd$;t~o=VoIOCE44iP=@m>WdTQW_SnvlTf4)7@HZ7JYKoAw@sxdg_ z%dQ^%%xit4$6-r7Bx3(4Hk0(dwk9EYJL>Q@m19kF`_(f$>VIPon!l+Ezs~omrS{$ z7nNGmkFX+lN-Cb__71e~2aQJq&RyZ%hRPO3slBG(sqeb{^Mj|p zZMF43+47gGEH@+!MUJC_*|89@X_#Dp$aL|qu@Bn3#ZCQ2Q}GWS{Plc*T-_D0%kbIoq6l!iwW>Za*J}V}SRQqWc@ZBV)nojD+(G zxz*{ze{D?|lTS9~QhQ8q;)n_iumc*BIN+Tr>PYqT#LJJ*)ZgG(I)1ILuf$+&{)S_W zj#iy$Y<1W%I=QD%f`(Y)Km9tbsaJQ>h!b-i4NAAb##pyFV3V~qOn=mlhH%hPli-6- z!N$26;2gUf$OMW_L z&}tIRdRQT{zbclkQY5{_*Qb{~38O5*q)}ZKIjDCUJpJTP`C(zoYM_y%zxe%-g+~w} zk=0KuevW3qf53#+B*8AG;%%ADafzRhxw*MeP*6~+@O{-yA8A#;_DKCo)W;NUEXS4C z7RD~4#38e81HUvMWgKhz0`=MPbk5tHptzY75^c?7zy(0r+!omAvQrqlaZ{g_>rIR7 zzWu5Qxt#pymF^HK&|}*BGQ!G>n46q7+R*5K?QVh>aKW-(_K;ToL!uQ*WJL1#Iibk? zo7$X9kpA$%UE*E)qOVHmq}*Q*I^QXn&V7#gwl$J_Q6J2>GCDFmzQ@6}U}e+?##EXK z@$KBAgqPy6ZwjQi3n?w>O<8q^FCD6@XQ%f)+)ECu90xVkPc^7v#Nm+v!Bw->dQ3yC)d98tm>Z z5C{8?&-=dGideJqH_YskFaXIv(G%DOt8xgMfwfLL-A$v@&yj1d+56-2&UW_b$VhBq z05tAxk*zrsed4WBKG^kJq|v&P30aF?mbOp*iAA}mD;e7m>Jqzib0DUbVy;ts-YEr^ z(_?t0q|&yj7WE+96p*KXeh``U^?S2XZ#*dbwM_QSsSb)f>-VUYt25KnE}|AqKUcD< zUw?^stUg|LS?Hci;4E%SnyLD4R zIa5v>r9SW}7R4mqQYf&*juY z9U^=o5NN@DMh`qK#!tKSI1%9|;C}RLb9QCmqJ zgI1AP1n;Ee_%wKT*@qn9NaQ-J${wMhnwvmX#nm#we-D_HHh1W*so~9VLyY*f6rehS z|01)QZ0tiv+BH`rOckYOK>pvUdvCav+valhMUgihx~gwnAzWYaz1xfctR=jfqI!^5 ze;HeS?XR8D0yFjSI|X5^Yzy5De^_MApFQ8%cuffXSnJ8GFz;C9=X)WN@~z0XZzM=o zpTOOY}5Nu8g#PWjaZYYnhDT{9cPRfod;V9; z(pcSyN>tscs|^xw_={}0TYgQWyt1K2X{=MYd*^L)7f-oTiuk4%?Lcp0h33h%+#E@K zXZ**^!}oP6Rl-oA=x~2Q!z=z>8kRxsMyKs7?rFeNYwCbpd%j{0Uk*Oqha#7|^WzOi5D4J=$r>@%7?62iR3IXk_H1uMi_Q@`CcLFhz$8af zd4O3fd3#{8lt-^QpKF$ENIY{A$e!IyK&@*3e(6H&PyqiuGw&B_Gl&Yp zM-4Y>`NfH^&b(!uASJq%0Tzv9D4 zpNP)fOrv<%PGC)p3hV&Hw=;MU0PK>nvnzfQ_Lzq7b*X*lrKhc}Z7n)1!SXU2dx1cT zTWx922A~S`ETvnl=DEL5n|7d}(LZ7Q1#S#^08q|A)5XMS_5_Sp@3}O!>7AeAGT+kX z6j?uLJL&6Zu=16W>Kgky5qPi)2O$8x7tz)~___S==ibAtGIqOkXIgP3?+6sW5NdQ! z3{x<97%Qps^lpLAs`{R}??2*BXn9X2o@H#!4|1&ZqUb}=oKxn0s|4o^V2j>vuRWk%=iJbm3#0e+g+PRL{CFe{|!gw))z-Pv$xV}Df3 z;Oy)=v$)wKwi`!vJ8Jz<*&+$T{&iFN5g5X5&DGi`7XFkG)ce^3A#nJJCE)?sqb!AM zhHf7&!M~F&1?9GKObz}lp*B7cIFcE_r6)Mc&v}LN(5m9&*b>%M-{E5MRCK9f>OTZ~ zMM8D6bIOCYU%s#WVh7I|fHo~fi^;b{?>GBFq0kbK?#kg7zYqJy)x%F4&t5&rZj6W* zKQkLV*A(JhP>SE|?cG#{F}m17ZE+cdr5RBdE@8r0hIfP%%w|Z^V{~7g^APbNpN@Z| zMD&Ey)p={-4v+_>_uP!pe%4vw>=NNlu>X-A*vGLW4%jH{EmEx!@$<+kX*Cp&%hF?X zsY|PF5L=gyw;UvPC*Y1Fz_;gE-*2C5Sca|Rq5<+h0KBG#5*h*m=ReK;3<3@ySJ=M| z6nLeyb!CI@vg0`(2*e%>edk7v**A!(VZVIw_t~@O`8B{}(dW#O6wKsX~{TI_oDL3{DoK_!&*p+5FO$NS) zrB1xsS_;C!-pj5MZ~FVSCZS&{q@0V&qy~BZ8gBG(b}ctundX%WnnwMhA2T6vNh83B zhNa1pqR|ok;#;k^nhU$0o$evm_9h@#;H=|9F;QX%E z57<9Q{%X+6QFE}ZR3O4J<2Ob(eC9-7>hoG%ikK=sn9i(VbfNhPFMBmKgj^SxO7-<6 zFpq0ddeQwHw!-_6N_gr$BC&G&{u{${@(?-ja2H6-~J>F+heFv|!xhgVMk-S~%^M$y4k)8x6 z90{NO(lb6xmMg`hxMT*gd3I+>ZmjVTWmlbU@=Jeg)^P-r?8yQ_r2d_{b>Lp7bO=j< zF}tB2Sg%T$=N;s8d~))+ldrYAeMm#g5Eq|A9H9ZMKI-3Z?2_sJlJC$WfmatWCFbO+7rHRd%yzpLCPe2?$eSf+I+ zWe<(Js~;U_T!`*l&y`zjd88l^V!q6M-Y34_TbyVb6`YMnYNaA_{aeiBuyns6QYgvt z(S1mr>Uq5T%rS5c1!E|^p|18%CAHAGKY8QT(c#j^pt(040^mK`_%l2Spj7E5CA=f7 z9H}APZawBA3U|}4PYEU*^+kIYMl(;p)WJ$f>_jDZG5KZ74KjV6>CR%j(pk`u!T7%K z^tMQQtz2|&%RYekQ{|`?-$h&Vj{;3@Ed4q52xb`nTu`8P!8Oxpyi)d+k87UP(GB1y?$H_kl~GIjVeTZA)vr`>n2acvq+|+u;?hN!ekTa zm%6cLkc|2=^?a!_wzxQKeK^`mTfEK2`4}&~>%UT!(M1i-eDU#CO=Rxl38=}yHLSgSy(8ENRM#DNa(-r&LQ0z6oGgbJ zxo@stAMaXDpix#BoCy2?UGsB1`Xhc8dvA>V3%2@SirVH}E(DQ`cpW(nt$pr6Rlr3a z?fk=&1}-rj7mtCoBU9po-wF_IgJ0HzNt0tvahq ziK{15oE!ghb<=TEHxDZrUHSA$-v6~jA>`=%?`S6-16DK`t}>!uqtwxUnW3ZyA0?jK zQE-LF-uZm1@Hf}mWK(UVdr|zcwe();c6IP=89{U5D*_^NB=L6nOx4%Y@v~fozmjfQ zDur!ydW5EC8Nf{|!c`jB1h z6OfS36ZLxZ-rr*Lw)9Ptl_zTG(I8^)Xme`vIEG`Zvq>X~Ev7nmIL zbB?SN-$Nb`@UBy4PgLTwt)FBE2m+-%YMy8z977_QrWLfO63*>A--wX1=enzp^QQ2n z@^(pQq!#)x6r#IDfgGhF(Gmp6$OGMa__?geXh5uz6|Ibrb5|c ziSX$5@QaI32}jNNA_JLSgwJrG7#h!&Ht|htJ_R`~UhZI{{0}t0$@o@0sx*N;g5@v^ zLYu3A|HB#obU9A{0EnyWk^8Z^lRT?s<;ph#-UykjJv2wDxmG+!h!t6~ECAAZMOwzN z_wif7v2__?W($v72YL-!Gz~C;Z^gY?ypl<0X3E&z@HaZs1&`%Ru5U?8^0T1AnG2zI zbIJSbw+MHd`Q>`**V~lqI8s7ImBTYdR(Ig7X6GA`5ilXk!vTIMX@*9UjKYG=bv0#PHv!-Zi8?S#&^Sa-4*r1eu;(o z>$_16FD$fO1iaE0upd}vpX7oGa~|2|<~p-D2$k~0-=R%k%gdZC)bw3Y@j>)F)u?Td z?gV=9p1^V6=x59znwDHup5}%PtXMct3QDPle)T-ls`KyART0pxJ@EXT z*iP3`Lu*+zPAk>TFJdmukD47h{g)f{D4_G2aY5+3a!TEl8TD6Pg;X0U@`V`4+e2SG zN|kPI<|ibKctBgWG~}~U_$?vUEM4PI?goUT?(>OcS&}+W#^4)=^QlVc+N`1Q7uR1qB498)-*Ukr;+XIz>XH zq`L(HrMvN=M7pI@8b$`COKRvEq~p8sdEfP&v(|Us^R9KibJp2^4b1Gl@3``J{r0uB z1k&u8qYGxN#t2=J+FTJau9epA>F*5k~@p8oY+m~&!%7AI|^ta$gkqe+;HJ{COTM2J014S zm~wX4VA9_5(;-#fD~Wd87JUwDqYsujy0CHnve%w! zf;p7M=%*B^1+FghtPD+Z>SS_8~UOvaV zd!+Q$eqBP+(Q?_YzA|oBQ~TjY=PIJt(^Zj1bo`x&bm=lss=}mzs<@)bj%PyRS{7>? z@;V*|BKd06-Tn59<;KaxilgE9FqRpq@esAQ&YX=6t7GX_#jQ1$)Iz}#L3Be|fm^d` z8q9_oYn9uXYU7pp47a!S@omE6r4Xx~BVxme6nwnpqVfYQV z4ve=dE0HX<%MVj$l zL#ZlbXjaiB`mVcm3D08-DovYLB5lg}nM-+DGb-4=`okc8A zBQna`{QN({w9?mp_MY=xu8)l06L1hebl|N@h`FTET{Q^rB<(V_W!ZXHu_&>{s>pBh zQk^D(dg^YRQLWmb_SSO`Kim2+@jP20JD)mUBlkOI__WinMf9#@y@(U z=>$=_dT7)lJi8L8CYJ-l`iX)uVN4wm)&6XJ?jz}L7aC?^E=6xnXp?oiiV7TBp9~V5FLk~cQz<^ht%*OBpY>t#PP|QYS1oCD)jwqtMK-mS>=`QOPFYrem|&R5#TUy{$(XIfV71L+PWQt6wgwqzBVx;&j4f-P zFNKdhACF2@AN5}U7Ux_7XXRV+agL8>Ra(xLJ+FUXtw@sLRW>(r%d4lQg6Pqv{d0g% zvA|m};ia)wFJ>ao^~^4j8C^76o0qp~**pU{w58KjcJmv1 zvOSmwipDnDC)>qLY~d=b?|299r@eGNyTSZ`7Jabl6)3kAFQfDh@rS=FOy-okVegoc zJKgOj#gXXoOW8c{4`kJ=Fm<=sw`{5X=G*aPw~A;sea_=9 zv-p_E^{5^Z4pa=o6-&507B5%ZMUVm5}GG6%Z=+rlS3EVR_y!DyB_-=kC!6b9CHVmb%jzFxG#l{oEEn; z8fEUfBw`i3m*O(gr>?~edV74`vmnl;U8YIA?9I!nT zl0XL>PF1XQ`VE?VN2ta8o?UL?$ia?498c37@;nR+6k+0yJu&MIIsCHPSo8)~$Hjyv ze&JVPz_L3`J%8bAi=j<$wNcBBqC1T_dvaOQSf;ZDQ*_)r=c;9$|HRHMPwah$&R`ba zFYwfD?g`sKAEQwSTK26T)Cqns=0eM0xaZ|szTtVdN6>YR9G(}`{J*`!>N|Xr1J*K< zku{#)d@1VWoj&M81$TqXnc;HU_(NtDN}uY)Q$hupl7mc}&+i}%-=22#q0{fvvaX|+ zIunnUXBNMP#@vu0KHlRQ4^R<%$k~`}J1>TF6tnonMWDwN%;2Wy8IDrYy_`R^nAQ4h zLfdpp=}8#tJaVaBWCBAdvT-pQzp2k;9k%NfuP(+Fk?+42j_mOI-jn>#(6ue0*2|bU zflJiz3H`#;{re$dOia38S88oZkxz~P%*qL`DNwIpxQMNPjD_2fe?Rm{B`IxQaohJ$ zt=kIAR_b1tX4HW1mN1C?Ntx<7WJ*ioJMi+>|5)(wan|pXYYw6jGjY{_nw@df4729l z+RxHm$wqt@BSvi#ol%v3?jgM0^17^ca@wo2a^JQRzR2=M$H{v5%)LEHWRIv1cJ6JU zNWS=~I`YB7ziH&&XU54d>;Q+yCu`lt6FC{3(741WY3o@>y?*+Jq9p^SZH-B6NZ%7p zF5l!9)cD}3J1|5|l#XECZhkUxe%H-g^=s{1VBVzraZ}3n`I|NQawK3PjDR)$`P@WYZ z4(oU#w9!r@R5*om{Bt>^se_REF3ZJKS2N7Jy&y}>gVt?d!!u9i05$Kq9Ckm$+p|a#=iNi z&TzqZ6xqIyhQUsv$TSmjUd`QZGP721`nC6R(9HiNiSzS*1Tq!IS3Tx6vbnNS$~p1M z&X#lbkgEy#l$|fsEFDLDJC-Az3{4QR@(tDjZ<&B{BcP;Yco#hx!ibHIPsn5tG_rs*73&9;S*NVPEVEN zjr~3IGymo7U0=%0#j(Upx)06kM;>pZs1{u9aJq2EyM?~wbZGUle3O4aN>43Q%(mp5 z%W|>tPSdKPO5nEa6MEaIl4~2@(H+q&dW0|dtt(~WobJ5#sbm1U?Mr& zvhNTx)(uaH4)`vny=eLtwMM;7s7LS4!^rcikob$4(0ZJd8(y8_gPfdn_Acp;Mv(mv_R1H7ZY0eVuq@OeN~tpL+jHKv>y%dG{x)7ovQ0%(Hgm=|*4He=!R80EE_unRySNLyb$!;hgn&Bd-U> z^|6D)aaxOmJUvsE)5mP9n&V@0L}i>wa?8FaFHI|=w?RtGIa-|>i5AO!gDDDYEv@f+ zwEel?wCE1=z{1wb&vr>kw5J+rtqQ1vt%rVS5Vm60>T29jpGq-vPD%(+j0-nh+lDop zlq{&~=x*l+>z)g%KScG1Xyh!p`Bj=a1Vlac;?y2ig7x*qPXHv0=)-zW{U(c3$du?;hW=D`MNj6qrLel5#qXa}bUYFqZjlrWf3wuAU&o`e5`O3rpNLXX zT$4s^{bW=`dzkoF_dm)cePdw}!cf?oZ-D?J7>*Al8l<)S4wtX)O+d?^&_hRkbk* z)~(KDR~O?JG&Bi>gn>e-Qwfp4q@QDexv-Pir!c{;ExC_$o2!+@$!^GN&D+KEi)ztf zq@5pO-_opZd{WP=sv7?NLW%rk8B3acZI45bgo%-e3QVJ|y4*-Or(uzCQ|r7~&R{89 zP7H@}^~lnCx>5A^inOLwGPPp)w|3T?qJ3PS2=~pNsI85!9;P9}fopUj?pxPp2Ggu_ zO@1sR9aWX|l5#mRJvlik1$_+p z-WEn3koIE!aTi+hOnYKWrxlM`fM%gt`iY<>25E-v`;Xn30w#jy;^LD>FQxNlsGo%U zyeSe>H@xatW2%jQB6hfAio%V=At2}YQd>h?IKmo3 zx|6SnQVRh=K|u#kn6oGHL_kbzt*WralzUO6hLL(}b7MoE)atEl>B9tlz+6$+)@CIt z=O6iS^mCBeqJwrmqv}2G4Wns0f5t3r{WH;qxu|n$w|vdxS)3jd`^x6#<^&_YQe7RL z=cyvUcAM9a`Rw?7@SIhcq?^N(oX$LAAtQY|d#9*okAz^EipuKrgKkkVNZQ~{PmQ}7bkq#gHr)@fq2{PPGFIl) z$j($HA7tYfJ?~iWI>7GqH9@n#)1_mQj?vWNM zSUoMP3u`{Bv48w616db$sUIRum1)&|=Bpz10KL6wadFYRiVy0Y(^*+rlDMXprw5y= z$&Efw#hPP~YnlDj1rU}5Ou}=fBhtuPOap;Hj8$(wKKvrmA}-!$c+~FcO$NS?p&5R3 zA?6eBa}g{fs(A3{PY;7tM5Z1~IPxxOhW@A*-=!x>#-ZAKRS94F4@V6>4_ha!Kxwro zF3v^y>ACmC=IkxV>d92F_<(p5-|g#X=-ORUayw>OoH}t0&Yn5nT&f zf@58jx-M0=Dkl^Vq%LVM{&3oZHf})yM-4DA7F?6&*yQ9UUFym6MAS*1y4jz6cWmFV zul`ha+E|rw9ta#&dkqy89vBagU091IXau-UNlAGy7JtYdy04^wtZfV0gs3RyF-*R= zmH`FeAjZYUw@#s1>{!!0E-X6xPJA1(awIoBC|aM}_KZ{_3twJ($PfeE`>4z&jJ265eY@h>BlQ&S!w}=JHIty6ikVoAhse_-l03 zg@}YC;+CGv@a{3$*~J;tCEEkTH!)a(w>!{bgsq+dp`epsZ*OnJo;YZV)8MkBv>%FX zWv{dDjKt)A{rYvPtgMX2f1Qq7tU2UX29}^GIPDt22p)sLxu3+Gnaww!K&6`7Ob8q( zickNLb}oY;oJ_NfqVGh~6VDg>R))|$E0%gJyX`LGt!r+4B(UZT2wHY0JdBk?>xgFz z-Y#VCFo(2o!EF4zN(B?^>gq<`Jvm#Sa=oN{b>A6ck&nj>XHE^RVfvStzrw1IAB=>Q zaXZjN5TiepOleMzDT3wBNzK_dy+}txz|IA>1lE*f@1y7A@o0_rl{EJz>;CW(5!K>a zUfaD7h58>g@EA$e68%$ME!bw*dw!O4W^v&}n}LlBDYxvu{pT6tKwovOy3WL&qBZ#$ zn1k^nG25Gf`x6`*8frFKEj}1_6uzq8h6Tl=3XA4GU^{>RKrnK1o)0R7<{j=!hP2`R zn3!;<5SrLt9&TPV1hOVE#IY$bX7DxINmE^2|2{3P((Ty4C)}BLIBo-Vp?8r~+ouF! zLJ&s^@lO7)^1=)_ES!Vv(=Ss5AsDm9_1#*%N3CoQRq~cz?3WlpY)C5!@4ji7sBIT` zd|l2zS^|udRKk->lI1ps0}&BXbGu;!{!vCEE&;^OX#Gs!@S6gzsI7^kW93JqUtkwA zac^bx-6AdIz5DTdo1a8xt6i!OvblOU-yecB{9^pxYOeP!M0LZH6D5i+Z#)vik;s1} zmOv4EJwE>h-R&w4_S`BPv>`n%R4PZUuTOdDPt>4#} z<>y002DPx+iR914bi~!%D!d0)E0%KJ!R?v9dXvp>8q_Q^}Tq zcm#XiAxXktJt#RP<@MRM$)5J_z@I5Ur`E(i%lZm-yriFzPK*3Xa@d2BOE?4?_sgy8 z2QUpO*H6Df5+Qc>4?ohX?_BS{$j{Gb^EbWP$149LJ+VlmxJMWM)8ECPf)1|MFH9Wt zy6oOwkH31<)3?n zIth|^s;^Yaa5Zq|V5BJo1`cP6dBrT?mUyX+fFR3HRKbnTY=QPQbXOO1f9`o8#Gqx$ zkfx|Bmv2_&6bZ(ah<2NV&G3_P5L}I#Y`&ry$OgTdni|#xZ`C_`nun0DKxdg5dq(xG z|MO3NzZ+ZT9wxppzlH@BHcC%cm#M1X2GU@Z_DJj$cmdvMS%&QW#aoBb#yAK_%OTzr zUfAkSBy}ZSf8HN$*n(+&ETpD4WNNU5N|&E#f01fa=D(VYP%!c$C@9lYOXLnm)%mlW zJ2OI%7RjqE#<*yq4@NvdVM}LjWq92rdX^OY-B{Jz|FaRKr8y-@`6cZ84@BK{aE)&? zw=&KN2{e-K-M>%37z6hV0lpNDLjz|jxK0jS7Bq@8U*Ypgt4w|;L3X@4?EO7xB+j_i z=vJBeZE*c}q7TJRDQ52c4VipN0bA+sCuKU>(to2UInXZS?H}N&uD{!^QBqN5IX)HLE(gDTsir0w7Z>+)Z0yYz5@e>$ zeSdOunOLJ_e^A*rJX=^;D5pU7&n~%JTU+x*SUA9inbb5j6m?&uf`RF!bI*0pBV`?Q?P=g*pYo&Azr>*zJe4732% z@;f&urf6BpMwZDG#`1_pjkOvrg0Zhg+pEi##`z^5k>KYJ|t z=O@YP(e45;9y-Cu@29qxo6X$odp;LvAH~^=gBpu>?|dPnS76`dB&02~H31kZ?d26< z@y%y*)H{qUiivNcA~H1e`sUQQne+#Zx0BVIQ#g|^KtzJ3`IW{re!LsuV?}&j!*YG3 z$no<0aG3iX#hr4S9sV`DOHim5*iOs8@)&7^6&tKNl8UN{3x2^SkHZyKLw!qDJ7nL} z*~KM=H^xdkh<`&hp3am_b+7#8cyR1&nuuF;v3|XDIyy<(Q0}G8x8Hbb)yy^xJkqC! z+j*b@`qXLdPEq{ShpB22X}78Tm%aOVGFj@&yYxfYzJL6WL=qavYYG0DDiQI%@1$FJ zX8Yz%@N3v|0v5+L0&?F7*{tcIA)etKIF8tpC$ij)`*eT){2AESlUv|Sm~LvU`hurN z`JMJ+2jB7H%*{dKVc0?A;f%SBq;%ZkMRsg%k#z!0i$w~|!%>7rD8i5?i`rLKyC+@L z1J)Bt2b#i`mq*Q4bQmU4q%ac1i(DiaxLdTky`4wp&G-Q0}}H5A84FmiNs?|fkP>`+~l0_v6k zcYTeLWQ$48+0`Xfwc&mI_zA}2;Vu5glAMS5M5y`{Z57L}iv@;Qke<>Kbp{4*%zPO5 zgh-ik?9IIvnQXg{UCgz=2l>ZTX;3pWit6gW5L~PEq$si!VFF#!N3b}55ej%YLu+(& zbk)=ASimY#v5KK%Om2TPhhm9At6Sd{U+s{m%%MO*9j z_DtixxzB+Q@+Ec00-58<{(bk&s?j6#s=Bz4?*K4gz7gGP3i|r`W((80Hn11ME?fL) zO?9c&;`*)SnSPyLj>A)+2W?g_S+h4@2x1i%6!4Md=WIwFXu-VC z616wE6FAW9L>I=SA*{po^<#TYV3VxqyUc4Jz$sle#$H9Uv)-4_Q>*kkb*?)hV0 zx|h&w5^tAKTLqOq0d`Z2PT&INdSx|!ODm=XDN-6HO>q!qq-IAYq^ns#rcO=9vfGPZ zhtrxnwn?V%ho=y3;nLF53a?WUg6P#`0zT(sSo;N=uy!W$U32{T;*4EzgxmP=wPo_3|tw_@5_9c`gp4XZ5 z5kXq_fYfBHt*-KjhEs5*=FJ|3%|52umuqJ}*q*he9Fb`G+r#-s5* z!Z;pmXjf2|>??20G^U8{MMWrcR1Z&Woz7nJEu&+UjA_WB7UNz~F+^=9gLcOGhG9R< zP`cFQrF?W1KCNf4_p-TnP;zo||E$TvLZio_vdiJRnWXW53?&***_|L~`Ep2xRv<;3 zM$9vDZJkO!%v(|tXW9*|zh7kQ^^u8*DSoQ?h1@ff=xY8sRXIAuo*1T|&rGUN*S7H& zf4b1@)H6v*Ne9YzwP%Ff4H8~nUb#1OA{V+MFtcSaM#zeIi6$fGy^0CiYbj<^z{GKV zPur1S!giWK&d@l@0OF(GAbdSW${tI3`p)%huCi zdWC4892+yCeD^4av);K1SzE}zFMhEb@lB;j_gx7EtHA0u=rawabV-LJqras+bwcUc z+LsS7Xwgv@Ydwy_@zn-yq1V@n>v*q^Tg}#%)@zB~kOvKC{lGtuf)LwiIl++Fo?d!- zdcNj95&zK8ozy-P$_X`G2*O;ct{4zp#)mm%QL=#+FEQ}i;Gct&2NRyt*+WjLansfN zazW3ZVMAkIZe=7W8L+cni@gogKBBWUHuh9R;X{E}eie)LcO;`th0}&AA9n#)kDMY( zjn!#$&xLh7vOLP`^dKoN(;iOLj2_^milRXR$Hy-wX0|11W9TlE;}<_^Yi-TBqlH4m zXK2Oq;*;8EJ`!KDgSR)^8scw`*)8-hnJD^g)T-XtOYT1ZIXa4j85(?;!g-K%lPl&+ z1=8BeD%G(5+iKD^NQ)*Lf677p8Ja@kHn2a1zWbl#EV>h#&rIEiQ?~LXKLGM8j_D(mRv&LeN#jBC z_+wR#j9x|pI6o!l+C|Q;n8Pv$?R61jIeiUD_g6VW2O-Sqj|)`vzrFK=px(Rl7*w{g z5x{mCxtX(E8yg$V9A2%SwXNKhu={(rpEC%8kPWrya~I2XqTFKrUKM-vJ@{*GqU=}= z*#)opj=ud~_LBa3h5Utt0RI?frDRH1SdX=-tU zj1h9hU&iH- zTH7*>h;M2#U=x5rQKPR8x2F5*()Z)#vbf0VS}B@3x0h`78(ept{QW_`K(y=jyGvPJ zT>-?JW_-Un;A@Lbj99`R2mPT5Ngtg2jyBXl&Voz%?lLBtaJ46vMj@#%mA(FJTz9cm z)vZ<5ftjGG^t0F)JtakZ-SP6M9GlmyC&jZ{P(O%w&M#WN_AHLjJ zyW^L)G6=fby=ne@Mzj(`^jTpgJ&fC&8k6nhcl3d?0**}bvvgn_K15sS)0)UUeK($K) ztX1%?MazS(-_i}J*v~DU;5lir6qMM6Pm0(GEp-%jcsX; z?!i@1VC;6%$Mzk-+N}j+X*UD6$n=%X_Fr4C`H?OvM0PY9NJz2T8)raCOUn$6Q3IR? zXP(c>49LwMZU9!@Qaq01;k1h#y@Ar4A(&{crlF|9?CH!Q0VF5}kQcwBn<@8!$rJ&4 zjVc3B&>i9r1ZM@+)j?}()c`&PlV1V}tsozVJyv8_HI5kxIseQBGrn$l!j1pKxBoLS z{%=d-UdMu6a$;~w#++??dprM=CqM#W55*Elz%uohl8{TJzhsj-KR*ZA%Qb+F((f=M z9aaJD`seVl+D~@Jttr@B8ylvbJXg@TW9$qnOXGg$4tPi7?oSK?>aZ&KE__|$*FB+&z zv~`PzdtFbpbvZ|h(QCOpH4q9sY##j&DgXO3@O-9V4xjm8A{hi75ne%a!m-!?MM(ai ztyOQlYA7T}l)>)%tNrjgumYOOT%|*gV>!F*Snk}28cEh0W%T_O88lgKeFGQUM0>>p zTZ^c}oP{w2X&-&WVMp1r9clDzyjO6J)KM+}4pw0hgZenw0u6j%ui#=bq*=E!KI?(9 zYB^IYITi)f;&TONRXl%i^Y1SGuYsQb&$iI+Iut)pZ5{f8?&rvebofFf+Kr#*c3yhA z5DQHNMMJorAZdQKGRqrpagPIzu&^*rSK3yiA5$`F!x~>pO7hA%w^al9}}bI zIj!Qp5GHiPpe8prsM^LF0P}BJo6Tvr=jI-UMMS93MP*j0s;f&~K`uXj7%9fZC7~ac z=o%W*HXFn^0%&M$t?t{(Iu@jSvjhDr3OzhF)=~D<*gIj=%GLFKv0kk_q)ZKTp1stx zkG$^Bi0-N%{tW!?-Mg`hc~?5RPjb0)NGLdWId6DI*ktfPTf=Pb2n0sdY2rTNR0}-- zG&-E|$ZtGn8eT!X<_L3pc-1Zo}4(n$bAr_je z_%RYsopQ8(yJ_;%57`B6ZEg7M@!;#9lasnTPhDo*n5!zSQjX8B@+Z;MTB2_ZG$^B4 zuftakzVC(=gzO^Tz8UJ;`%~hR2J+Ws{y8TnC&y8Jmx0>mz?&o_DN)%%Gb^77DY$l! zHpAaaGz6pEN9$+Q_nlPpV`09LT`yN!@E{}BU}PUN!9NVSAp9now(Cl(G)K}nG`*zQ z-`{_S(mFZC{p(APWI=4S`Yd*4U}9q8pJF}%fu4|_UJrpK@5TL7kXG(%0tAo$>C?CF zdYTAg`S=uu#>2*KfkV;9kB#IUfvp2T3)+*{{qUfsIc?^A>oAdlcOAr=nY3qf_=NGo z*b-O3ny1P72$TEe>8#Aqo19~PIfZQE%d#$-EJOmAozhAv%#cz725)C?OyS+DNW`F# z!&kxX+SZm}LY#r->Ss@IrV>zEzGehT$^O^gDLgo;x!h+kB#3wM6b9e7*DWJFxG*B~~3r9KZ+UXDyhWd^bFk!q`4q3cHHTPnLyf zc_K)lc>KSJ*m-lT#VuA$eQe(Y;e%)xe1Y8|?tMS|^}seWn-~U8opmYOQH&m6$!c35 z#xsRgt%*5@Il4PIP1q$Fred%BV{A+|XTNdV3n{Yof(|Ci{i1NGv%Iuk0#W$&CH^w(_-gI}^xalDngQ3T?f3YIzm+tIK zd69T$9U(FMYXv3AI!AGPBG!}&t8x8`(eX1QnueQ;2IED!SQK~fz8Y53kwc1L`u%-k z0!S^!YHVl4VvKq)30+vw95WS<7=qESUp?jthr?(`m1X<>grzw}*z~_yTT0@7aq%H( z3nGf(H_GD%RGL&ISlTopwNU5NT7B${G3j^92#nACM2fSROU-2pb779XD%=OI=$tqYc={ef?YjIvSAv zgajJpVJ6+(5u+pq?~0&Bt8h5lV-?~_18I4&wq@wKE2a2UXMmc~q9up;5get$=4~M7o1kvM;wQmhO<4iHhJ_i;fY>^rUpq2vAoetjML>` z@T-47T9kjcVDE``lpLj1eic%%x}2kQ1GHuT$ZpYMJ;@VOVXDraWXpMZwNBDCGwvz* zyAyifKmDr(V7eF+3mi_#VHv?zD5*K4T!rYrox z&zYIQh|eRA{_y7lBAx^NeeU~1wy3h#UHwg3{|SDH2ACt$KhK^G_#nhjD`+40?aP(q zGCXpbvikt**=q!_ll4=(`S+{V96|f#-phPG$~(69KYomkBAWNun~k@`eaH~2-$gu+ zXfXG(^Qt2Ir4wxv@8-qR9XbHhlY+~RkS=DZjczpfA$$Y!u8f+;gg+2U?07uuhl9U- z#>Gx(rb!WtKHO=~$lLZr3y(?T7!h1)wyfM@w$Mv=VsQX60gv!=-~Ps`Q>?VUJ0-2# z;_ZsHpQ?RO-xSvP2R1}J)!>#3T}_pfCEt00&!3Y1+-UCRv%OSPx15B7o!{+_?L5y6qFAE%11~OZ=h?cJB&OWR;0(GzEI}mcGNh zb~-*jK54c@zuxR=t%;+Ypp0T3pWRW@>6XA!=&xT%jW-TA#x2=y0$acV5)NOI9{a}f z7bP3_8Abou+!RVGzir`|^C(U=8oVfJxVy(9KV5b8ICe6o4~|WLS}1$mb+9@4rJVQe zfu`G>AHF=G`>@n9K8&V@AAb}H|N6C!tTyM<3!CO!Feq|>m*m0I7bIdwk*wmk?dKjj zkejd?!JsD&%luf-k$!ELd6%8Mzq+Po0w%R{MMm5`nReT9c{f`%X=z^a^TTQTKF&&8 zL#Ecc-A*bCfY9Rp3zVy&#(G0le0K1CB${^!n{~V;tnd3= zWHi^C;Bbj1WL%!1R~LI?n98E?^xmFcRWb&wcv(fWRbzaRqKAo~g^TzZPF!ez%?TL8 zTPB4*ui&yWp0zFSw2K{*%U}}n8J4iV}cuPosG7(J_3R%qp+7KJb!Ss zFw307p;&s5GMl6KSeW}JL=K|hmM#$_AJGg&^O4jr*~!cy@zCVVfip(Pq9X@971j$N zN|-4+@A(hBX# zjH1coI;T^o2~sTCyBczL8#q~+donyZ8?#Xuci@_nM2)jKq=5KG3;~$IQlnjG@EJbj zI~!~1nSOeBhgmxi9SSdJ4lw~Wnf}{2e=$mys0{?i@8(6sUJ~ zTUHCKaOH=Og*arh*iBjOb%^t~6DH2Bv@`j4?Ap&)Sd`fER3_({}34uj}X}fgN6TKB%UJ};&-k)!`B$-4KQ1P5jX4w{;-40FY z^mbs48P))}ni3r|WuOiDR=q!Jcv2e?6eL}N<>{SdWY!u!-t}Byf_9ebtM8}9Qmi)TiyY3 z&)mfvm{t0)f*bH`eNHC_7PNvzXp{CiPpct_0}xo|7k9fI0NiNxDIR^f<&S;pSmzO31L=F-Qqifl(;$SaOlbYJv7SZ@{7UBnWfgphW$ItKCu$CPB z!#**Mh^tZVO)1K);$p6033P&!Y{XK^%)Z$$)Qi=!xM)R3$1YS=wV8^!xA^e6qJ7H8 zD8~2Ob?aOepL1fR5@j z^u#dt`++V}e{rqvJ=P#I9%3(PFcu^sS4DA_X{W#53hIpJudW{4(q*}O9aJUgAT8>@ zv16yUplR%{q-?*zkwXM`Hb5Q24u-On_E-j%+(M)oR9^{x3Be9`CR|)bPhA$ z2E9LB)$iZha&c`#7etn5&Gsl8<_yC^QLw?^rnkZ_! z7Hv>5VQB+*Deh+rAUIiXzOckJEV}>e+zK9sO#^4AkamtiiYxv3%U_}+zfT~8kxnUPoGxx~KzQ%#9;-MyrF=6i_>H()2= z{aqkLFL4fo%Cc8r`y=~QU)GG+Y&io-n0~!rN65l`%u}ou)SYMAb2Ak>HunxDs~a1# z)U_x8EKe4f6BYV`ZD7$tyNs>EZvj{6+j*{@6h&=Q5jAf2X7_T`IG#wF^Fa2xmBOX* zRq2Miu@G_p{{7#R`YDZHvi;&mU*!h`dye-kAqI4PoiJ4r4|jAaxD7nhG9PxB`$&2k zB#Y!x*?enl;EML`O%;IOs_2Ph_&kRQMv4XA=I|wr4Cz7jvBYE{p2& zwsRaSP7RidE3f^fLnJUE(Hr9xxKNJY=C^-QzW*DN!vBWc^#jza#hb*R=lE0sDtI+1 zcnurt#)o!XsNloV%^YKGa#*-zl%a6M;gH>jbH|Jo>&|PE#>NLtb`dAiw-QgXOugkp*JvUq@0k}{SI?2LcoDw zs*PqKY(YN8zrrDt0WQ$E`S-4tSiARoK|{~KIs5+--TZIDD5YvtR&w|xGtWU~X*f)% zdT6A`=}wsR0A2+kzF6^o)SvGQ*a(@g<5Eyi7^<=yedFlp59(s(7c-L{pl-UF{kkjs zHWd3|>L~T@b=)t-#i7+!btrez>Mw9h_=afm?s~apfMjtB73^h6kmkqCu?udJ_lQtDbjPAGwR& zU`U_WR#q~TlPQPaw0F|yuz)QWv1e?YyO)N`BJ)|AqTY-hWh=x7%0q&r3|gi6vi^)8b-NjK z$Ho>#iSbnMZYYOy^a*DDDBXppd^)5Qqy#@&l^wqqe>8oRU3h()Pr|M3&;=7IcptN2 zl+{`;Qkrj)5E>Xpf)Rvz?=KrzlcSgx8iT^{rX~MMEZ`0SjVEex(yy>s`1$jWMbW?v zoqc+$J-kQZfT1y{Ts7e=w$|d_I1Z$xJ3hv-`!q>DIb*yrGc;th{MYpZT$~+oPXtzP zB!fzASSA0D@gU(-7Z|5w;i5vuLqw!om zDfhjJ-*^_ose}dj&YLd$>FDT?XEn<2-ZWdNVo;Bg?ci_-M?HfsTWW4+-Zb$L2Q63; z=~_!Mox02RJ?GuQ@u{V@_aF&71`Wg>ecG$<;TlD;Q{L6XoHeg+LdD!ygaW3pJcg$z zS2K@iqjjx=jk6on(4!D?Ic4x-`}nY5h^(Xs zDmvSz38%3^!n$n1(o~R9lU0{ywIjEWRLh@lmsUBF&QeFVe6(Q6cB?X1zNn(X95?u29~?Hz5`5?_v5D(+mQ?e{ngx1DIiB; z9lf8`_^cy$uxs2(2#s0wa=z-}QpSbW0l(ju#6)w2y@r(7Z7SsbC4v$Ql9PHG-)p@u zHrbiE2K{>%EQ`{<&-{-wL=0lnpcb=e{piNjd8g`Fm3aY;JGfdNSE2ty-M5-HybfhQ zjBr|(Bh!3z!U?5KXoUgNFap3jh?Zs4;JZ}>%YinQZx~m-5_oIgy8UK94hv{K2^D8q z1?X=P(YU|BGU8)7)Jn6qjWoe3~G=a^jb??704S`r!GV{WK&bn16CG@QRkla+CbV{B1*(h!3wlU=PDGkyOjS31gXj< zIMY{MGJ*^6vRO)S02dh89ehln91^^wv{XFdNHf;Irh09g_HkZ5D}y!_ zoGtF3kPy2oX!;1U4uvus{_N|`4@SR=dDq(ch;d3%a&+bF_#O@3le|pU?HhOjOp3bkfm-^~ir;tdgm}{8RmYhq{tvoPtQ*P zaDValo}Np+rU1KVYPHuNpnpKmFG!wOSI0I{Zb}&V{Bx2b4+{xt9-~{l7AK|%G`WNc z8VdfxgQ!4;F&3+s>N=iUTyt`Q2OMNpgy&-W%fWQF<@@ft-ziq(Gz}g>zQ1-5o`a(R zp0xFtoLm{vwH1IH)VeQ5*{YhGGlTlJA7CdN0hb0rEp*vmpM3>JBGm8ARghSEJvJPt z#U2PI_(SV`|7t85B*6m&7yQZbT)MRM3m$X_#3g@EH3^i2VL^9*_Wu3zO1%H`%oolM zuOVOKhdSUJaH3#u+`rd(Hr^IpTS~rDi>)Q<8DYdZ=WuNa`>H6sfxymT%3x-_)`q@e zn$E>exa-eqba*LW-0z@WPK(8LW{lRtI zel!83VNTzH_9R`v#tI=>^75s<5%{INykWkIp@uYLsSM|DAUQ(a#nY(0R9C0=gjz66 zy4Hbul3KP0!>W1j_BECw`v~Om;+z;J^-JM3>~J@(r8^~{4m5RD2h!Lwr9x>T>{YiG zMI+JvL$px$#2Zs5(gmyfGm`y^6efBDoWcip?%-Ta`hQO)5EQgp@FL}zKpq;}*~Q$A z(Yp{8aOk~z{fSus9H##AJu5Y_8p|DCD#hMe;Al3phq-c8S;;DxnV9;KK%qg2BtvIC zsZUOR(B=kp$jknY4G=nGLq6@d?@HX?6B!_KN+u1#4GRxXlAd%LUKX*)6a|=$vlxu~ zR0yV^sAz{{lOiob8HXp*M&#OkMhrm(%y!z*c`uAZW4^M+7{MC`J6=V+t~Q^yx3Oom zU#L2Zb>-~Q$W`nAt)>5plxtGD0dI#27*Nv}ltvbXF+KA3cO$wiTwQI*VC z)}}PLVxj+X_0pLvgB2QBImY!&-mW4vG*`oxPrR+ z6;HYYP1gh1C&g9`H1}5Wc6YypjOIitOe_Kg6bey){Tiohg9RilWX)LrUJkGP&q+YQ zsOusdOkeT2<#YEJGSZ#L-l{LP?|%lM71cHy;Be=_Su^@SJfNmdW}P%(kM8($Nb&;& z@&NWzlav&V22Ka?P)o5aaO^B=jn#(Zbp*Nbvh`c7I& zyp5QIxfz6K<$4cH($$xp6Ko1A1dftuYcB_<%&PgE@(q5;N5>^aa^llO>-DtCl?hyc zl2)bdtoKE2VEjTy2-#nPG2qRYg4bOjqaWT)F)}qS5d28oWAXX4r$)J9@I88|!*T#V zoehIGEMIVjkN>{5;H01s1}`-(yT{oDJl9HF;78yB<3V@=Frd-xbPwWT8L>STjID*d zQo4u*_!t z1|Q~2bWeU#$dGEM>L)9L(6d;B-)n1cR7NeWt#d#X%*(Osfy)njk+U820K-;=Ig%7= zv5#XDGheP#BzMY4x~t!%P^d>7@~ixdvKidjgUO(eE#RYdZP`5wK9G39LcSWjd;dRZ z`s%Q#m-g>fkdzP!SwN(uK@pHH$)&ryTT&X4kdRnlLAtw_k_MITrAt)0L{drL+4KC~ zf6nE(E_U}DGxyB>sVQ%13IEl8qT+Yxzp6NVw4S7j0$2~KAYKdxseOjtrZUDh^8V6@ ztK+pkmxBjtuz@_&cTD)2R=UYUr)D;O&qcX~Zl*Hyqtr+QS#t)fH5!Ro@!@-{MbDY> zA^bE7L5zKy@pyv)?5z07A3Kuw@v1c3IjyJDb|HM+CW?cm-daj33qG7QmPdcTo*w_g zk9QcfO#H~z%+27ZXH0As2=Jlbl&oh7$DbCUx1S5^Qu~A+-A}_*sb|9#oYc>}u|Yr(;!8ECn0VetH>8Y{5`;x{TxsyZ+ z^SA|{aL}O&=o|U*gGuRQLid*!_?g|>-8XnTPEj$Q*MHsa{_jEKD#%)S+F4kXe}Alt z!eAxvw6rYHKVUN~)_}QZXgIJ8PIY(Z`ig`TK3?yZN^v6!C%OJeFSF9bJI@pRqk#6N zl$Bt_$x6xQd5o@!pbQ`?G8L*B2QdMbsNh6y*k>#oQ<$lGUX)1E))+N6lzgj@@%msf z!B;7~>1d#VQ{mzGt2{OJW=^IL?}=#np9?FJe@ZC44@p526U>FWo_QB?Y3}VKn2xNt z98E64ErmTP8sw~&aA`TL9Pn>nEWjNplT=EWp^_^(n35X!CY4{VNTcblmY0n^j8Qz; z5`Jnqf#;Mb4;geJ-*q)QIT?;Meloimz|!!wq4G{+`@P!MLnpHiAyH0kPn@OL5bLWi zP703YkcI2Q^%8wn>bPJW>wh-Zp-fiiX%SUG1zoSva_ej0Kqx`+3a|(e>HNOTU@vX$ z-uQt7fgG`Vj#Q{+G{ooJ;W{BK{DHWDC^}6_0U1`F|6xa}7*NuVSAJ%)4l%tL(%rsV z4W^Fehy2a{pf?!V_O)KAu}8XRc(62)!-g&k=E#|0rO-}@$>V{<*A-MwR%X!jf)SB!@iUjzK=2e%;pmqwQD)M-y#lu+3 zF3FOWHd$d@W2-CSPv@UNo9Xg`0!sk7g~9kArsOCq8!G2N^%w{IoC-flincC3Cl=$N zc3`0Bn@F-|Q?oL8?a+O77WVrlpMGDqci9RLF|v--$Pc{v-hC^0WeGN<@Xniq;HUq| zDIl~7k3UN#B{NaN6;K+(UDL>IN6Afsf#_h3U7q01kp1zW>DiOvX}#$efouq{pIBB%yM(k`1NRCHvXt11oG({($J37Kub#KbD<`ToWVeKLECtvB-vRJ z*V5%KOT6|_M-hW6jD`X=J8Ppu;N;@M-=(OvA|G%*ty~B$B{8Z?cL*pY@sW8_5Bf5s$beNu?f#%~%p6kW+)rx`2Xi zwNYMPUXRSnNlb`d@WRbyF`oBb1$Ty`WfWZ#d5i%E6L?n{&xOkIKh#=pj&YG25*chP(NAM%FM>nVM9?MT3HUV=nKTThji-CYP=P1c9jLn^D8yVw10_jG5%9AI7_t#qWgkjiQAdO2uR|od(GeI)1c^OrASvLfbbB$< zL@khI9}n*rel0{IEo>fs+fyDgDcmodz}&UcAo=>4%H*+l&`6~bBJi<>J4*(m;!c4R z|8-q_;Bs=&z=fCBS;L#d(PhU)xpDD-B7iRq-%_ZI^QbKUs9ViBoIW?L1*es-jFtXe zyf5;GE^Z$rVGZTHXR4s__S`Sn+$7huUK1h#RebjMR4=ui*gY#Orvo*YlV$|a) z_vnCOoD7<9h5NaLb3ABQGw#}V){gw}VG!#hAZ(vFFEs(>e_|xsVkFxPhORUT>5dg1 z)|X^Xijkp8YCVOZC(g1gr2JQ;o7|o(G`^^B>#4Y~cV0l8lSU%914+U|%b`DxacMn- zaI9WZ;9_)t82P&PIVx&IGqEH8S}KuN;T;~lD1;(Iz*xTNy%tIB@=eXUpdteidM&AE zoFz;D!l)4CDRmLf7@*;$7MV=28E-adwBY|sP^4bqJ!7KAV*%jRg470p`B@DDG?hUS zBg9JmM(W-%08W)^H@36e z^b>wQ%=wGiaqCCXak*PjOi^20JX@LkX0Hm6^7t)d-@m^VR95;hPT6tP7EV$rWnk!< z`{g~+6tKr_j@Bo(7#+9M`h3sY^cw6?)<@l3Uz`AcB1zGQ@fNG#GDBBUX$r1(G3E=T zb5smiI*x=Iw>KCEog5r8FO10>6E5l?weDaD=tw}fG)q(_qaHHbt#$@33-G0#6fvv$ zNul))Lz{)>sj@22!+m5Ys{XQ}|)% z$wfehHsM&)A2kS|I^~+%qA6oguqv&vblA4}c2+RJX!A>{CrVU=< zW+Pm?!fMBR`Ua!hA(>umD-l!4VsU+az1_V{iU}3;q-Zly>-_wDH0?dpzp;bgiOU*4 zTNzWOWPd#)A~*8_U_|BTKVAOvE z^);Pldf*Pq`b9Jlyl<@Xg}j4`Jw7Zv_Aa}mtVWM;4mO4YXsLbVePuRMDEpYBcqS7Ap! z=wiSE*?6UqPhTJm2%>AHr?QGX0#|LegdB_^2Ky4$=U$TWtPmsG;n?vDKq3uVeg0rG z=zbuT*fg{J%Maeq;!(laa=e!L{B7*l)s+oMMi@twpVRzO%}p;S#XdHVI}-svK)LA^qMa0<3kXPWSXfzoxV*f? zh0J1uE6+(VnLIjD|3yagXMMKs^Y|w-+MAhmH^<0Yu`2?xht&{eojP#i$@0W2Y`y^Y^97D2 zuDZ#ZqBn{6VS+C=P3~0EqIV(mKn7`c(MXrv&!jK|L~bM0n2@)lFrm-D2pbz?JxSX& z^b|&a`U)n{*AV)^C-Sf1b_HvveHEz63VpL{sw|ssGS& zNOWS_yr{#uIv|2Rz~PQIJ+%AW_aOR&caqCS3V)tPKo#7m-*XHfZ{VaTeNW+esDJR9 zK?9aHCq|6(F&LwJ$ocEvcH*d*H1)vXI1+`Q%qP7=f_IWXa+H`oi~M`v8l0+@%*i@F z@as#1I^gcchvRWmD#1Y=4bvyC(oN18>uk{2^x%RA*m^Cok8&s;#;aa>KTg@NW0}Dw z=^93O1}R*z)KLOZ@;?S!1NpQhmj9i$v9UAsVHYJ8)z(y>v7lOZYCtyQARxYrA7H;l zEJPmpJ}=WkHRFEpgen*%sW-Vlt=Nx{qVq^>BKaRNNN?U?hMEK5>6q?HiHYQPc|>>fEaD5uZTXM3ivR0N zX&~s+O^pG0dtr4?vWqL!MPJX0$gBh1-9JFk}4ArN)`;tkhex8HFX2`AnQ{9 z{Yb48vMPeMc@{(yU_ZFiE$6h9UtV6yO|>`LP(lEasIxkSSrnMc)v~cOkmB6~W0A<) zAyiobCUiIC3GlldCjpLDrM5gE%}jwNn1!@>agkGt(CDQzpujD9rPETR_dUSmW6oNE=@RMxxqSXc zA0J5DgU|?ZRIwSQ@7uF*S851p5w#-b9ms5`7IrHaaDhER^JMYzOz)GGArJ|{Baf)u zjU^t*x3PG@eM0)=>|dNEfV7Lx7zr9ow;m6W9EV6mqU=+zC1j^~GW?$goO}cbv2o4c z_KzdqjBhy(Q%HK`3oVQ(2qgm|Mu84Y;!af+Cs@B-T0usGohb0vO=zl$%B-zfcJ2`b zE-ih7adIw=u$cxuGVs1n0b6M0i%4-D)A{Z^A23DQ!Ll#XZ)x_tATo9=iUN zEs5)F#J|UHc~LJ6fol)gevH7)lCnNH-4iKs%AjY*t=?j51mBPbCc*&2$%pEGjU9lH zCz%f3@&>`GA!a8n;5Jy(e9dn~3x}Gx=dm!&0?k@@AUPj1GXWPRa1nbtG_*~olLs*5 z%&#!?dF|G%P&ez@fzO3v1I(gGD&psNp-#_-gm4zQQsBXPT#MaKDt7cZ4< zE`B@#bR;C@NgXQQ+6UFd2*eTp_YH-2P95Jdu}hhQ0$}Dw!hZA7Ou;v|QVk!Fq;g6S zF}jYJAdVjiUtu~#r)!d-$sL7LA9W#++wXIFtQFJMZ?GSu?Oeba_GBEW1;Acg71(0c zJx+8CxD$;o>cU=~uO}waKJ4!76x%&GU;-AfYTW>?6xu(}NC7q1*WW+K7s2p92V-xn^w>iBKFBU{AgE;n3?`ib8I%F4$A}3->2t%DQtL-ng!uKV9bBV0MZmH zv1IJKkmPz#1Iq?l2v`)mKZ~eZ9c(!0fwtzM-G`K@3aOWKn3=tTa>q;{D4y?Frf7&#IXuqJlF&qUZVdn z0>$mix*tjFLtf88L097t?;jm8(UV6)X8%*TCy+>rCqCe)d<^cReFP|Ne-@8);9KC2s-sDBD!Hn6YPQ#!a-kgVu7m9i99lZ>kyrYYd3ec)KSrTk}pb zc&rO7e6js*cbCs|-{P8af)Vy9u2AVqjpF9hB!$W+IFQ=ANTs%~s^TelHs{7)f6#Ut ze13T!(C}Otdvfy_U|v}>dJb7o1X<~ypGJIv)av{u!1xpg22ZA0+CSh@<4Emi(6c{g z&@W#Zk72*#go*CH8FpmuhfA%?Y0yXB;{tP+8>}WK>x63ukbHx7+27$>ecT7E ze1L`#y!-a}d+OhIgTyBhQg;^X*03}aw_cWMB|gl2(CXKiQ~iG?)ShC`i(_k#)@(um zOv%;0BaK;+5Be|pAtE9IwAZ2+Airw}G`ppBb#Ur_)AxpJYY(SaR+Q0>dmIStKez}$ zQgSmhx9lPCqHr`liRla&W@{6#*l5-RCt0Fz;PZ3oE8pSH~4*1um3% z4HRB3*M)*EB4-yoLWd9!J%Ve6(hzzzs~X$*@FwOZ@(Zte|F`Y{ zcs9$@Pf;cyh|M&+@PO$1y?Fn=#vG0hd8-YjIkSC-U~*O$Fp`9*8KCD%8l#Tzn{Rfo z7uWLqm+gLMY%maIqXY7Z68idYUShK=F6Ixx{vpaSl7;_MKYj)FHE%^Y@z*W{K0R!fSp^HL%Qd3OmPwXvPp7ohl6rt z0>5O^^uCoHodF=Ax606phXp7I>t|TjyIF(=M-SNFd2r^t9bV<)t`NfXZ0c1s+Bl z5HSNujz~6+kG*)4htF!f{6QNZ7k7!qg{W0qVGfV8 zXecZ5-I3S7_kTr`}>`1@FwS>WexkmCLKj`;AE7-ji#F@Qx^y}zOh$vDSYiC&zi)r%wL4vgMAR+*b6D$(ZK`=jXLXDITli!$zP3SZECC7O z`@&`UA`!#vKqseZKYd)fFb)m}Gy5{{gDrzN`*%HN-Q=wkIOJ29xM|`6F*y2G4O6w7 z&5q1J-A@UhlDJVqp>?w41RUVkd>SUgp*qkW7~{}*w?;vEb@l8nYcVbfPq3ZY1LQ#I z=hpQ^#UcJPsVifUbBaS3me4CRg$dGq*f#)sW@vS`dwjsIBwr+f5tASz%Z|M}ue*`% zowrt>DZ0cSMl401IytGH!KqeV>ZWaK+8G#*TSkih=14ogT<^y^7!^hgdPhD=V>#m7 z|NS)_ZZ3>quh6MNVv+ZMp|=V`-}r;cZG<FEZ-O@qLfuwg6i-A4!{%87_D1S{%)IxpWK5;CDt|sZlO2FFRf7?z}w!@jg1$z%;TNG#(d>;PlMX zY#|is^8e#xWudumU_nY4y=f$9E5PetzDmO%CfLiS^JC5==?#=QOAQG5W zQ?FTNui4MBGAom-HPUY^CyRYY-g1hfLE4T_p=M?p?RMVzWdZJ#^(9bD^78X9V)X+W z*eP5FyiyhzV!Xx3|G`(^j<5f>#@%m*sH-3inIuv15EA#7)Kydui#N@WWl|M~qp!6@K}i9hi7 zMyhEF2*hjSP#k@zp@M!zgr#>~9Su2`nq8awq-Gh?969V@W-W5H4U)k9L`Pxl3Ae*q zix0lGT!QF-a6ee=ZrMzCG#fE?s9Bhwe*_h&0Yi&}v1njR3beO)q4rtQk4<_3 zHK#Pbl^?^uv{xEY0*wz#D9Z!Sm_Bbvm*As&oLQC03SNy>%*}0E*OkAHZ~2dt%?#He zsRDv%bV?Jy2C1^b^=xxz?_H{}glGx&{lI<8F8o&o9!7Dj24rgQ+a{*F!P4V6U>#cz zT)TtY+w$MMd3#^qTzXkMnpv~c-0s^xXt_RSuOyuE?5i?t9t2TmVH=jmQ#m}w2?;ys z_f4M{&Zz}YVc50T<}QM2mdno=RyZvnwW~k*0Zh6+ZZE$+acp|!3`bnjC3qJ z&GtuPsUaG^On%pIR+@Lo5P1MYes>KLcj`060%0QK^$`f!N>6Y~H6QdeW$re<9wisLx*?^l}1*H2G-7Wfc;`ntf!tHEZXteh=2z98Lh0W`077Ok5U`6KMYI8 z=dH!jH{;bcE{UZQHZz-15d7)eY4mBrmEEMVQQ31}r)E!zB1;X!r9Vz_13BzH-_*JQ1Md0m##zz}x7rS2r)Ov6dd)LnW!88_qXys@`2#ina)+)vyxbVyqqh~sG>|Lxzc z;R~`-)ZD8n6+&tOvn!@$(Qy=K2pa70PYHwIBr+xd`mVvTbELV@8)~zOI)1F6Zw$Kd%fYKo17# zs#dXWzg!jJpx`pYH3_BW&sEnJKBo&y#sG;U$JZz0OYex%7b{uE7G2M> z4g*R!1M(K|4cg^2Z()?XQvIKYiWO}82iaaG4klNzmBK4bZ{2I!Z!h=w8#Dj^ggW zE}jYIG{nA6PFj!s_n&8cu38`pOV~jdi@Tsq+D{#VVBA8Z$w+nEt&}549=?D7_7@%l zk*}&cQ%&DI+MM}~urf{fT#;+BWEa>!eNCc2q`bI|1s8tQ)i^Y(+jXhUW-BExldsN- zF#We9e#7hw6i@73mla(OS8%pnt*ISH(<|Czw0T}ffz$dE{!J|TA68uqMFvL$2a^ii z-4ti#uUWQ_$$cD7aUGn69tVx20|?yI6= zv#IB#%$lq|U3q{<1(Cx)OwOC^1OKAHHbPo4E2;;T#PT?0R$6_BV3~qa!{xQAyKep1 zPm%$b3?f#WZ?1va(9(L&rBXLzBWiiTjP({rV`NyzW(AbEDn`B=SZe_GVOIC(^xT1)5?CsxX{-mQaW608Pl&d ze9N#Bhd4(~CiWo+W8pU@F8vbIKUf;Nc06VLQ9L!1L;47>4VFV*ggT9#L9)Epm+#+m z4`O6+(x6biFb+uh+lFLkusG}-rXppoBmsHJzBZ(;DgNywd&T`^bcdJX>?_zAU2Jd% z3*J)kxvON0n0Kin=zV^E#jWtHcUldqqrv8gIX`lzW&n|%JAd0fS+APf-D(@QBhEc0 zd_mZ2Gfjo!4u=X3onov|If&W#r#_a`_2e-4-_=HQ&|Y(2YT4Q@(d~Dlb<=-mrX_r} zMlYY4JSi}$r#=1TiE_eHj-CD~4Jt(1JuCFugs7 zB0=U0+ots!St{&*Yr)q<6-FiHze|Q*zNuB>G*j1n@%+&SC>Pwi#$IQISYfgio^ATS zKCLxZN|tOacAaGPZ!FkiJDo{N;jrqO`26s7bh7u{0+eo8R>|ZAdhO6Q$dVOO4kui% zaMv%;Z+URzzBXTH$zK%!0$}+cN?N~oaUX}35RAsnC|SMy$&ayE8&JxPNVX%RP$p>X zI;AKy3jaNG$(=E8qw>$mJx+e+{GzYfki&e)qQ}PISKmyt7KXnA%;8M?1)FN)cX;uf zva4BdQAvp$SlOI{HLLBg!gLh1^sBcDcYcHC@xLFFaH(!*lcS+-HmZG;n((+zM^ZtdBQYst49(MZ`LV>#mD_G9{&mDs^1d3C+)2eWjtxc|#=nqG z|4xCC=Yc`8PixR?)SFf$(>!>GrU(e8%X_3rvYRyjaOQleO_b?KoHI($@ z#{fX(lxK%UZ!Ie>^)?IvX&FXPC}&ffki$Ru>Qu(Wu(Y zRXaMVbRBkM$S=PXCiy0_VB7=zWXsCKRUX`|qYL*beEA!8kO%B=>2jV~| z%Y7or@XK+w65f(P6QdT(!AYz&rhShORSd73oP3}Y#$*KrOBWBeXRgti&h8_Pn;Q&k zab(Blxg*9J2<3b8JJq$Lkm{vN1BRu8?LLRYbo<4oA&)5%j$C#K3*Y{0uzNPjX_2?_ zjc?pE#>NgaW`{W}VIdJQZg8MFDIYV`tqN=i?jW{--ISD*E00WzyJa2aRv;T20fKj1 zUjJwxRT_|)y8>2JOX2xn^Lkdx`b9U&GYW;F6}E%2KjR;ZIZ% zmEF%-YZYt~K#Y*)i&#CUZ3)V5Y>VVnLA`a4J?dr@)vT0i6Y@q*% zcAtHwu?CCHx6KKhEe_Zc3O@bgAZ=fy?M2@L!2-B&D#3?^-2P3xAQ^(MZYfDn(f`DeUpogd;9LnuVy0=#rGczyp{lkOvV@7e({G~$FG|p zlErYL)hmOScY3K*+uT|UVZU35IshXmL#DZ)m)=wKfJi4&_V=s2*9s8}?s;Dvw{ySv z5jvQ;RZ2-c{ofVCw5OD{2C+gnZlGm}bK6>V(W-TN%f(0zxqH&i68kG{6tK$)A128& zA;&I$(5&Dz1YE{&_x+IHg>rLe(>yaKz~CdEE>)*2-SO2dIw@;MiRG(`T@>;?Ylt>^ z@?C|k0+6^vOFI!w6G8gu9P5}AtGST(j*fflL+sqC?lN9cvZtQQ(g)1KnvHr=I`rs@ z%CtZ@l}?`*q4P_kWg~(gZ(6wG0XXz?uFzftWUwUhaCd|C(2Uoha4LYkqx^X>&*|Bm zY3fD340m?$?M42uR>?M`H`NHa0u9^H!!wRoILnTUDk7}=eCrTK3_m^)kdjZ!m!ab= zQYNT$=dVt_y_GcFNRVLwll@v-KN+wW57pV7dMD1-HY|9iqIu8j`WYV*7>Z-gSsAQ~q`*+p&)o=`=^o;| zGK{n9KkzJ9$f-&OpDW~1D}Z62%t%pg7%8hM{GoJgzUdnhwa!i`3d16&tN(t!B+jxo z=bGRyhrKWAvDpAFv5cdE$JCpn1_*EzS_*CwYubK6sYdC;3cls^X!PXItw#B?_E1DvZMxJ@4*Am%(W-1s;a2^JP{H;Ov8ClFC=XMpTqaXduoftOu~1ML7R>qNBw zoh=F`(UDQp;hlhi|4>REk%tU?{aT*w-b|JdKRGt-Q+6^iLo5cpA+yhOc>U@VoK{-3 zK^u*=R-Q*M@;aSle(sG5NDyH1=k%l|;}M=N9n<+ReR$eV!t;h)Q>;eXbK==*AxO ztwk0?qYgp0U~A4!FV|(H9$qyL7t>tS&%nz}XsOpb_ASbvQ}6FZ)jeYG>DE|jaaj?{ zPlo`siW%YPblhRn8bJ& z8aN3!sZD4pP#5n=xiorySAO7H&+bA=8su?1vwtnG)5hKY{WKjnpHOj>@MHW`^l_RN z6wI{2Ts(=2d*HL9&yYez8jqmRlL@c#R7^!Hb^mwfub!xkom&@=&&R;K7S1PmT|uNNXBj6k-Td9bR3Yc~zktoCxM z1d--=bnN%ed+4Q6eTX=TnqJO^5hte8u1TX)c}9nmC2pfnPr#?ksW|PXK|gE5Rr*8Y zm!-1xl}ecl4HSyB^s^uIl`5wJ;|QW``soy<=!__a(WNOVxVq*X1oxu$!-zF*N-dSy z@yhpWKKXQz%&qX{Z&P4Y~I zVNs$Q(Po)uNu^tFGzjmUg_n zyz?r}LXRwpaQ&3=_jh-ZYbKN30Q5qH3(cWBR1nc#Rob&L(~UA$g~l+q2NM*fr)d1`8GxZ2s}0RgM<*;#%iDVrxGl;O`80Wy$)CqcF1@T@eX z(ajROQ#a*5^{H}Ox$>K2{bxqp{A{4&{aj{kss?Qq$uY5enV ze>#cOD-KW-YTStE?3m4>RnMtd{w|Z@h9um?!9q{3j@UBA*0ymRIxFj1Uk_}0>MZa3 zOnlXBMZ6ap5AI`C8i|0zE$9sjG@`cunmEoj41>(sAm5!LiVdKZC0pwHP^n}G?pkvA z$|0vBE>79~Y({q}OUM`i$Eyy_gGfyFv!ghvUjc; zaR||W*OLo?AZ_o1MJ6i;uGVe(DA|X{`}O|e=rZ)sd8M_Hx7x-G91a2qOR&Hrpcm?v z2AbD=3>!dl@BM+p#ytTBW=xq6FTw?I8txzg(6Fci?-OdW6agjT&{YHvq^Z=c(Pk8i z28P!Gz}$56k&SFrw&iE^a8=O19-`ZOSeDbWTR5z?-~mJzx3tIrfz?LqUUkyh-rN#P zN{Vfb7O(pZ3S|+?&Z%!WxCKhPmG1!IVrA<(=wNvS1oY};Q?eDoB&%Zc`9g4*@a=Oe-*m zrL*akv3iHRjVYTw@9+or;=IbEOgRFLRia_sz#`z#a4V(Dhi8MzOW3Df0-z3)@;g=O z4FFA3-kCThL|<6v_9qR1PDSsp{)_$)7bjm-px!b&JJUow-5JQe0E^;SQyC(F>J{dz zeJStgSb^#;PPU}qhXc-GkSW#3qCjtfg$9ryJhTlt?oBKM zIn`MY^M$K)=+2OCEC6D4G)dQ$nw3VouuKcwD0`?Ck4r2Ek8p!(jS!3WoR+4SfmeeI z*r2$y?mS(RHQO-O;F{1@8A^8|E3Bw)9g?joC!oR6XQypytP0t3o>wX4wWy112N<;b zzFsP^ln#~<$SUXVq6fzVxr5dPKaHl`ad%Huo{XxL9kK(vc!o z^AzkJoopiG;FR(G>(gaZ?%?1Vurf8ND4(w~DBc6(NvG7!3%J^cty7I{t{Amn7;wEL ziNd{FBLN*041h2P( zf}ry9a+j-S*SC|AL?KE#|5?LF3VHNYRyPK@rsiRfDl*RM4fLB`vMLQ`@N9o!8RERs z#C{(UQMp!+>c7XZfeqH9^-6A$2u-PC$F%7W`jHhwJS#U32{T{jq#fmz9!AG zIOj%`T1_q+C(T|ox~a6C?$%giThjF_%2nscn#Nt8n_~t<0g|tQBubLbiVJ)J{@85Q zlGSjE$`V{_>W(fOv;{aohI}vqwVZG)NAqq`*XLky=2?T00Q9YD`Sx*vhgk_0m-bAws@5bw{x8*9HDW*UYto!+e&>nWV+kLmLFjGU(+j*b;?I4X7~wD4n%z$jJZ z=sUd&h7}_Gu*6GGQ!~(JI~ieVWsQr4VK>Y>0090d{xMmqh1~;Dc93?~tc~rZH0%Iz zChuqw;#WC;x)qo7aA1VHNolJ^CM7NNuw}^$zlpv6dPQrv#PChIb@BH@i>tqsi=Qxf zYa5E{J=+W57zU19qK(u-~aK3 z>)z7P;~G-xSHD_(Wa7mCJR&uv`iXF`D(kK)D{BmAgF<(mTh7qSlvXza)m8pTe+@SL zEiU#sX9C!#Y~+23E+xdrG8VSc5ui3whjYke^@HQckZxxw(kX2ee zt!1pU%F6Xw8Yzg@>Wq_S`5@1VpL!jwD-jco$7A3)k}7rWs!)t^EWT>q4nP8+M+wa4 z_3N+2ZEY?|vD=keybN+n!a}To?{f!ffjhfGbfTgq?B3Lzk0M(7RG+-Cj=g{LI2qTn zZ&bfiZBOXlu!lH)YKzP*A$cVg8`31#EZt${^G+~qaUlKSey9vgihzZ$bULX%=myK) zMaP^9d9lD=#;~tt#h^>Ub^moHI~W|=p>auM@j0oDj!U_RS!5K0Y!Np5fauZztSa7; zs-j|hj#%Ij)WjfA?Y%9XbqK3lHZoT-e&boKUP|H836dZXAB%AAd-S-TdOuQS(O8Q8>0XIY_RMcE zZ)&GN3zgL{ExdGZxKE2BXp*XSn5>6wL&0({Flqn_+li|j^ZDi`C-?q9vnUCVfG4f; zXH@^gSg<)4)~4?DZR45rgDhsaYbZC<^?{UCZ_Ha zHd4?~SsyI&H9_OzjH+4RrsI`{;#aS;y)DYmW}#uQw6YrUhM>3nI(N&oG%l|i!`f^M zUM{*k(JWYDN9s!gMuKdSprC1W_Tpq~eYxf2{A&Ku3$v62rX##$kLV}*5Ut-k-M&I) zKMG~F0y|PDouviZK%)BYz6c*u-oA88;meJ|XPy2Qi_#px_`)O=KRFs)&+APG$(>lp z=swECb@ty7s%C(f(lq$Yl#>fIFIoBc45$dT4{vWrI-A;Eyt!dK=^OdoEZ%$VtEu9O zX;>EXw)ocGq_+5`%Aj#b=gxE~VvO126`7)eL?5^j9Q~+5g38m{oF22#1k^<0p%73G zuq`!3jYg=4?tKmq67(vkQm+zQijTpX&DZdf;hK(2FDD{T!#4ORRiIAA?^L@JBxEmm zc6lijaY#W-_7*O`Ox}d<{B?MQIHH7TW**r&I9T~x5>8Kt9ZWB_!hl^*NG$mE>j9|2 z%?(9i4Y#vr(G+ffSn>z)FRKf_+LPEZBXB2#xboIikd{!D0Yc>j9B|uc|2BOq!?^e{ z3E{19@bs&aUJhQXnjz`_HuQjs)cLANWzZ>ZMG*e_Q(qihVe@^{kHw`Wr;9&mB$0!> zK8bt~90ys~N1nNN242%l1mOO&+W;?XiH@sYU~T7hCn%S}z2s;N;mb(Ut5dHA=K?)Z z10g^Wa;1das*f}=+9{mDk!iXPJE}DAVX=(@cFl?V7|&JgYQ&SK|#du znXmMqGX+%7E~9gHWBoSKa(JeN6;udVu7XB4hxLo>WkZ?OC3 zC^^JK51Gx3IzrBWx;<)f{h}Z24NlXMGdGLHVbz(=chJP7*!Vg``ln?pyF%RO_xIh9 zkdPUl?WKRm-2dEZOu{OT=snF)>btXMoqe;$kMu4nzba~mQpk3_Sapq>c+L)($f$9X zic|Y}oySqq%bV3XLS8T&x((H0IE(+eNNnF4*bK$3J=bA#z_H882d{L9ziM3~U4LHC~(yI19g0#SClc!xT>F{alRFMkc+F@7-CKlN82ihuiPc6Qd|^}gxfGeAsA z>MwAVa%ym~zAL&fV${_A1n>0EpSc`S|0Y7BH#m6<-@!KUbDf^jziLTLwn(+=K3}S% z?{k)&DNFWm&9&pZs9yAG*9Y}0J}V<78h*=7puZ7x?L$#)2}Dcb&qQd%5Yc|!^)0($ z2}2SawVfa+8%kq++LAcg`J+U&d4cze(@@B{mFg;-o6XOSru`N4vTAs2q>1imA?%NX zwoMaMGZdVoOLVw^vsh(mdb6u5H%UkX0_5e*Ru~sw9RX7q-6PYezP>)pU#9of>+1jr zE)9-sE!z6FC5u#oU!2$oGU(Yvk6Yg%O6&=>^|vmEs1@LvyO`4Qj2us2_OD{i25SgY zS_IPWF~7(n4E2#PeAf8mP3)vy!x3n}Isbl(IOAKX!QJCEZ-b6IbBu6~%gWiwbJF@9 zH1fo=oz9sC8^=-m#=wa{^}uSN&pPLxjMCx<_QIw(MP*RN4V|yzrv?o8(`V951vHm( z7oW-qvxFQn{|>xAg|pcH#ctet?KU>onKwGO8x~>e}9{|9Q9?a7iYL6rli==ZG<#=HB_a$U1Cq_tQ2F_ zjVbC@dmo5uZF#h+uKaC1cdZP@JE6aPNpH%0lp&_{Gi0oCbYyFA5*Y8HBnS4dA% zDm$O@ANk~-kIFV8&6I7;w~u_%6uKO5OIw9_mv_FDPTtA~J-urs*F1%N=jAuaM=x+| zQq?V=wh?i7nBBK`$d12oDRxaFDk#MwLadck(a`QqTj=XC=q((@*5Y(2hczSxU||ox zx_nk%7k&{k;%81rHhOz=Wo|^1)VwNY&VDlS30)=037vDhet$mUzqti!uoTcYIEEU@ zTf+%RtYSG1PQv+YxXw0{6L93F<(2zI0>$dYI&9X-LXD^CNnS}NGxy%P zgR{5FrxG37Xhq`d~5n+07#mA z#|^)9z6IS>1>z{O5O+P_(^oty9{;0zl}dVP5?7~+vEO2`XRJyiX#ay=3gIp_zhH7| zS1NPWP`)a_jbJ6^e8v?7&F_P<_PpDV^E4eBkw5qNFO_zxganSafgRI*l8L{I4fm>b3a4uP!$qmge z)^n~|$1`k=XSLOc{v_DGarz>=n8wc5@J-j;yLX}(W2`{9%%})9r~HKbSUEKy%J;WF zFCz0m&u@H9>*b?jJl?dxkY8iLy`;M))CI`d4d}^wIqW{OTZy|{^(Qt~O9VkQ7p(3& zA!`g2+TVl9p2I|cFD5ntjn)9(QS!gC(@9C>)p~!Pnn@JGDw$F z88z}UFkg>dOMxf4s8qxvJqNckc1S7X|HD`(~o;t|kfY6R9{kOsJ&6z(HDCRDh!2)wXK*3osN-JZkdqJV1IIOizM0IoqWLunismq_2P?ofIGt zn`RBU+2Tegu!@jUq???Ls^k>y9(=TrDh4^^Q~JX41Ju5hptG~FkpqggN+Z{!X?zZ6 zF-hh+D9~7T+*3S+QOO{|*pf^~r7eb7(7s-(!9OJ?9nf5Q0aj~ug&R_yDib}lgPOFu z#dy#-5V2?=s?Su~w-syNoVrS7s+4D4g1*rv3q97m1_Jh+qq;&>36;<5NZv$m2Q zcRcMx<*Kar3Mh(nssJ^Xj#XR|XNYf2-PwuldvU9MF`lL~v`uSN=4o`kQzHv7Fd&M6 z&tn?l6i`;`m~~q((0VD4Hkd**Fo196NNYfHZ!#tzDFj|8W-73#1YBR2bX~G547yW7 zzR?CN!^Jb|#?k~L_BaZAL$&k2QWOPP zxSgb?Z+k_n*V&}?-(8!`vyKjJstM!r@;(5eIuXF*lmX*18*UoeJf;*<%`r$9hG|~} za}9Pv;nA*`G?_PQ-4GzU-1OLuyS>dKGALG!MF1*cM9WzzV@= zr5<&!^*P@5)w+I9`*n6}ZU8IM3DEP1TH19B?{)Ht)~CFUiWMy#G~(H`#GRqq6+3{h z*gyG(Xt|dyGa|#o*wj4md`r6ieyyL6Zv#{&wyX0-!(ik0in06}S#Nd#kWfXk=Cjgo zN^w5BxM+q=aSL9!V5HpGaZc81Z;#=HQ{yNVabNtcY0gaCnNf>Di zl$V2L_ zs41X2Z;Dm7uqd`&Z&4cQQ(1;9k5h)2TKyz-*V{PPjodC++vAx!OR_N}bQq1U_>9wQ z?0I)BVZg^;P)Df4{!x)%jVJaY=3ks>wOsp(7d-~(eJOw$toJSeYCgNZZafFqza1C% zmwY1?@I(h7J^E?y-wW@mLe?>N|Kwy7W$|>_)&KJM5mnPj17IrCW)m?$J(2+l5s*+< z#y484MH1cuPGQc=KWPgHATE`-&@u)kA<}0i#1$bD(dB?Lz#|)Ftt9!rarRc|-$Naz zJaaGV6}Pi=x@E0twfd)VN#6ouDw=EwdgJ1-4U5eujdROq z&V0bK%}__D{hy4x&hJ$_#REiv$X;_fn`&j?wQDy@rnz98HrOI{NdWlh5;gKX$I#g?O zS#pfV;~;=@t3zjCxTzA?_=c;w^Vcm|A}@elBPh~r73XJP-H{CXD^15$$hai|1MQQjwLUDe0LzDfCJPIC--$JawMC2ZJ~O{w!@f25& zZ_k<)mQ{Ola^?Uz1JWf5C}^kStEqUd37>8?iEcHIR=~#s_)@lplEve>;^73B8;S|TX}nbze+2>L{oQ45t?=DLrMzv zrx4A*oi$J<9CXpiU#RMjVAa`+c4xFfLeaA9xef2CO#^Mf0@x?ZTiaUA$62KZs}jX9 z`{pnO37eBAm&Jx-@2wj4eISea_DvM_-THAATey7Slq@$6gY0V z#pT)i=zAQ1bZ#8{re3Yj0H~Q-&)N%zmZXbWlwlgRw`V1mu{U~@5&gNJb>l-M=q@dj zq|4tsf$`RhxjP+K|3!<>$szItUfOmr zGAi(m;KhGr2Qm|Tk0keRtEkNcd@bOLYBP#NnrKWC5tb;3>;gj&(P%BuS%_(`NlrMjP;6sFOxzLsOOZv6izaJGh#k2W-Biy$Rpto!s zN)>OJM=Lj{EsppzTfgU9lQKT7b>yp_Sew#GWle8395FrJf~QO?#AaMr>K_y&l15yp zBsMZ;CKhyW<`$te zH8yU$ut!zE;xEiT@mR$pz3Q%haKlLrVW~-JXIe5o^&d>OGJwBbE-!V^vgdYXP@Gv> zvY2~UwJfO>gV%BrWj^C9o#*Cu4$h{P>E{JX$$$D5)LjTR^$8&mZP87Ut8D#$h($B9 zw=7oa*+etSQQ0~;G>fY@VvVHP5Lfiw7@X6y+RZUFY`2p`BQx!V(50$&OEb(>{ZegO z1>NW45fRmHpKWiG@EYW+c=;~W7)w)-(50Y{G+zkAe&csc5#r>FJ!wKWumaTcfl5I# zgiC<#n85=-I3bH_jF#f9NVcA>prW*G=q6#eP}>l-P$g>C~X=#+$zFIQFTs=)SNS z=jL)8llRYDDC*C@CfwQ=wDt9RRcnZbm0mGs*8Q`+6$fpBr+DE66 zxDQ-LF%1n3qU>sENhqe5nU7pZ{Dm{!$Uv~>Hl@R`hR{cfH~;Wqs1z>&O-`2E5;I2I zFDx!DBX|V-jQ;7#(|Z({b|0jiNAC-wvwLT%L~k}8Yp_uHoNWD9uu!QK$iR7_JPi!0 z8AS7tNnbPf=RT3te{{b7D&PmTzm>iA-`7H?gNgK70Pi3UntDRVLnFo!hEET$<;jz8 zwY;U}Z*eLI-hcX#AIJdgaaXO=%m}O1`K5e*6eA&;&nh!RR^e`tu=uM^nlTAdXJO2* zZ6g6_kw!!il}W88u{j{iCypZKxo+~lKI#SAG)NoBN6Q{RoC43bi=klBVROjDmyr{q zkrS%Ce?1by2T@Hr?5oL=w|#j6CC5Pu0I9UU6^MCAzb8Xi#4(Z?Z3%53$h;VQG5IJ* zS7cLb+Z$aS!cw6g7|`?^sbU=*vbxC^F8oR2k~<2_{EELuI9S~FZJsx;lt+sGq_^J# z7&LqLm3ATa;#F^WirnvhJT*IGKE%7$KJwcV2#rPItAo7ICc)4lGl>fAn3Ye2QHA(lVGZb@73(`4vzY@?*-hNa|(Z;LVI-HKqJd+E&QD6C6?v`?jM8 z``fvww+H`0<*;?88PNowhPi($tYu~sq@T>+!(f|6_H0~Mq=HQGLw1uZjYg!q^cjV~6jT#BV|7lE**1%3P;&T)VE5tJ(UGR)fzB0F&V5U!FRw zN{ld=S(z;597d~J%WyV%Q3Vyfja&thy$p@$FmS?>prqkQ`(r!Y-a-e zQe}kzZ58d-eV-y_1M+yuD8rSwqWf(ZRl&qjQ-WWrF#3VDz~se|<>fRl98%4yldS$1 zk{9(kC>22Ztc6DREhQ?eg!vq?tAJx0Yy;$5<_g4nvwmDbQvjK66qO8(13P9RJOWiy za_tGo(3C_sz{U_0c(i#E%}eZioFP)2vqfL)V58aFzJi2JbDiSBK+9gZTlWQ|DJ#tUo0GS@_6`JUkJ86Bjes=5@r4d{9LbTzR zsG2j8YdMq@=p2Ue~Rrw(O;=CmJn-42e=b|S4V%VH`C9W`172VF`WPsPBJhicE0anE$`2Jj^7B2>Yxgrd3J#KK6jMx8dvuwMPDI zo2ZY4J-Fjf6RhiuKtEXC+Uy7}QuV?!p`Ax`&Vq$o{$POcito;gO>NaZjZN11vLTgS z61O7P<&|&^7HkUYe!_q$a)K=zeB!StWv=v7zHE$i1$;ePT?u6JXd$4_1epNO5JiLl&CD1(#WO^2Gl zqP{x;L#Q_R`S}U9i~{m}D1#@Gt&_nF3=D?@L$cFo3Y2h8F(0Dkx99MzxGr=}7=AwS zDA=Vanzt4)ShLBb9+!-}>Am=}LE2>-=xvr)j&K$c_vjJL3G{cx4b8jsC~r)qkSemI z<{oVQDP(KI7E^9b`>xsu+(KhNdgDQwbn?KsR~#G5@C;s+2!lt zliVDecUh2`lumTm^7^k^CNz*FjcIX0AVR$k(A?wo5ax6lxr(hiYkrN_`y*6hv6L&P5@|MpiD zc1r&!4;M)yU;n8 zQBP|`U~i&8W7&}e6>8Act8d?%Ch$*c5Kgoi;!ctX44sW6vvFgM2e4$&R~|2Krn%>W z#ZZk3pGZu_cDMsXJ~hAj$G`?%oUL@dYH_>#8uRx2k_yJCYUe~R@dpwQI*hTyzXe&O zno*hzP(Z;tb6<21;??gEG>kfUy-2zJ&mPe4^s>fK3Cex!AV!&Ah#t-1PZppTTgbik zRFOLoz)Vm|WZ_xfXH0RVG8Oy&q)SBaQQNuCRwLU>rA3h7D}hRMe0s{4ieVLUT@{4U z4Fk%^P$mnGvldOhr7lu&EPI(w+f$fwbLD3OI?r?XJRjKv6dYC?j(^K$#F_5Zqm*c& zXi);tN62++$7S)O08`3bkMV%@rXZ&3dWQ|Zvok*vag~56Jg;pUXh_T+F0wzM=cojO6|KjfB+=` z>_`0JL{4IvPY4xvGJ!IO;dJnJQkC{D(v;(VD-ao`fi<4ZMJ#CuCxWVK0N)4;%*4apYIU&y165uq!SIugtX`TgVUb>1MlcOcuqtU; zL2NHs8!E{A51Z$gF$iw5yIJ1#7h-7ONe}&+*}dCl&-vDcJ}1}GjNDQ^zXB9uNnpA- z=c6F@g)P(g5$dbQo}L~|LPEk~e(;I%AaY2TUNTn^9<8(R7=A~w6!0TOcflyz-E+GP zsA}&KN=*}>t#Wjm?Ki@OzD{Q|SMk(Vgj}b`U58$`o{mQAn!W2Q@)l%08`df;vP^Gm zR1(s;K3@YhfM5jJVEp#O5didX=ObS%T|R9XetR_4(9npDEBg&}R>9#f9NPd*-|xBi zph5m_Xn%KLJ+ktA^tY!G#fJjWJUFl;J7Fo_IK!;<5JgU4;lmidfRA|q$dQU!)`Gb+ zx0EuBix?>C9e_Evy1=~I_OAtHPI?bdrYtKVkT>_1ch2vO7Xyc8kMk2j9O8}efJTT_ zGG9%>gy-kO?dP$>2xGCkUgNPp7>QRkx2Kz(kP)EM)H`olLCDD=WJf>pNkh`>rQ&p+ z{+Ok&3c#Z=h%a=Z3)zjhnc>Sry2maY`Dzj1^`R#`hU_2V8FEwJChNT|RjZ)><-^4>kUtk*Du>cCB=#Ms; zOozF3be&-#yAT0H&wn5s0FGZK%}!0-k7`fP_X5FadHA1P>ECuNdM!f}ZsdyelKUh1 z{fFLv3=M!|v1r!0xpRNfq+`&F`KMz4w^41#8CO%Q-?%W<%z_xOwD`9bJ5<>=(sql^ zpsZjdL=e3HzYhW#F#Ze8Th$RMj(N(>KWrZOuqnsHP?P5WJyy*B2f6~WA0ELDQROn7 zx7%77W|IE>pQ-`)UV0*H{1#Y6Na*wGU4CjrzTL-lSB@|ngEG|v!35@gNm-&xPL+;F zhx}CC>4VVJL+&0p=$(D$m|cw`~21c!NC8vSS{_Z$_|jnX&9 zi6R$iO^%JWxwAQ%#~hiw?WKRlApf^a|927oKmAiO**G|f3KY>H0d>Bxldi308zo4s zf6Xqnc%CRR0oXObr)LxP#dWV>s;88x5(w_BaT0ZNN}ukyVf*44&q`G^vLWxrZc_ z?^M(YPdQ^?!Y_#tS!zczsh2GVB0MM-AJZYb+s5I2C)Px&vJ6?&mJrqT;2i>)j@)>BH5Hnsw25K%{H+}UZ}qK_#Z zs=OfvD3U( zQ|({Tnd2nS-{^+g@UJIt5zu$2dl^j0DDDYst}ouQGjA|korjG=4`sR$uWVuSx|zY} zJbUSuLqBF6za6=MVlRw3GxnCa~dr%suu4Na9%duRSh;#f#|n}aqtJtj8;88pv`$*;0hhEe(7vuK zNJ=@&aF%)mKvuSz#4FVLhR9VbqFWfKIroG!_Q!mitoD+ql8KL_HhD&jy-$TIppN@a zMWk*twca@={hG4@`A-eH0b6de+WI!~f7-I+2uI1Lr#I$kF*;GUCCh_(nqt8>`~oB~XrrKG(N;gcY}U*@dZr#+U@^+J7J4T&)y zC<%zs{cV`KveRxe>BvsVU8Qt3SyLhgYQfAj9kGr0?%})cJEoj}@HnLUVfn!Qh5X=W z`H#CgV%k-M4!k!hPYfH%a~`5i5;2B48^W|f!1epII9xbLj5*g};1=_@en#c`zR25e zL*AE>8dUu6k{XX58ZY$@ue#QfvQV(h`ZGY1rE*g`;ysY&Zs>)|r&LC6P4e+BUCKKj z2_~aw?GSx*hYqvY&vIK3)-z^&=N@dd&wlsaOw?UxZM$VCZbboUxgVJv?&5ZZ-?S zjOTN8lRf%KcZ_t8^-r=s+)2Xv5uR)X+*lHWEMSnK4w=Xsp0!$mU(_IbbS0WdIvv0AOti{xPf1bJnB5(9%`?FI7Eu_VWU-spIQoDbi|x<)`Tuo6{i(sn4!lLbsYIFz}`byJs-2yn0+7**WQPj z1+`9aIVG{%ZN1w@(_w+bOTz@vpgNq{U8bs@i&HU?Ca386)`#8Jc2#9_hJ!^pFE?FX z=&k4-7kKcL0R1~QUy;LTez~+e(0ja{n7#c>_KEb1PW!N1@ZGKcnk#< zI|*(L)ZdE0Z&_H_{^a=cDw~eF^Ylvi!fw<6108xr&bxU&tjK*0pf z+v+Rd=fifK2hsg{vga2$Tv_`+|GjOI4I)*n_8@!B+(A-v00{77M)L><) zF8Z@=Mo&Wo1%LXLpF;s&i*BFk!;SHm#ym$f;i7e~?0cHj!eArS31+QzcMNEedtGMZ z9;JiHF-%jM5Iwo!0U% z$TbOd;l}R=QL(pyw;5_o_gRic=6&yr!6t`3-_{Q&3LZZ!%1F&BjJ9Yd^UC+TmYd0m zct~Q?!hfYW%^x{{@oSvN?6%htMssv>YlX0%bsVE-&=NoGpWxfsW+y4bF9lPiA59Qw zh-`91Jt$uhbDyw0&>iafR9tX5YxqabFKxu^um1P+wWI3TCau%YD=P+z$)y4qV2j6H z)eDIdkE$KSo;|^Ce>>KaTDx?u*=SombVQw@MxC5mlgB2Ki!K}X)&M;cuH$q4w};24 z^4t+Dc9E5?EMP#2*Y30~&z8&i$L~Do#aD!gt@if`+;0*HUlC|9s|2m-f-4Ce0)P;e zK8$XJ^Cj1l_#Ohla!QSK=N=EBRE>=EpT5nH_;nsRK%qoU1zJoTDI&(;O~}n|20x)h zF}=sge$T`kJ4{AKrcVG%1*GaS#uvOxb&Ohp_h8V-&3|VkBN)NKrrRB9QIwK65pto8 z(Oq&ndub+bbEgxMTy>0-*z#PxqR`0|C0;FFvhw5%@$dBR9v^uy|K(3;FG%uT8C@r< z!c0|aR+!zLO^Ug4e1nZx7(btrk+`4cy1WGUb~n^HQ^qXG+vF0!qD@|+r*n~Q>GHd- z{_+yw;NkAs*WT(=y)_wJGbD0`xD{te^V_AiD>bhG@+M)=T#e22~|=MI|7dusY~ zfVh>dR)H)_tBcNUIz)^Y33FMRJZM@#EFKTl$rlfSmYq54Lso-U9hrT2M`Hmgx zQiEtfgBvhpcfN7S-Jvd;8nX)2pmLy#iyX8%Z}+Y0PkJz?4aOTy$1>GdsQT?2dHhz4 z?ym}K-5Y-sGM*P6cS+JeH_gggs*k%nors}4y=kT(nC(IYC9Zrh z`r8j}$#Z5Pz*i-JRsY>YxKrnE>$RLtKrus5EA*0cP{VwA@lB*ybI#=F)Ia_MPy5BJ zQ4T{I;AC-)Y53^|X8-oj*R3-+8{JHxWWya~W{o#l^Et3kNNagC`FlQBY9$4Y1H~G6 z%IRGvzVI>&qBwMo6;ny|+I&7j0FG0Gl|_6gCS3c&!e8jQI$VK zZ7lC|d-+nV{^MAl7rD~8UR)ST46+pIZ%Z}bNDZZ@mqT6(OMPib1}AQcbfaDze(Df} zoAci#ytcjnZn0@CuA-#!fvZk^yBa)hdpT+T;zzgd0y}m{Pp2aQ2^--#h}Z$oU;(%* z;FLB3^MJGgMU~(+XFq{HOS}9>?CPE1#sKq9`HM#h7I?mgwNYA`!+=Pi^`vyda2oAW z*%>?nHmkUprY}P(vrzk7hw$j%cX+cb&xBQ9lu^-tXqJF7v!%G#jGt#%M}r0-SqBAK zoSVHIBt3qjRw{-b-u&cS{fh7dDJJ~KC*m9~GL13*ptZv4W6@)iK&$yFg!bDjyc_WH zn-i}GAR%*I&^fLQ{jUp|Xec(iI9hf(+|Eb0NcoQmm;7)m4%Dsm3kPQgS8%t%pdV3! z>2+8zv=Z(R&8Nr9&v-9G-B>5<87pp6+hfGShH!hauZh=N6D3LFxSTq3HXP#~Wz)Lj&4+&9tkC@XtO0c zcF2|lwo)<5+nr@nI2af6=Ss^;!S^AUvI;a90gi zMKDhEHkfZV33Tn=O^hx0IXG7xb={SFbJmTzYCMGR#>Rn{CeGmce_lPsJ8?2@wNHt2_fc8P72n0AhKC{xgubC z72#umV^=gRTgo5NQyTIaAsT<=UJ22v;3i+A!7Qr3FV{S)o2mE2e`1vwgr zGI{gWv_Jt0C1(8nKR1@`tJx0Mr4<@7r%88zIq6=n@@TK+2Ik_Wd$g+8n>6?YVX;wVD zNX!B6`etOm2HAoVjIb8!C+s%b~o z7Yxe+SgJDVji5uCBmD?iFvFMqN_j+I@%f;4k(WCYsiFIk{*M~^pIHgc`)Ulzi*>_^IsV zZ>4AvhPwY(ci`k%a8m*!n_L>f3)JTyIqhG%rVh>2hfi$UMn60`KkA633R<%Hztgm8$ZUGKA4oh`I3q{~ zwTpDP$swj+3V6b!qXcixGA4JpC!Obi$RfN8zn8YEVCM*oIWh|K9NND<1!M zk4AQ}!=kKTk+Wg2l0g+%l+VFOF86%ruMd}ueijGBJZm%XZJQiFDKAg!chiQ+v0p;? zFmiO0&rT+B_88x_tfD}_RKy@=75EPk_-Bd7y4H}O$)*mrbioXwJLsYg0)0@`;NJ*d? zhJZwrE@>lmIT}ZmZf~!E+wvo8Jq~i4gvGiz?IR(s$vZs{)G!RKrTh0!jo+!0GU`#y z4o}n7CB^pm@DR6BJFrTxOaw?N@1kk^%S*qZE(%!BQM?3hLAb;TWhmKXhJ%QR?NnH|Db4>|c2jHKN6%(>?_VD!DE)wP3lp(lI*0zTySqGn3^A$Hjl>;d5Kci! z&ZY%5X4vSf$=FPvHeY)dVRd^P(Xg$n-5+_U#uOFsAI5>VU7ec^s0s}sBZ>mW7D~W6 zT3jlFhHotjzi0?QbqE{6M25{T9cdC?_C58!d@MpNYheQ);K8rEpa}1jx}+z;ea5Yt ze3kboH(Q89x#x1p$i2U~p1l2>wBDQKac9!>mWY+4GFK}WC=Z$tZL3Qx3Qt+&Upv_`vm1k&+*0i6*!LX-IFjsz#1kvyoXhPlAf zbw(UYztq=v?_ca5X_z-Zg~M2!cVme3)BGV44&r#vmWw5BP#T%7g>{%JiB8xXr_HZh zN#a;Of*onw{FbG0<BwxR3CO0LEUHiM(1SMtS-JzGZ=DKojWm+3O6&+>f; z8OLFdJTSizNV-QZ5;0Jda!p?f?z$#*I3EuOogjalXe%+RF7$t*on>_N*rIVJZ>3Ft zrzJGt{e@#omW)MJgZ(g=oPL=Qz4IJ`Tu-@1qBo8xSXr1oDXZ~4`TRR|NLRx{g(y+Y zNl`QugB0&FVajruxt7Kj&BJzDmgtCpcd2Pl#?HM*c4N9raDpSpA5WywF-jF>9uG;5 zD9a28tvpM^Iw>Qr1QDjpCe1H64dcsd^zJ$qOKa&7@ z-1_XlGRfe8d7c*mYyW)HeHVo56)tV18uy_yT%Eei>y;{*C)ua0c<}0BS?2(W6%ap> z05~}(@EcP3fE&Dw#wXM~i|hoW&3ya#fEn#5 zVJ{A8>nu1cc)`Z+oQ+R&UNb}C+w96;OY;=khzPoFC8q`*O0SD2Y|K0AZzov+94QTu zW_VO%@;Dl*m`@MtHYCi5!^h!Euf|-!A@Fom{l`Zw#K{qPXJQ;kM#>tMU7}X*GpDH< zYh~eXQm&lL@t=v8;doy(mm+esbD^jyI10hXZyo;$n$IM|!H)t(d^l^SOylpy0!^Hh z8YyB3t-jo2dX!smH~&paDjI%G&~I$}V(gh1&my~ct?jg1E@karri-c-)vsrJUFE=u z&^0fFbtjmz?cYQJ}z5o&op#?a2pVrsD+56S^= z%=v@qBbeNrNSr+8db~ZCzxk|@x_bvi9?Khhg_jkZ;U>#QDmawN(g|FGcP&H%J|1aj z%lzS4CgtNEq0LfGW^CRB)$$n346nLUW{Q z+S5Jh_~mS@0g&+5wWlY*z%v7@`PYKgts|jt9=&$o#$>UaQ;_sBf}#NLihtmB{V6lW z?|?B$y!x#806dLjtS!rV?%_P+lfUZUL(UFW2Ce8{V_2##gE#P$)9b`X=l8duMP4w; zTRtN4tfD+fs|>>c_oIFC5&|^|-&wt>SHo`T1Y)r++-rq=U`wXHc}13w6sFAhk>~g! z^Vnf}6M*JFbqFa6RO142N5JG`J4C~z>}z2!1RZN$MG;eB)TbmGRmxw5=5rY>>M(6^ z%>;4h8wH2+i-IJ*ClrLS)8RXblpu`(%A|SE)XEI^Dy83Jw&ECwq3#ynSXn_I0IL|nnv>yDfl6l z{5V;?&7Zad@9a?IiH|=C+^Q%5CLBBb42?`5_`rjUF6aqk(7=EQLcv8AHZVb(T1p;%V|x|An&#)DlwQ;XtzLiflACO|Zl%tM zbrHMvKK#i-kR~kGm?!uN^_0@m=zyR_9#~c~9I;3A)T9uxwM?b-CApPlOnf0>XR(m{ z)03^wnG5^&_Ji#5eP(vG4@z%Hlaatoiu14QR_Y}1IQ;(2Kf9lRW_rBO!~8}nc-ILu zvTz-a<?T4ilU?1o*l zD|&qAB7gM^+|AhdIS}Oey+bAGcR?wv{567;>0)w!Vu)*ddWBM7VGEue8=LyLv;xa) zpvbtWF@rj&=dDV_Hiv4OUURIN7eQ!2fsGnb9&J>A8`Mg*ZHFqN) zs(NUmGhby|ZT?<4Zy}CAm*2ecv;Pw769BxnQ!=3CKYC^tl6^vvKbSH0*RS z5$)Bdy|An|jr?rR-#cq~2q9mr)9NewG}%VbPfT=o(@%?7Hq-OlhjBdE&0UDaAAR5+MI7KV=7f z^1jFlMK~XQxIB0lteeNZYxw>J3002CeV)!jbl7aotg~QT1a&j?1vVyyiDUMY#KlB* zTVlL6Z&!4^t~~w4p>YQ>_JnHe*)T=e(80#r?Lyh@N}J$slRCbX_frpp@22Ow!YMHe zapSZgLMp+1S-Mv4qPebJ)(r(cd-V;={rcT^@{643O@>+AlAYP9Brt`N)b+}kd3CX& ze$UvUknus&uM-%cHZehcvaKO9GOAEAm-%NN;#_p|UxKjND4= zdKy}lnHAwC;f0+N867d-`x}ara5p6=TwnXv-RlJ;-664_tA0Ssaoaol2LiWU=r@C+ zdKx=ic&ELSkr69NeV0R%{J0^2r#MPiyE**r^R0nXOjxRy$jIzfaf`pY00i$dcS7?- z>ZH0rs07mxWK;L{!3(sp$LsXqkoe+0R$n(5>Vq;r)%jIq1qgoO5vkxet%|kmcW+no z?*i@Hs3Wge<&kgaE`F6hEPg=+f!%cX0t_nnY1f`&Jy_p=2g(^-wyfj47Ctg2HB>K@ z&E^npHSE4`U|6@vEoPcHgZzi?SCk1be$o}{FIVKzzhWwO^}?j^p52(JAYiysr1b`2#79f%;b zM_6IjR@9|WO1v&KSP&H5Og%U917V}{Q4;tS2ds^IXi7%+C#nLHr^9bP-Ekn+>fN{= z{1ZcnAA!DVu>_PKk93;iMfObB7hX!_w9?`7rxp1}EiKg@razvm5lqBHAhU|Rng2e% z`S(FTneZtH#{a6J9fRc)Ji8pT8}jtQ-paF`NaL4Ru$T6xlrhCcPNMXOIpj{Tl9hj1 zT=FK<2n&Lr8(@x>6q$54!?|yipu~bmUVE0J{xJTirv0SvK=!VFD-fAAg^40y1_GzzE zPLKr8Ys^q0=gu;E<2`RA8(+*HPcQX1+Q*K#!eJK>+LFtq{JO15UeYNq#kJTvSj6MW zAY^^itLDT`P{;&-!0R0`N2Ggc%%W6~aRFif21~WYCr0ePKkw&#=#xmr9C@qz6rNrT z&k&lHtU$ocm!L0DRGJuv2R>L2^^%m6C4mUD=XKK@!ZQnwW6y=RAfM0P`yo%}b=iSaXHd-XF{$lX+jfA3nWXEDwY z_Fa@{^#mL&JH8&0fw#;2h?L6S`|;#F598t;y58a1l4bFTHa}(>1#$KYQd|ujQs9d6 z4yUYCCl>;#;a|Wan$qJxrfWSvPys3Avd)k2`bclb1KuE317kwGp7Id3{JKeu z$d0e!e|(i$u+ycH$1>ItdCfqr8WUFOSCx{5KlSNX5qTx!aNkPx|5<$5`en{#e$5p; zQg6IY;!(Rb;$d7*WaS%b@mIEz`_*ANEP~ddy9vB`GPw5v(3yyC{Vcv#T8m$<^%bil z+Pr0ki17qKE>-b76bM&BFK}j~6SR^(sGA(dzAA*~@uLntN43!^Re!$2n5B#>ilok`qk7+B zfBnC3q{@fCzhCGExh=_0fH@?KQ^BiY53fWO?@^DV7Bh~oANT74zNE~js~88pvJJHJ zkYVheU7&K7fzYIu235jx#*m3;v+p9LLcufxBdmo2uw1}E47KmHDd$|)lw~=d@csA} zri)Xf4HD;U9U=xnmuDg+zMQ7ie0kal3j$2NSF;t1E*HfRxRS3}DZe{^isW?ZUS?od#7FHCfaR_a9?>@aK0%*-IUP>C+1YE1fjeWK z#0C?@8y<`a&^U*2J`mDEb#vE@Ue*Ns!_9< z6Wm`R{{NagtFAb{@Jok4aCdiicL@?)g1fuBTX1*x;K3!hy9bBH-Q62#Wcv4?nNKh` z(^qv-RjX>PQ|ImTp8f3Il~uSA7$QM=^nt+LPPX}lwTH-YNHX1J0$z89G@tBtG`a9frbgmq5fgkuKYK^_80o+5HmVq#o*3OB&1+P@LpSX-#sx6eTH14@V+Zmelq*Y4^7t8svv~hx z^5II(1v(((@HKvp{O1ce)%=a?Ap(P3TFMDIMD>Za5&qED!M5Zxl3Hj9fkyS=)c)>@{!*(tKv$;87>c_YY80e%{h; z`v4tGHv3*Ul6)UDBTYqRxih@GYREb3Z*WuS+;)TfQ3p2s3VVgl|NGAh@?xZ?$!0Nh z;O@7){&f>?vy!NS(q(Bt{Sn>UXKKwBX7sj6%(uvPT8ylDZU}s=n?&C&W8ZFHms(Hw z>%^QPAH~Vo(d$uEbyk`iML!lx3q3#24})=-!B@r82QII5Ph%2I<4%C0U@@LxZiMN~ z;U6g5o)9fP(PI~OTWlR+hSTs*0Jx*?SW4Z1Jac>L)y6#7)3P@9Yj}_YwphPQbCy>S zYW+M>5R<+JvWtVA)?ibW6|sSY<952l2$O%=RG2GuCUs_^VfOB&7pA#)?Qi1DC(Z%K z79qjTV<_%)Ou;bJNp4IdV1Y zc-m8IbVb5l^R*#(pplIj94A&zB+i1)2lF;ivozPHtlqyp?-_&w?-iO<=4EHqw{u|} z&EesTJ)q#-KYoZj2aVjg-khOs&PHWha z*~o~R@LWBgr*W-=su=Yp{#0prPpOrIAGIE!z)SGXuDEh;6K1aYd$Vj&z(M1pC^qu1 z6P=E;T4fxzg@;@z!z~V)iqJ=cyaC`;DxN&SDM+MLCAdrUYq0RfCnCg33noF@jFWZ$ zwj5uvuXrQnlNjJj<^5(knEgr-DfbYK0~&tr9@~3X<}@*0q>?J>J~$UEGi5t;rP+F{ z#ZQ|N>GKEC<=^T=Wx;_zbK?9NGzL$6OSVKOLtiN1XQbD+yY_bKuvRahmmlChm>xX> zLjkzuQEn7py64V1&vBucs7FQ~4~i)iUm>*tV99*pHMFIvj7Bb4v- z1%}HZ)^l&tVpruJTwkEK@Wo_zdxl_0OFsWvjlb&o-4k_51~Q<0>-jI^F`XF9GfHRm z2ZjG%34Rb*_xXS1;0RD)ZSwy<)+YUrKmMQNv9F@w)U*G6j18M^O;duI90ib=Iv#%P z&F0yDmbzHM{O`^%|8r;Xxrr)&Y7`OzlUx!G|0z+1Q!X_*nHv44$0h>vZ~qV4^U0|H z?BZRHKRrtQJ!{&Uo0HkEp8WtyGSy_v37MjFI9I3ks1>%EoS#PTeBL@LCb8f*R*3?O zdFSVdl2;fcPH?1V z(MZ~&J&s75svZV@L4k`TW#%jd4^U>c-UbJ;_tN`stO#}I0iAc<9~G8_5g4#ZSGq?9 zmg7H-v0YmlCf)PPwAUbFKZ3-=Bj#R7{G!ew&*lEUO1uKcF4uX3g*Zo07-}OaxEUKW zDnxNUP0vNiX&wIA&25 zAWRvs)t(KXvDaS#b13n;A&xz$roCaPc_~ZL3Ys(VZxG}6Fgix{`qp5EIbfVn3{xV+ z%nx!Y)8N70!Cs*o_(N2+zixaAo&)sf+!0gbF?i`wo<|QMFfzP%w`psAi@gGL=Z525 zr3ICG)gyw^B z5S|Q(6$)+Q=MtuIk^|3(dvAl!wf6C})Bf^)e?VBg({uJC1m`9a?{LE;+ZF-aj11CW@^W zV0N_HAZ1^v)6vMW!q`aCzFV>M2(h>h$8bhtw2kv)YjuP(V%g{+NvD*ce+D}1$1PqZ zwxfxK)1FOZm`V{?i zVU(wyzG?GI?&v8{Uk~#rOkOe42Q;%(--6-7=9%k1aSnU|9J$8lWh<4EP51}-NtP04 z31frd>ew>lKfffqe2G&IjM+GiI0%$8(V*&V7IffTBZmM&OAMT?mQl%x(boUy+KJoq z${B*=?*C&m?1~^hxt_%Rpz!!$Ny-2d$M5}N;e56jBH{}rVC1z)Y62=fe8K_r z{KJVM=ut3n(U1V^Ws|{6y>t@O#r1Y9lLo~P`5pc2AGnnU!RQ{@q>Oc?&Og!-X(A(S z&ptyOc?w0dv!av=P{=Br2m|xHDipIYEOMSJ^b=3Pg9y!)C+2)JI0Be1GwH`dQ+?JtxJ8tFZDl6{mlW(T+Z)Uyb1nVF?6!UqPI>zBOVn~;A~^0-M8z-b z({LBcSDv_*jyTDG1-Z6*!EwAh>XUNsU6&WuC8pnsELMMS@K>j!l|;{mn=~vPxa(`i z7b*6+=5y*1`0E7oshd|-dv*g0ie-1P1d{8qDm=r%Yl_G4aH#no|*b_2wlj`Kv?#e!>{B>Kf?|aM+`7p{^nfqjWVcchIBx z>$(S-8QXKNAw$%GkKo`sO*3SyH~eL^Ce_tbS`(AVV>GW~c?rk>%K?rGJFv>&lprao=V&vAQAr_BkDkk>Q?yv@ywVc)MoRoTM*#KbS7M`?^3 z{qo^}F5(nsm!?{Z^7YDq2^nlU!iM6Ht2N?gyfuKQ<@v-tcDBYZ)4|`O+2fZWNj9ic z4k@_r7+|?O-)-6J1NE16d^!9U$|X(o&5T}RjFW@Eb432QJKN6-M<*$g)?-Uh78m^?i%(?~kGKuPG zb2{Khdp++7xtO1Dwy5|-AN#F@S&k$)rW;AS9<{DF$bW4!!6$NmCxZhJC6%>pWrQi6 zdbJh8d;&4`Jq-8WpGjtT)7UD$2=W=hP46-;2^SWj?814dwmHvaQw>7 z8+Px!{2loiGulaVhgFa~;Dq!6j4uj4XtM1^F(tkZQ^-XaJp=`(+(JlHyi)xv zv9`;_=M_BVq@V%^gfXJ@@X;&YT@QPD9?wLO*GB#MJi*~`82sD*LFD~j=LZQn`H5z6 znf!9|AM8-;%wU502^aUwFb8Yhj8;0}N&Uq!mqc2&k01%O$6~VCo1W_W^P|$i1JZqq zq?_pYzCoUf;$jj`)cPdKTya`%cf%w()bqaQ*-&=E7oCP6WpyvEa1Qg0w&&d@sr20P z?VVrLjoO>7M@Yl$6pUxwF_6bt8ND|cvQR|HIpJ&}nLkUlQ6p*Giu%g&n`E_{j4b$} z_?>3>?st1p_fQ5c-rsGXgTqM8zV3HBsD53zwl6EZvTc7#koLb}DP4rviK{b3JL1QB zGUbodu$}43!T%qSaT>Ja)&V)9sH=kw!l!bEUw#YC{8_h=zf5<{+P5ESW#l9-t}9Rxf`7xcw|=wI9nNVGSfZ zw+SSDdSxL84tegnT%@~>1N)xaAX+IC@fLYB>1-cpn2jYsVn}uU5RcUo7~6^cYg^t@ zU(!g6qAG4CHE=Lp{*>u@0KD$JtaE~m)e)9-8M25GL#JTCldiYZl($TKofx7N$5*}_ zm3nbOU}-m9gxJM21G}|~ReZIWj&#W8v;O7_-LmL4G!eCnE4COgo$XSG$ zp4Hc>7nRNHN$*zjW-A^jeRMnGAJ5HZ5)(-@+WwXqK_>}c>>zfn9r|b~FCf}x`k?6M zK8woWVjT!jQYj-s3O1Y=EPNM(l_^%nyPX+unj8DPn$nX9Rv#IoezE&Z^mERB*zH8C zKpF;^zAwiz&e#Do`TWpxvUO!M%Q$S)R$hm$cdQ(SoDD+SGmJWo?Ev!b>_w7A4QO%P zGL+{BJ!34Tfp!Icu8$A#1>5&IC45hcex2@mS2-0E|8!T28k=NnHqdTUL~sBLi$+;Q zKMwv#P7JcadRq5rkG^Dwe2k(b6sP_VbNYr{WAvCNA%KDCDK52t&q9e=az`jVeQ z4c9KJP4pSR;eVDX*yt>O6vE==fzgX9^7d3>o*myNWg&V)E`Y4ZM#zh=qF7N89tvJ^ zOom>g#1IIEN6v)B0{#kje+ga_7uNy-fx39vh}FFJ}r` zMN4IKj&4WUh5Q6t9=xy1OfGHP@w@`3Npcz*evHWuuvy-2 zy`f)^U0o0gq$k=kr%7KvSnKa<5gg1me=|in^#!qSwL;kbnF{+qq{5{iI=^Tho&El` zp9Xi?wm(Y2nVB=ksD1<%=Z~7fc)1Lh^^zXtZJ__cRh{t#I@E~6;O$3kjg0_aTqMn9 z&+rvoN55XUW6luNqLWopTA%)?ZAk-hc;uh0S5K4CXnajE{MVp6F1&9S0(^~H^O99h zX)fF`GLMm(BJkay7D1yUVs@W;WdyC0hoUK%s)RUx0Tz~8RfUAM0P8tDF>vmqwlF%Y zVEDn-v+8pI0+db%hFgc=nc`5x^1upRz6Ic0P&Wnqw-ddDM;dZ1NvuDeEPwU0xK=Ou zuQD#%^oC!Zyt(c72~(>y#O(TnJ&$m;wVSe+k*zBNb@wQI0)`hj*IFIJRRTm(XD!Ql zi*Q?nLj6ku>p=JR6ykBb-3ED?TV(C(os~)38THjMj#wvL^d0S=C&2vZf;nxV!#I|C+Kih=lIyDe4K3FFA>N#W4)o z^w2tzRq*TN4Vp2{(H%a(S2+5LIa;;S4;*%W5;Gn$CazSPPD`cNesRwotlJ}8K5(4% z)7zNUZeI{#PRJQ6dFQ(t{aqU98eRO7#QQ;9{UG?1bX9V`f{qMv+Z7n`-c2C&OFG9# z8HbLkPR~kS4_fL3#tvWp!+)-yqF|t_-1!=~?_=FjVGu(qY&-Ymm34V8wE%cvXHRjo z$#9!(c{sf#e)pVRdJ3f|&9~nUi4N}}!(MnD;&Gr<{^~89YdF1_M8cFrBuAFL>+d8O zo#f#~D7sjoOIT-54)VqRj~w9KbNo^Kluj-wr^C%ZjgT1kp6zM`GV_K;gS}3e%>=j& z z`D*dC3q~Ee5Q<+BNH@3|k&kOC?nxMNeO0Qyeqz)Pi%`J3;qucHojQ}%VbJNR;-E3S z%y^L6Bu#O((3NURlUMo&_fk&|lf=32&>9cHTn+Q-aap(5ZQ{Q5rl_4EwKqAxAc?B{ zCv=;Pi8zTpM_Lhp$$Y8)v}LOL z=EC-Yva|b92sL$oXJij3*$#kLmIsWwnm2`BPfu;=5WKs7&5+hD=Kc!q!tF^E-wn_H z+Q5m(W!JTbuN}4<@NH4PBDP79%BI#dl^fSc&{ZfmGjPNx5599_1&ti6|>?)ZoI7vw4!m-!jf zT@4A}N0=53t-?zJOH;fBPYW z9+@hrQu1{Zki#;j|E?sX-$p=Ok77YCizhypUfC`Cb<262tDZR``?O}sbul%4IoK7q z@4APjf7Vo0qt~bHh>nxe$ji#K&rOyK!(a(MupwP0U+;(yD-d<-9^>w(>uuL_UFg%q zyYo!^DTlF9sdJpLVfL#V*oj0tApf*CmB_6PJAB&588X}WXUT@U3I#gO;W}0izL-dc z69)cjF*}W)1SVmirhz`0;ppG{0xXed&$~oE!v{}8WD#TYViq0P{bD}3hwF8!gEyot zAOB+-{0WCPgSR0jR(SCjQM^D{w60@TA+5zWzoFQ|!T>R!qED>R4kVCYxq@=7%@h&u z%yifJ!6cksr{zK6O>6WArTtWX3TX7B(+k4=yFtO*d-DuQBDG@v9R<{X436K9SSB>tYim;4=c?Al~h!H?Rd*5B-xi?^c8T zydFzN?6Yq4`TPq=Gyh?r!;`eiIx_R_+U{(2l^Qu%baM4F+US|jLHLL_!xUQgK#}Dc z>S(I!5dtB-@2BPBnC#DSa&wOA1g6R0LVF?^Sp7eLgJt=&AC7YTp558D6I-c@o8!X( z%mSf1r@;W*9Yrado!^wIhPhpRenZHyvasW1kD-^g$uXk)3(k%#JwDI+sQCcQDI!#s zW+~)}3m8!MWL**ny>s}8IgXE~e*UF6 zoql)6X}zUgrsUWwaFi?)HxQ)YVtD6S!t#;{rRVM!phC%$$C(h~h4WE0RNvF0n(i`d zp@IdTugrf1su89b!VUnY=PY2}v|nUdPRQ@}9VQJmtFXs~qa-QVkHw{HCuQie-aYnP zIu^-t>hk3HieeJjAWm1I z;Rl4*%Y3_SN5psPNnHjwDEO-7?z(1812Ev{fx5S`T$6bLHK=0qUR)8R5luEb5{IhW zfDaBHEmKN{!H0WOCl9=9_#9`}1{ahMVTBFu zlQ(+r<~HwFBZddtGH&8TL!JVB#ThS8>5Y;;4D46 z<`IK80=tO?sElLWP+P<2;nUS#4w|Vl@z?)u7Qr!&AnGTkPX-6ctn;21W&$}-nbhrPC%x9ZUM-x9d4=npmZX$vjlq$dzLPZ(%Y z0mt!{!|~bfRNuZwiF)g6GQ_uFr?vKQH|_hYz~{S45`M<2+2ZwE$#u6iT($*-`KO{g z!JDjxD(S9&`?m?{kkuT%e|af|#4K$E)Kn3!40sK97Dvg30q{wbc<{ zoM*QU$zuEJC?JU`Sc?*opW5Dt0Fvc@Z0QwcmPDW=EmBhzGw`BW#)BmeYQP<@vdg4~ z3bE;4^elxDbCV<*^HisH=>PB`U6EsfB)FmAem&0dlxsF|Lsh6N{JmRLdgj@0^*Y1O zA}>yjRG|q=X6-fxxosCDI3ejXZ>`5)@7aeP7PT8!5#-#6&{%u3IpmmkY+fs|H009M zt7*U0ceQrI^%pl}49C5LsKJFC2hwx`jw}9?Xm)_lKwo)q%vsTVL@PT>`Ym-a3}hj6 z<6F8j+AO>GR5VHGia;)2`lBG0so2mg(dWbnFjcs5fEYT^YE&KwTWsw~onyxT6qE5L zoZIGmAc`!hbo5^hd-KDZxJltgFQ#f0p-iU3EDV9AF#(JV&E4NQ{X=UPX~94%W%?FI z+vGF%mz`vUTmc;xDu!~3dU!Ec!p+74Qm%?bXa{BdrRda$D>=BewU*<(hffl?Xa+Mn zwKO`y4KI5wjvWk<`OsXa@%Yq#=TB?8kh=ZR{|->R@y;u?EMey0benFWVkGn2?Jf&d z1NXtVIefd0~S^aw|xwKp8fg9sIJP#Uqjazu2Fmu;<|%(a`aYGdiUHlsg2`|pXE1bMSa9T;6K=+ zOozMf=rfdlQ~>ZGC|$WE?H%{3OI}xqaL&#y8qEoO0`6u5O=?qL(9eY;#SS} z%?HyjntK|565LCLD(RH=sW-D+v+B3}h@do8wPc4ln2Q`0xG%*jxq#cqjFkZEqLS`w zl3&h`nI&&DX2QG`f7%}JHP1rT2;wznTze2!??7I=oU&<(wom2^f*;7SRpal@f`)*C zecsSj`^7=LG{VL(JNbBd)m7TE2h7Ho@sVFOv^lbk11Iv&(8?s&=gAMmY2FYyuWvVR z37ZWM3F(Ewt z&x?Y6E$eA&O;Vz06#HZhINoxs-j)pq2>qS0Tn0l}t3t>TZmWW84!w z94wMFlL1$c2}sk;G#OkKmY8%HZahc^eb}!Cs-MVj%86AG(vtLif&CwUq9**6A6IND zzblqNfn2A|lcNQB_PI4y^L5tAb`V_WxU|jH_9Vl6I@lbW-a2zfkkP&2CjrR=7Bxhz z`+=rJre&VpKD@#dHi=ArkEJgC3nS@7Zr1_DgJ7RB9;rFZT98d1CuQQ1*Vg$s{p z8$5AFb7SO`8N1=e){(tskUw_A7Jw8?rm-ipusL=?nQ(KZan}$sBdDd0S%yKsjK{iv zi)eG>7ei}UH!8z0JQdknJy!`LAAGC#yJjTh##n+i-3@>%dyVll=fc^oUaH{v8-#VY zLx{l5BAubN&cmZ_FzArk7fEzpb@O1i90s5*+=ccov9{RPAxa`*^#Muc+#l7+4j(?I zbUe0~+SOU>#hG?u&iptoF&BYWo>5vK?{D>>b%;y=szT$!U%ELgroKKt7NC zJ!I=%aji$fJ5vMKjbNv|Mx4`8+G4`~Hkr)%>shmF6_@x=LXvzb*M@@(SciVT>%>*m zs$q9__xVHuh&W&*Tp=3`SYjfd=wUz@JSaDMB^Rstcpn$ffHg(}i#w8*DNe;zZz*P8 zyr1{WSuIm}7XC#i+ir0rfW2PvtA$ij^XpI>26TFP7Tnc$=fSacW(&2^-5l?q_n72% z@c?>+V4Pe|$vu}8d_2^;+u0KAVr2WhoX0(AHQ^b*&+|f6ee2Y4r=GX?VdxU;l(VsR zq_guEUg-A56ZZYYHot2qtQdXynNTq#aKrCEg4~rgBRn;8O~GAV5rq0`a<_o& zexp0HPB#8*>FlAvP6QmO!y4^g!Y}yv$|rjjbCK>VxvbEu;vx_`mM|IodycaF$Hs0& zf{1ngzzf!fY4K=VXUTqdd8?=}-f3uwFW$)7jI&5VerE1BeiX4vtZPS7Q|i|$TmfO@ z0O}ye_&(v6a7HLwRAmp_{9m!&1lRrnpVD!PCCP#GG!Z&wo01Bfos_uG-{;In{Iy>x zFzyb}ia{qMO)#Js?2BuPy->##71p4HosJFWzdwo@QWj=BzjID^ZG%Sn=l$c}kh54S zeRHixqUIs;KPy}09{5+)`UO39I|3wR3xyvcq9{owG6pl{jas2{-PtvBzmQj1RN6ul z*k0GhwnyzSaUwt>k}*L6#>Wl<~_t?gUR#?vtgK8-eRMK zU%r=xsUI6xTWQSno59-5OY_gyYs{G!F1r$Yl@}+XCe`E@y6`+!R9{=f2UwdOv?%T7oN#_UbtrTbna z#j!TuZYKacTy~%rAJkUxdYsWwwL84HT90>a-ud1+3+u_E|BjAANb2fdhhFQ&3 zuTH3tneojzoP(t7;*@~EnH8o)IFg~(9QgDhV_4w?)5~-x(;!oZd6piK>sXO(Lqcwm zIP~4%HWhG4m>MO5gu$olOwTrWZpU}6ST#_0Jqv*}{J|4raPn(gfXXpDI3eRRU8 zaLRL?u0#`vq1bC#Rz2Zqh$&eIL$TOfC~a8lpUL$JYo)B#TUI;pm(3GWF^X!jzKhh* zF<&<|uA2A1infQAlBo1UF`B>+Z@>#4a+2ilH)hHfC#;>dL7D@lA3_&x~#tFQ1-*>>xvK3Yz=35?fY!#A5A?U73i)9<~58rvkqFm&!?rfe%m_) z%euidcTgc;Tv4;*<%q>IOBMGB*SRgh82v}7BT!}5K0Wj=LbMG}K&Q|>s3ZBwdS+6A zR2Da*i=2wP=FW%qQuAPeRc(JRl(6zt(r$u4^CmFpeS^RsDK5{FV~*dmE3}u`k#v@c z>0ji@%jkVPU@>%1eAvx5aQJwDfj_JM%x^DtZpF+OjBH7DB2E_=7As?p4VwFFI8F+hL@Vu zp~ygmI93S;8cCjM+bx4|7~a}nG7z>=AZz;*aKfg?$h50_;P#fd!NX5^tLCS&R!*`K zQlM`Mk_PREDL&kz6D|P@ySk=N&E^)xll5J{rFr<+Um1xIR^}cS?0F z=Ahf-CJ*;L>lWIew|U&VmMz&(PN_M5FxLoC{Tb4^l5 z0PqHU3I0eJiI-b>fi4mZ3^pC(BGe%(5sBV^p>=!2`1$krH+K4{zPj10ciJRQy6u!eclv*n~0;~C6n4IIQY9QCo^LR zhpvB1$o{3rG-U{#%im6smpKuqzw%k9g#6bapv}i zD)Y{5+CLTev%{x<-~5hz&~h%^m!f}-5258Ad#{Gw2Y}N!72G_C;{+Mpjo-MPq{?p5 za77G~J`pXe@qK6FN$Qw^7&^}K6!tn$CCtaKmCmJc1EXec2T0(G`7aQqml`;XCe&hT zf@M2oikGW`!g51eQ!$&uO6kV>w0SDr=52Lw1TcDb?~h7WG<18@sP5!5)x%;pahGL( zL5g~k7oKhcH=3^G2*V93W?a3%n{cu7HlYA<>Z=ep;qbh;frjNsZ-L=9O(SoqHLY8e zy?ZHk21tfmeOx9dtrty9#-GK9!Y4(#ln@~_J7%vzSxdaviUK0~+@IbTnujab8BLv( zAKoqaMqNgTl1qk~1F$b&AJ#mLwMmlds|Je&(}17|aZsViQy-~KNcFqXG`a-nfZ1h`;ai)W{a>gm zU;i5)Y&r`x3UG0h6;g7eVG}jUI6=XDWKlb+S#K|4`?l_zE}t*G*u4-F1Abrbp3BA= z`hT-8*Wb>gQ3-I%8TSmr<;0^S1}N)@scJ6isD5VaMfOP}SVbJoU}~G@DDZe)@P2lzs7$GpRa~~qcJ$3vzG3rI&B!8cWXf$w_arlnoEd+ zdoSQcCi3$s!f318H#_woCv<$xeAEir-#VGf+P}sbO8DGPt`7XRo@Y4B0nYQz2SVdN zh+x{TU^1ta54##9h8>YfL<=W9qmf%Vf+u0Z>(+@Am4^+LE z_{>F~g}K}5ipjU({lKqT@4kr?y|$`HE)63=LBS>)2EHi08=6lNbYsKegy zqi=Y0X)W2^F3cYa4QaSb4P0=z29m19e@76X&dc3@Jx3B|ce7>&M9s`2Rxj~mMgxgX z2u~1opM+a8B^5?sU&U|pTAf|Fn%^tHo`9P)Mk`Oo1x#(9op*UL7D(}RcmRLRBYsh( zms2&+K+k~BKfzB!N?ad#*b1@#N{eaEPzjB@=vQXJ;1;$#)ti11d(~^~wYG_(m#c#4 zLEkf}29mgrKS^9@Ty*K^8Qk9&g|Pqn*9sP&S}M)(iglI2Z|#$y(Y!)ou9f12nEP7< z%4!51U#kk3RyiMnMAD@%5*K;8Fl4JA&KuyuZ9R9uq_|;3PXG@2_lzCN#w1!Aj&GDA zcg0pBe=L7v&Kb?cuTZNrs?Ls8Ju7whtk*y0d50^#`o22OgqAEHWaI(d`UT!In`{HQ zUkKz`N9-OL0jhd}`fTevcki`RTnl6C0jHzgK0}P(70ksdiOPBM@YPGG({B|caYUF9 z;=qr4T9EM?6uH=Lh_kO3V!C3zkg`yMROxs8lUey33Y{d*>s}R((y;Zg9Vq{IX0C%` zo)Cm)*3i+gXf&P%3Yt>W+Ejx19jKgKL9*is8j^W9vJ{>9Wudr%dUpKEs-%b-PZ82I`RqAbnq5NoQgS2Np0GNd-jHqd;$^Lk3g3A1CkrivBhFas2VNU z>}jvi-)3V6HuZ*2dTqn1{~Iiz;=XX{rfod}Rq;YWvt~kO^=9fEb;5B%9@kY=6O0zF z)YZuj;$_{o8p2(sflS4pU-)vMPu3N?kd`5jL50EZl$YNJ;(fRaXI1IE^bcw7j4nA; z;8AQpp)V7#wrv)ShxFp??WKW^>?1y3?tv)-F9qS6FK{whB$LHe&<9&9){N(28GJb` zBKME}U*aE!?AGFJi4oz0gqn)E8l$i1vjew-Vp+C2o4yd>!)b#l3@dc@7N$`XT>kVE$2Umr? z7(8zBdff>fL{hEFRWAg*mNeHrxR)!nz+pKpCC3S0`L`Q`m#QDcR`bBw73XQw0*t#Q zVc$kxMF=qqG1K-Q4jYgn_8u>sOjwx*ciXsYvRlG;cB>(?J)-;De4EdLRFOvz=1t1! zncwgexcw`bwf5t_O#v7;`EE*Ij@mzWcSa|noh#2td7Rhk-}>hE$>a|dQrhm#xa|b% zHh0)wEWD0Bgtj@w?|eU4%+$ZSC~O7s>6F&ZO;l8aO}P@DiknYOItl{&Uq#Z@ZtNxc zY}j<`QYJ2Ay_2G4jItXt)VM!(jp`PcB*=u^Mb>XXOByhk6PyB8-6B?XoiYKV`0*n( z!`B@gcLGeVe!Ls5N9SOQ>S9qYPG^LRxyErZ_Lm0<`M9q;hb!S&E^wZ>66SMjy>>5ho+i~TUZ$-&fxB~@(40~R6x5o>Osd02&o`t#QdC;^ zL_?C2M}mpFo8tE9PW(geo>A8}Xka(lz>!Ya{eL_+n7%YrxB-~-HjE2j##7Ii1~9UE z;7j$kyYA2JQR|d}{x|`xa3FotwC4Ejv@4!c(@-I*pzQbkt&y35$?RBz97W@)BL5K^ zl!*|F*O966J)Wql_x9YpQ|Q=8u5ivUmaNyJToPfLgu1L%)Rsd)^IgUh(d<{kfaQ=% zB(%$Cf6M}yxs|mHI<_v(74+Bf8n8J*KX9KHS;Fi*q3=)r%Lc5r^p&Kk5bFA`u{Wla z^in+ux8ltd`V#C9Va#447t-~}2 zObiv=h#uhL2fL@ZPZq8P`#@C4_%~Y&R^ zl*KsJg_e)~+9zY>MzJJ%nA+7Hq*hf^)4gWzY9((JZ6KPvktRwreB6HW7%9t1SU*`1 zm2kG2}yySt1RQUz9yU)tB+7hzK6!XaYXV#Cb6B{Rvgah31(l$*nBhxW0z3Sxq z{cg_F6>6BoyS$j^k$z;BB7@Dx7hM-3d=2-oZgWHS?>AoA?97ke!$Tsfi(E`Zf+F}hepM7)0m^`HciC-%%pAq% z+>m%D*%w02J36g~NaakaR}Z={yt;vowkvhLsO_ywXbcz%j#B{Pz2V=rG0 z2yElNK$u-AQJL9Lr6-Yyay<(w5jsk2tR?Y{zlbgO;kh77T@MGaF$1~S=Dg^5Ur|eu z6UNub_qnjo@*74V1URMWs>7@KQE1d@UEuF7ES;R%_md6W<0hKa*vT&olX6wgqQ+m5 zeO#=dwNuhS@mo24e^)+2^O0;Yd&-(B1AGjjxlZ{XDVx-5mdFf7+T<Update the deck configuration by removing the fixtures in locations A1 and B1. Either remove the fixtures from the deck configuration or update the protocol.", "deck_conflict_info": "Update the deck configuration by removing the {{currentFixture}} in location {{cutout}}. Either remove the fixture from the deck configuration or update the protocol.", "deck_conflict": "Deck location conflict", + "deck_hardware": "Deck hardware", "deck_map": "Deck Map", "default_values": "Default values", "example": "Example", @@ -255,6 +256,7 @@ "updated": "Updated", "usb_connected_no_port_info": "USB Port Connected", "usb_port_connected": "USB Port {{port}}", + "usb_port_number": "USB-{{port}}", "value": "Value", "values_are_view_only": "Values are view-only", "value_out_of_range_generic": "Value must be in range", diff --git a/app/src/organisms/DeviceDetailsDeckConfiguration/AddFixtureModal.tsx b/app/src/organisms/DeviceDetailsDeckConfiguration/AddFixtureModal.tsx index e03306facbf..99f2328b1e4 100644 --- a/app/src/organisms/DeviceDetailsDeckConfiguration/AddFixtureModal.tsx +++ b/app/src/organisms/DeviceDetailsDeckConfiguration/AddFixtureModal.tsx @@ -17,13 +17,29 @@ import { } from '@opentrons/components' import { useDeckConfigurationQuery, + useModulesQuery, useUpdateDeckConfigurationMutation, } from '@opentrons/react-api-client' import { getCutoutDisplayName, getFixtureDisplayName, + HEATER_SHAKER_CUTOUTS, + HEATERSHAKER_MODULE_V1, + HEATERSHAKER_MODULE_V1_FIXTURE, + MAGNETIC_BLOCK_V1_FIXTURE, + SINGLE_CENTER_CUTOUTS, + SINGLE_LEFT_CUTOUTS, + SINGLE_RIGHT_CUTOUTS, STAGING_AREA_CUTOUTS, STAGING_AREA_RIGHT_SLOT_FIXTURE, + STAGING_AREA_SLOT_WITH_MAGNETIC_BLOCK_V1_FIXTURE, + TEMPERATURE_MODULE_CUTOUTS, + TEMPERATURE_MODULE_V2, + TEMPERATURE_MODULE_V2_FIXTURE, + THERMOCYCLER_MODULE_CUTOUTS, + THERMOCYCLER_MODULE_V2, + THERMOCYCLER_V2_FRONT_FIXTURE, + THERMOCYCLER_V2_REAR_FIXTURE, TRASH_BIN_ADAPTER_FIXTURE, WASTE_CHUTE_CUTOUT, WASTE_CHUTE_FIXTURES, @@ -43,7 +59,7 @@ import type { import type { ModalHeaderBaseProps } from '../../molecules/Modal/types' import type { LegacyModalProps } from '../../molecules/LegacyModal' -const GENERIC_WASTE_CHUTE_OPTION = 'WASTE_CHUTE' +// type CutoutContents = Omit interface AddFixtureModalProps { cutoutId: CutoutId @@ -52,6 +68,12 @@ interface AddFixtureModalProps { providedFixtureOptions?: CutoutFixtureId[] isOnDevice?: boolean } +type OptionStage = + | 'modulesOrFixtures' + | 'fixtureOptions' + | 'moduleOptions' + | 'wasteChuteOptions' + | 'providedOptions' export function AddFixtureModal({ cutoutId, @@ -62,9 +84,26 @@ export function AddFixtureModal({ }: AddFixtureModalProps): JSX.Element { const { t } = useTranslation(['device_details', 'shared']) const { updateDeckConfiguration } = useUpdateDeckConfigurationMutation() + const { data: modulesData } = useModulesQuery() const deckConfig = useDeckConfigurationQuery()?.data ?? [] - const [showWasteChuteOptions, setShowWasteChuteOptions] = React.useState( - false + const unconfiguredMods = + modulesData?.data.filter( + attachedMod => + !deckConfig.some( + ({ opentronsModuleSerialNumber }) => + attachedMod.serialNumber === opentronsModuleSerialNumber + ) + ) ?? [] + + let initialStage: OptionStage = SINGLE_CENTER_CUTOUTS.includes(cutoutId) // only mag block (a module) can be configured in column 2 + ? 'moduleOptions' + : 'modulesOrFixtures' + if (providedFixtureOptions != null) { + // only show provided options if given as props + initialStage = 'providedOptions' + } + const [optionStage, setOptionStage] = React.useState( + initialStage ) const modalHeader: ModalHeaderBaseProps = { @@ -72,75 +111,232 @@ export function AddFixtureModal({ slotName: getCutoutDisplayName(cutoutId), }), hasExitIcon: providedFixtureOptions == null, - onClick: () => setShowAddFixtureModal(false), + onClick: () => { + setShowAddFixtureModal(false) + }, } const modalProps: LegacyModalProps = { title: t('add_to_slot', { slotName: getCutoutDisplayName(cutoutId), }), - onClose: () => setShowAddFixtureModal(false), + onClose: () => { + setShowAddFixtureModal(false) + }, closeOnOutsideClick: true, childrenPadding: SPACING.spacing24, width: '26.75rem', } - const availableFixtures: CutoutFixtureId[] = [TRASH_BIN_ADAPTER_FIXTURE] - if (STAGING_AREA_CUTOUTS.includes(cutoutId)) { - availableFixtures.push(STAGING_AREA_RIGHT_SLOT_FIXTURE) + let availableOptions: CutoutConfig[][] = [] + + if (providedFixtureOptions != null) { + availableOptions = providedFixtureOptions?.map(o => [ + { + cutoutId, + cutoutFixtureId: o, + opentronsModuleSerialNumber: undefined, + }, + ]) + } else if (optionStage === 'fixtureOptions') { + if ( + SINGLE_RIGHT_CUTOUTS.includes(cutoutId) || + SINGLE_LEFT_CUTOUTS.includes(cutoutId) + ) { + availableOptions = [ + ...availableOptions, + [ + { + cutoutId, + cutoutFixtureId: TRASH_BIN_ADAPTER_FIXTURE, + }, + ], + ] + } + if (STAGING_AREA_CUTOUTS.includes(cutoutId)) { + availableOptions = [ + ...availableOptions, + [ + { + cutoutId, + cutoutFixtureId: STAGING_AREA_RIGHT_SLOT_FIXTURE, + }, + ], + ] + } + } else if (optionStage === 'moduleOptions') { + availableOptions = [ + ...availableOptions, + [ + { + cutoutId, + cutoutFixtureId: MAGNETIC_BLOCK_V1_FIXTURE, + }, + ], + ] + if (SINGLE_RIGHT_CUTOUTS.includes(cutoutId)) { + availableOptions = [ + ...availableOptions, + [ + { + cutoutId, + cutoutFixtureId: STAGING_AREA_SLOT_WITH_MAGNETIC_BLOCK_V1_FIXTURE, + }, + ], + ] + } + if (unconfiguredMods.length > 0) { + if (THERMOCYCLER_MODULE_CUTOUTS.includes(cutoutId)) { + const unconfiguredTCs = unconfiguredMods + .filter(mod => mod.moduleModel === THERMOCYCLER_MODULE_V2) + .map(mod => [ + { + cutoutId: THERMOCYCLER_MODULE_CUTOUTS[0], + cutoutFixtureId: THERMOCYCLER_V2_REAR_FIXTURE, + opentronsModuleSerialNumber: mod.serialNumber, + }, + { + cutoutId: THERMOCYCLER_MODULE_CUTOUTS[1], + cutoutFixtureId: THERMOCYCLER_V2_FRONT_FIXTURE, + opentronsModuleSerialNumber: mod.serialNumber, + }, + ]) + availableOptions = [...availableOptions, ...unconfiguredTCs] + } + if ( + HEATER_SHAKER_CUTOUTS.includes(cutoutId) && + unconfiguredMods.some(m => m.moduleModel === HEATERSHAKER_MODULE_V1) + ) { + const unconfiguredHeaterShakers = unconfiguredMods + .filter(mod => mod.moduleModel === HEATERSHAKER_MODULE_V1) + .map(mod => [ + { + cutoutId, + cutoutFixtureId: HEATERSHAKER_MODULE_V1_FIXTURE, + opentronsModuleSerialNumber: mod.serialNumber, + }, + ]) + availableOptions = [...availableOptions, ...unconfiguredHeaterShakers] + } + if ( + TEMPERATURE_MODULE_CUTOUTS.includes(cutoutId) && + unconfiguredMods.some(m => m.moduleModel === TEMPERATURE_MODULE_V2) + ) { + const unconfiguredTemperatureModules = unconfiguredMods + .filter(mod => mod.moduleModel === TEMPERATURE_MODULE_V2) + .map(mod => [ + { + cutoutId, + cutoutFixtureId: TEMPERATURE_MODULE_V2_FIXTURE, + opentronsModuleSerialNumber: mod.serialNumber, + }, + ]) + availableOptions = [ + ...availableOptions, + ...unconfiguredTemperatureModules, + ] + } + } + } else if (optionStage === 'wasteChuteOptions') { + availableOptions = WASTE_CHUTE_FIXTURES.map(fixture => [ + { + cutoutId, + cutoutFixtureId: fixture, + }, + ]) + } + + let nextStageOptions = null + if (optionStage === 'modulesOrFixtures') { + nextStageOptions = ( + <> + {SINGLE_CENTER_CUTOUTS.includes(cutoutId) ? null : ( + { + setOptionStage('fixtureOptions') + }} + isOnDevice={isOnDevice} + /> + )} + { + setOptionStage('moduleOptions') + }} + isOnDevice={isOnDevice} + /> + + ) + } else if ( + optionStage === 'fixtureOptions' && + cutoutId === WASTE_CHUTE_CUTOUT + ) { + nextStageOptions = ( + <> + { + setOptionStage('wasteChuteOptions') + }} + isOnDevice={isOnDevice} + /> + + ) } - const handleAddODD = (requiredFixtureId: CutoutFixtureId): void => { + const handleAddODD = (addedCutoutConfigs: CutoutConfig[]): void => { if (setCurrentDeckConfig != null) setCurrentDeckConfig( (prevDeckConfig: DeckConfiguration): DeckConfiguration => - prevDeckConfig.map((fixture: CutoutConfig) => - fixture.cutoutId === cutoutId - ? { ...fixture, cutoutFixtureId: requiredFixtureId } - : fixture - ) + prevDeckConfig.map((fixture: CutoutConfig) => { + const replacementCutoutConfig = addedCutoutConfigs.find( + c => c.cutoutId === fixture.cutoutId + ) + return replacementCutoutConfig ?? fixture + }) ) setShowAddFixtureModal(false) } - const fixtureOptions = providedFixtureOptions ?? availableFixtures - const fixtureOptionsWithDisplayNames: Array< - [CutoutFixtureId | 'WASTE_CHUTE', string] - > = fixtureOptions.map(fixture => [fixture, getFixtureDisplayName(fixture)]) - - const showSelectWasteChuteOptions = - cutoutId === WASTE_CHUTE_CUTOUT && providedFixtureOptions == null - - const fixtureOptionsWithDisplayNamesAndGenericWasteChute = fixtureOptionsWithDisplayNames.concat( - showSelectWasteChuteOptions - ? [[GENERIC_WASTE_CHUTE_OPTION, t('waste_chute')]] - : [] - ) - - fixtureOptionsWithDisplayNamesAndGenericWasteChute.sort((a, b) => - a[1].localeCompare(b[1]) - ) - - const wasteChuteOptionsWithDisplayNames = WASTE_CHUTE_FIXTURES.map( - fixture => [fixture, getFixtureDisplayName(fixture)] - ).sort((a, b) => a[1].localeCompare(b[1])) as Array<[CutoutFixtureId, string]> - - const displayedFixtureOptions = showWasteChuteOptions - ? wasteChuteOptionsWithDisplayNames - : fixtureOptionsWithDisplayNamesAndGenericWasteChute - - const handleAddDesktop = (requiredFixtureId: CutoutFixtureId): void => { - const newDeckConfig = deckConfig.map(fixture => - fixture.cutoutId === cutoutId - ? { ...fixture, cutoutFixtureId: requiredFixtureId } - : fixture - ) + const handleAddDesktop = (addedCutoutConfigs: CutoutConfig[]): void => { + const newDeckConfig = deckConfig.map(fixture => { + const replacementCutoutConfig = addedCutoutConfigs.find( + c => c.cutoutId === fixture.cutoutId + ) + return replacementCutoutConfig ?? fixture + }) updateDeckConfiguration(newDeckConfig) setShowAddFixtureModal(false) } + const fixtureOptions = availableOptions.map(cutoutConfigs => ( + m.serialNumber === cutoutConfigs[0].opentronsModuleSerialNumber + )?.usbPort.port + )} + buttonText={t('add')} + onClickHandler={() => { + isOnDevice + ? handleAddODD(cutoutConfigs) + : handleAddDesktop(cutoutConfigs) + }} + isOnDevice={isOnDevice} + /> + )) + return ( <> {isOnDevice ? ( @@ -155,40 +351,8 @@ export function AddFixtureModal({ {t('add_to_slot_description')} - {displayedFixtureOptions.map( - ([cutoutFixtureOption, fixtureDisplayName]) => { - const onClickHandler = - cutoutFixtureOption === GENERIC_WASTE_CHUTE_OPTION - ? () => setShowWasteChuteOptions(true) - : () => handleAddODD(cutoutFixtureOption) - const buttonText = - cutoutFixtureOption === GENERIC_WASTE_CHUTE_OPTION - ? t('select_options') - : t('add') - - return ( - - - - {fixtureDisplayName} - - {buttonText} - - - ) - } - )} + {fixtureOptions} + {nextStageOptions} @@ -197,43 +361,15 @@ export function AddFixtureModal({ {t('add_fixture_description')} - {displayedFixtureOptions.map( - ([cutoutFixtureOption, fixtureDisplayName]) => { - const onClickHandler = - cutoutFixtureOption === GENERIC_WASTE_CHUTE_OPTION - ? () => setShowWasteChuteOptions(true) - : () => handleAddDesktop(cutoutFixtureOption) - const buttonText = - cutoutFixtureOption === GENERIC_WASTE_CHUTE_OPTION - ? t('select_options') - : t('add') - - return ( - - - - {fixtureDisplayName} - - - {buttonText} - - - - ) - } - )} + {fixtureOptions} + {nextStageOptions} - {showWasteChuteOptions ? ( + {optionStage === 'wasteChuteOptions' ? ( setShowWasteChuteOptions(false)} + onClick={() => { + setOptionStage('fixtureOptions') + }} aria-label="back" paddingX={SPACING.spacing16} marginTop={'1.44rem'} @@ -289,3 +425,41 @@ const GO_BACK_BUTTON_STYLE = css` opacity: 70%; } ` + +interface FixtureOptionProps { + onClickHandler: React.MouseEventHandler + optionName: string + buttonText: string + isOnDevice: boolean +} +export function FixtureOption(props: FixtureOptionProps): JSX.Element { + const { onClickHandler, optionName, buttonText, isOnDevice } = props + return isOnDevice ? ( + + + {props.optionName} + + {props.buttonText} + + ) : ( + + {optionName} + {buttonText} + + ) +} diff --git a/app/src/organisms/DeviceDetailsDeckConfiguration/__tests__/AddFixtureModal.test.tsx b/app/src/organisms/DeviceDetailsDeckConfiguration/__tests__/AddFixtureModal.test.tsx index 846c060dc27..74d150d92dc 100644 --- a/app/src/organisms/DeviceDetailsDeckConfiguration/__tests__/AddFixtureModal.test.tsx +++ b/app/src/organisms/DeviceDetailsDeckConfiguration/__tests__/AddFixtureModal.test.tsx @@ -4,6 +4,7 @@ import { describe, it, beforeEach, vi, expect, afterEach } from 'vitest' import { useDeckConfigurationQuery, + useModulesQuery, useUpdateDeckConfigurationMutation, } from '@opentrons/react-api-client' import { @@ -17,6 +18,7 @@ import { AddFixtureModal } from '../AddFixtureModal' import type { UseQueryResult } from 'react-query' import type { DeckConfiguration } from '@opentrons/shared-data' +import type { Modules } from '@opentrons/api-client' vi.mock('@opentrons/react-api-client') const mockSetShowAddFixtureModal = vi.fn() @@ -45,23 +47,25 @@ describe('Touchscreen AddFixtureModal', () => { vi.mocked(useDeckConfigurationQuery).mockReturnValue(({ data: [], } as unknown) as UseQueryResult) + vi.mocked(useModulesQuery).mockReturnValue(({ + data: { data: [] }, + } as unknown) as UseQueryResult) }) it('should render text and buttons', () => { render(props) screen.getByText('Add to slot D3') screen.getByText( - 'Choose a fixture below to add to your deck configuration. It will be referenced during protocol analysis.' + 'Choose an item below to add to your deck configuration. It will be referenced during protocol analysis.' ) - screen.getByText('Staging area slot') - screen.getByText('Trash bin') - screen.getByText('Waste chute') - expect(screen.getAllByText('Add').length).toBe(2) - expect(screen.getAllByText('Select options').length).toBe(1) + screen.getByText('Fixtures') + screen.getByText('Modules') + expect(screen.getAllByText('Select options').length).toBe(2) }) - it('should a mock function when tapping app button', () => { + it('should set deck config when tapping add button', () => { render(props) + fireEvent.click(screen.getAllByText('Select options')[1]) fireEvent.click(screen.getAllByText('Add')[0]) expect(mockSetCurrentDeckConfig).toHaveBeenCalled() }) @@ -74,7 +78,7 @@ describe('Touchscreen AddFixtureModal', () => { render(props) screen.getByText('Add to slot D3') screen.getByText( - 'Choose a fixture below to add to your deck configuration. It will be referenced during protocol analysis.' + 'Choose an item below to add to your deck configuration. It will be referenced during protocol analysis.' ) expect(screen.queryByText('Staging area slot')).toBeNull() screen.getByText('Trash bin') @@ -105,8 +109,12 @@ describe('Desktop AddFixtureModal', () => { render(props) screen.getByText('Add to slot D3') screen.getByText( - 'Add this fixture to your deck configuration. It will be referenced during protocol analysis.' + 'Add this item to your deck configuration. It will be referenced during protocol analysis.' ) + + screen.getByText('Fixtures') + screen.getByText('Modules') + fireEvent.click(screen.getAllByText('Select options')[0]) screen.getByText('Staging area slot') screen.getByText('Trash bin') screen.getByText('Waste chute') @@ -121,8 +129,11 @@ describe('Desktop AddFixtureModal', () => { render(props) screen.getByText('Add to slot A1') screen.getByText( - 'Add this fixture to your deck configuration. It will be referenced during protocol analysis.' + 'Add this item to your deck configuration. It will be referenced during protocol analysis.' ) + screen.getByText('Fixtures') + screen.getByText('Modules') + fireEvent.click(screen.getAllByText('Select options')[0]) screen.getByText('Trash bin') screen.getByRole('button', { name: 'Add' }) }) @@ -132,23 +143,39 @@ describe('Desktop AddFixtureModal', () => { render(props) screen.getByText('Add to slot B3') screen.getByText( - 'Add this fixture to your deck configuration. It will be referenced during protocol analysis.' + 'Add this item to your deck configuration. It will be referenced during protocol analysis.' ) + screen.getByText('Fixtures') + screen.getByText('Modules') + fireEvent.click(screen.getAllByText('Select options')[0]) screen.getByText('Staging area slot') screen.getByText('Trash bin') expect(screen.getAllByRole('button', { name: 'Add' }).length).toBe(2) }) - it('should call a mock function when clicking add button', () => { + it('should only render module options in column 2', () => { + props = { ...props, cutoutId: 'cutoutB2' } + render(props) + screen.getByText('Add to slot B2') + screen.getByText( + 'Add this item to your deck configuration. It will be referenced during protocol analysis.' + ) + screen.getByText('Magnetic Block GEN1') + expect(screen.getByRole('button', { name: 'Add' })).toBeInTheDocument() + }) + + it('should call update deck config when add button is clicked', () => { props = { ...props, cutoutId: 'cutoutA1' } render(props) - fireEvent.click(screen.getByRole('button', { name: 'Add' })) + fireEvent.click(screen.getAllByText('Select options')[1]) + fireEvent.click(screen.getByText('Add')) expect(mockUpdateDeckConfiguration).toHaveBeenCalled() }) it('should display appropriate Waste Chute options when the generic Waste Chute button is clicked', () => { render(props) - fireEvent.click(screen.getByRole('button', { name: 'Select options' })) + fireEvent.click(screen.getAllByText('Select options')[0]) // click fixtures + fireEvent.click(screen.getByRole('button', { name: 'Select options' })) // click waste chute options expect(screen.getAllByRole('button', { name: 'Add' }).length).toBe( WASTE_CHUTE_FIXTURES.length ) @@ -161,6 +188,7 @@ describe('Desktop AddFixtureModal', () => { it('should allow a user to exit the Waste Chute submenu by clicking "go back"', () => { render(props) + fireEvent.click(screen.getAllByText('Select options')[0]) // click fixtures fireEvent.click(screen.getByRole('button', { name: 'Select options' })) fireEvent.click(screen.getByText('Go back')) diff --git a/app/src/organisms/DeviceDetailsDeckConfiguration/__tests__/DeviceDetailsDeckConfiguration.test.tsx b/app/src/organisms/DeviceDetailsDeckConfiguration/__tests__/DeviceDetailsDeckConfiguration.test.tsx index f3b008320af..5c8d3974dc8 100644 --- a/app/src/organisms/DeviceDetailsDeckConfiguration/__tests__/DeviceDetailsDeckConfiguration.test.tsx +++ b/app/src/organisms/DeviceDetailsDeckConfiguration/__tests__/DeviceDetailsDeckConfiguration.test.tsx @@ -6,6 +6,7 @@ import { describe, it, beforeEach, vi, afterEach } from 'vitest' import { DeckConfigurator } from '@opentrons/components' import { useDeckConfigurationQuery, + useModulesQuery, useUpdateDeckConfigurationMutation, } from '@opentrons/react-api-client' @@ -60,6 +61,7 @@ describe('DeviceDetailsDeckConfiguration', () => { props = { robotName: ROBOT_NAME, } + vi.mocked(useModulesQuery).mockReturnValue({ data: { data: [] } } as any) vi.mocked(useDeckConfigurationQuery).mockReturnValue({ data: [] } as any) vi.mocked(useUpdateDeckConfigurationMutation).mockReturnValue({ updateDeckConfiguration: mockUpdateDeckConfiguration, @@ -89,7 +91,7 @@ describe('DeviceDetailsDeckConfiguration', () => { screen.getByText('otie deck configuration') screen.getByRole('button', { name: 'Setup Instructions' }) screen.getByText('Location') - screen.getByText('Fixture') + screen.getByText('Deck hardware') screen.getByText('mock DeckConfigurator') }) diff --git a/app/src/organisms/DeviceDetailsDeckConfiguration/index.tsx b/app/src/organisms/DeviceDetailsDeckConfiguration/index.tsx index 9c1d852253a..97194aa90d7 100644 --- a/app/src/organisms/DeviceDetailsDeckConfiguration/index.tsx +++ b/app/src/organisms/DeviceDetailsDeckConfiguration/index.tsx @@ -21,6 +21,7 @@ import { } from '@opentrons/components' import { useDeckConfigurationQuery, + useModulesQuery, useUpdateDeckConfigurationMutation, } from '@opentrons/react-api-client' import { @@ -30,6 +31,10 @@ import { SINGLE_SLOT_FIXTURES, SINGLE_LEFT_SLOT_FIXTURE, SINGLE_RIGHT_SLOT_FIXTURE, + SINGLE_CENTER_SLOT_FIXTURE, + SINGLE_LEFT_CUTOUTS, + getDeckDefFromRobotType, + FLEX_ROBOT_TYPE, } from '@opentrons/shared-data' import { useNotifyCurrentMaintenanceRun } from '../../resources/maintenance_runs' @@ -39,7 +44,7 @@ import { AddFixtureModal } from './AddFixtureModal' import { useIsRobotViewable, useRunStatuses } from '../Devices/hooks' import { useIsEstopNotDisengaged } from '../../resources/devices/hooks/useIsEstopNotDisengaged' -import type { CutoutId } from '@opentrons/shared-data' +import type { CutoutFixtureId, CutoutId } from '@opentrons/shared-data' const DECK_CONFIG_REFETCH_INTERVAL = 5000 const RUN_REFETCH_INTERVAL = 5000 @@ -48,10 +53,14 @@ interface DeviceDetailsDeckConfigurationProps { robotName: string } +function getDisplayLocationForCutoutIds(cutouts: CutoutId[]): string { + return cutouts.map(cutoutId => getCutoutDisplayName(cutoutId)).join(' + ') +} + export function DeviceDetailsDeckConfiguration({ robotName, }: DeviceDetailsDeckConfigurationProps): JSX.Element | null { - const { t } = useTranslation('device_details') + const { t, i18n } = useTranslation('device_details') const [ showSetupInstructionsModal, setShowSetupInstructionsModal, @@ -63,9 +72,11 @@ export function DeviceDetailsDeckConfiguration({ null ) + const { data: modulesData } = useModulesQuery() const deckConfig = useDeckConfigurationQuery({ refetchInterval: DECK_CONFIG_REFETCH_INTERVAL }) .data ?? [] + const deckDef = getDeckDefFromRobotType(FLEX_ROBOT_TYPE) const { updateDeckConfiguration } = useUpdateDeckConfigurationMutation() const { isRunRunning } = useRunStatuses() const { data: maintenanceRunData } = useNotifyCurrentMaintenanceRun({ @@ -80,26 +91,109 @@ export function DeviceDetailsDeckConfiguration({ setShowAddFixtureModal(true) } - const handleClickRemove = (cutoutId: CutoutId): void => { - const isRightCutout = SINGLE_RIGHT_CUTOUTS.includes(cutoutId) - const singleSlotFixture = isRightCutout - ? SINGLE_RIGHT_SLOT_FIXTURE - : SINGLE_LEFT_SLOT_FIXTURE + const handleClickRemove = ( + cutoutId: CutoutId, + cutoutFixtureId: CutoutFixtureId + ): void => { + let replacementFixtureId: CutoutFixtureId = SINGLE_CENTER_SLOT_FIXTURE + if (SINGLE_RIGHT_CUTOUTS.includes(cutoutId)) { + replacementFixtureId = SINGLE_RIGHT_SLOT_FIXTURE + } else if (SINGLE_LEFT_CUTOUTS.includes(cutoutId)) { + replacementFixtureId = SINGLE_LEFT_SLOT_FIXTURE + } - const newDeckConfig = deckConfig.map(fixture => - fixture.cutoutId === cutoutId - ? { ...fixture, cutoutFixtureId: singleSlotFixture } - : fixture - ) + const fixtureGroup = + deckDef.cutoutFixtures.find(cf => cf.id === cutoutFixtureId) + ?.fixtureGroup ?? {} + let newDeckConfig = deckConfig + if (cutoutId in fixtureGroup) { + const groupMap = + fixtureGroup[cutoutId]?.find(group => + Object.entries(group).every(([cId, cfId]) => + deckConfig.find( + config => + config.cutoutId === cId && config.cutoutFixtureId === cfId + ) + ) + ) ?? {} + newDeckConfig = deckConfig.map(cutoutConfig => + cutoutConfig.cutoutId in groupMap + ? { + ...cutoutConfig, + cutoutFixtureId: replacementFixtureId, + opentronsModuleSerialNumber: undefined, + } + : cutoutConfig + ) + } else { + newDeckConfig = deckConfig.map(cutoutConfig => + cutoutConfig.cutoutId === cutoutId + ? { + ...cutoutConfig, + cutoutFixtureId: replacementFixtureId, + opentronsModuleSerialNumber: undefined, + } + : cutoutConfig + ) + } updateDeckConfiguration(newDeckConfig) } // do not show standard slot in fixture display list - const fixtureDisplayList = deckConfig.filter( - fixture => - fixture.cutoutFixtureId != null && - !SINGLE_SLOT_FIXTURES.includes(fixture.cutoutFixtureId) + const { displayList: fixtureDisplayList } = deckConfig.reduce<{ + displayList: Array<{ displayLocation: string; displayName: string }> + groupedCutoutIds: CutoutId[] + }>( + (acc, { cutoutId, cutoutFixtureId, opentronsModuleSerialNumber }) => { + if ( + cutoutFixtureId == null || + SINGLE_SLOT_FIXTURES.includes(cutoutFixtureId) + ) { + return acc + } + const displayName = getFixtureDisplayName( + cutoutFixtureId, + modulesData?.data.find( + m => m.serialNumber === opentronsModuleSerialNumber + )?.usbPort.port + ) + const fixtureGroup = + deckDef.cutoutFixtures.find(cf => cf.id === cutoutFixtureId) + ?.fixtureGroup ?? {} + if (cutoutId in fixtureGroup) { + const groupMap = + fixtureGroup[cutoutId]?.find(group => + Object.entries(group).every(([cId, cfId]) => + deckConfig.find( + config => + config.cutoutId === cId && config.cutoutFixtureId === cfId + ) + ) + ) ?? {} + const groupedCutoutIds = Object.keys(groupMap) as CutoutId[] + const displayLocation = getDisplayLocationForCutoutIds(groupedCutoutIds) + if (acc.groupedCutoutIds.includes(cutoutId)) { + return acc // only list grouped fixtures once + } else { + return { + displayList: [...acc.displayList, { displayLocation, displayName }], + groupedCutoutIds: [...acc.groupedCutoutIds, ...groupedCutoutIds], + } + } + } + return { + ...acc, + displayList: [ + ...acc.displayList, + { + displayLocation: getDisplayLocationForCutoutIds([cutoutId]), + displayName, + }, + ], + } + }, + { displayList: [], groupedCutoutIds: [] } ) return ( @@ -132,11 +226,7 @@ export function DeviceDetailsDeckConfiguration({ width="100%" borderBottom={BORDERS.lineBorder} > - + {`${robotName} ${t('deck_configuration')}`} @@ -197,30 +285,28 @@ export function DeviceDetailsDeckConfiguration({ width="32rem" > - {t('location')} - {t('fixture')} + {t('location')} + + {i18n.format(t('deck_hardware'), 'capitalize')} + {fixtureDisplayList.length > 0 ? ( - fixtureDisplayList.map(fixture => ( + fixtureDisplayList.map(({ displayLocation, displayName }) => ( - - {getCutoutDisplayName(fixture.cutoutId)} - - - {getFixtureDisplayName(fixture.cutoutFixtureId)} - + {displayLocation} + {displayName} )) ) : ( @@ -248,11 +334,7 @@ export function DeviceDetailsDeckConfiguration({ paddingBottom={SPACING.spacing24} width="100%" > - + {t('offline_deck_configuration')} diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/ChooseModuleToConfigureModal.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/ChooseModuleToConfigureModal.tsx new file mode 100644 index 00000000000..59bc0b6e52e --- /dev/null +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/ChooseModuleToConfigureModal.tsx @@ -0,0 +1,154 @@ +import * as React from 'react' +import { createPortal } from 'react-dom' +import { useTranslation } from 'react-i18next' +import { + useDeckConfigurationQuery, + useModulesQuery, +} from '@opentrons/react-api-client' +import { + ALIGN_CENTER, + COLORS, + DIRECTION_COLUMN, + DIRECTION_ROW, + Flex, + Icon, + SPACING, + StyledText, + TYPOGRAPHY, +} from '@opentrons/components' +import { + getFixtureDisplayName, + getCutoutFixturesForModuleModel, + MAGNETIC_BLOCK_V1, +} from '@opentrons/shared-data' +import { getTopPortalEl } from '../../../../App/portal' +import { LegacyModal } from '../../../../molecules/LegacyModal' +import { Modal } from '../../../../molecules/Modal' + +import type { ModuleModel, DeckDefinition } from '@opentrons/shared-data' +import { FixtureOption } from '../../../DeviceDetailsDeckConfiguration/AddFixtureModal' + +interface ModuleFixtureOption { + moduleModel: ModuleModel + usbPort?: number + serialNumber?: string +} +interface ChooseModuleToConfigureModalProps { + handleConfigureModule: (moduleSerialNumber?: string) => void + onCloseClick: () => void + deckDef: DeckDefinition + isOnDevice: boolean + requiredModuleModel: ModuleModel +} + +export const ChooseModuleToConfigureModal = ( + props: ChooseModuleToConfigureModalProps +): JSX.Element => { + const { + handleConfigureModule, + onCloseClick, + deckDef, + requiredModuleModel, + isOnDevice, + } = props + const { t } = useTranslation(['protocol_setup', 'shared']) + const attachedModules = useModulesQuery().data?.data ?? [] + const deckConfig = useDeckConfigurationQuery()?.data ?? [] + const unconfiguredModuleMatches = + attachedModules.filter( + attachedMod => + attachedMod.moduleModel === requiredModuleModel && + !deckConfig.some( + ({ opentronsModuleSerialNumber }) => + attachedMod.serialNumber === opentronsModuleSerialNumber + ) + ) ?? [] + + const connectedOptions: ModuleFixtureOption[] = unconfiguredModuleMatches.map( + attachedMod => ({ + moduleModel: attachedMod.moduleModel, + usbPort: attachedMod.usbPort.port, + serialNumber: attachedMod.serialNumber, + }) + ) + const passiveOptions: ModuleFixtureOption[] = + requiredModuleModel === MAGNETIC_BLOCK_V1 + ? [{ moduleModel: MAGNETIC_BLOCK_V1 }] + : [] + const fixtureOptions = [...connectedOptions, ...passiveOptions].map( + ({ moduleModel, serialNumber, usbPort }) => { + const moduleFixtures = getCutoutFixturesForModuleModel( + moduleModel, + deckDef + ) + return ( + { + handleConfigureModule(serialNumber) + }} + optionName={getFixtureDisplayName(moduleFixtures[0].id, usbPort)} + buttonText={t('shared:add')} + isOnDevice={isOnDevice} + /> + ) + } + ) + + return createPortal( + isOnDevice ? ( + + + + + {fixtureOptions} + + + + + ) : ( + + + + {t('deck_conflict')} + + + } + onClose={onCloseClick} + width="27.75rem" + > + + + + {fixtureOptions} + + + + + ), + getTopPortalEl() + ) +} diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/LocationConflictModal.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/LocationConflictModal.tsx index b4a8e634b0d..c696b4ecbdf 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/LocationConflictModal.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/LocationConflictModal.tsx @@ -25,11 +25,10 @@ import { getCutoutDisplayName, getFixtureDisplayName, getModuleDisplayName, - SINGLE_RIGHT_CUTOUTS, - SINGLE_LEFT_SLOT_FIXTURE, - SINGLE_RIGHT_SLOT_FIXTURE, THERMOCYCLER_MODULE_V1, THERMOCYCLER_MODULE_V2, + getCutoutFixturesForModuleModel, + getFixtureIdByCutoutIdFromModuleSlotName, } from '@opentrons/shared-data' import { getTopPortalEl } from '../../../../App/portal' import { LegacyModal } from '../../../../molecules/LegacyModal' @@ -41,11 +40,14 @@ import type { CutoutId, CutoutFixtureId, ModuleModel, + DeckDefinition, } from '@opentrons/shared-data' +import { ChooseModuleToConfigureModal } from './ChooseModuleToConfigureModal' interface LocationConflictModalProps { onCloseClick: () => void cutoutId: CutoutId + deckDef: DeckDefinition missingLabwareDisplayName?: string | null requiredFixtureId?: CutoutFixtureId requiredModule?: ModuleModel @@ -61,9 +63,12 @@ export const LocationConflictModal = ( missingLabwareDisplayName, requiredFixtureId, requiredModule, + deckDef, isOnDevice = false, } = props const { t, i18n } = useTranslation(['protocol_setup', 'shared']) + + const [showModuleSelect, setShowModuleSelect] = React.useState(false) const deckConfig = useDeckConfigurationQuery().data ?? [] const { updateDeckConfiguration } = useUpdateDeckConfigurationMutation() const deckConfigurationAtLocationFixtureId = deckConfig.find( @@ -89,39 +94,54 @@ export const LocationConflictModal = ( ? getFixtureDisplayName(deckConfigurationAtA1) : currentFixtureDisplayName + const handleConfigureModule = (moduleSerialNumber?: string): void => { + if (requiredModule != null) { + const slotName = cutoutId.replace('cutout', '') + const moduleFixtures = getCutoutFixturesForModuleModel( + requiredModule, + deckDef + ) + const moduleFixtureIdByCutoutId = getFixtureIdByCutoutIdFromModuleSlotName( + slotName, + moduleFixtures, + deckDef + ) + + const newDeckConfig = deckConfig.map(existingCutoutConfig => { + const replacementCutoutFixtureId = + moduleFixtureIdByCutoutId[existingCutoutConfig.cutoutId] + return existingCutoutConfig.cutoutId in moduleFixtureIdByCutoutId && + replacementCutoutFixtureId != null + ? { + ...existingCutoutConfig, + cutoutFixtureId: replacementCutoutFixtureId, + opentronsModuleSerialNumber: moduleSerialNumber, + } + : existingCutoutConfig + }) + updateDeckConfiguration(newDeckConfig) + } + onCloseClick() + } + const handleUpdateDeck = (): void => { - if (requiredFixtureId != null) { + if (requiredModule != null) { + setShowModuleSelect(true) + } else if (requiredFixtureId != null) { const newRequiredFixtureDeckConfig = deckConfig.map(fixture => fixture.cutoutId === cutoutId - ? { ...fixture, cutoutFixtureId: requiredFixtureId } + ? { + ...fixture, + cutoutFixtureId: requiredFixtureId, + opentronsModuleSerialNumber: undefined, + } : fixture ) - updateDeckConfiguration(newRequiredFixtureDeckConfig) + onCloseClick() } else { - const isRightCutout = SINGLE_RIGHT_CUTOUTS.includes(cutoutId) - const singleSlotFixture = isRightCutout - ? SINGLE_RIGHT_SLOT_FIXTURE - : SINGLE_LEFT_SLOT_FIXTURE - - const newSingleSlotDeckConfig = deckConfig.map(fixture => - fixture.cutoutId === cutoutId - ? { ...fixture, cutoutFixtureId: singleSlotFixture } - : fixture - ) - - // add A1 and B1 single slot config for thermocycler - const newThermocyclerDeckConfig = isThermocycler - ? newSingleSlotDeckConfig.map(fixture => - fixture.cutoutId === 'cutoutA1' || fixture.cutoutId === 'cutoutB1' - ? { ...fixture, cutoutFixtureId: SINGLE_LEFT_SLOT_FIXTURE } - : fixture - ) - : newSingleSlotDeckConfig - - updateDeckConfiguration(newThermocyclerDeckConfig) + onCloseClick() } - onCloseClick() } let protocolSpecifiesDisplayName = '' @@ -133,6 +153,18 @@ export const LocationConflictModal = ( protocolSpecifiesDisplayName = getModuleDisplayName(requiredModule) } + if (showModuleSelect && requiredModule) { + return createPortal( + , + getTopPortalEl() + ) + } return createPortal( isOnDevice ? ( unknown -} - -export const MultipleModulesModal = ( - props: MultipleModulesModalProps -): JSX.Element => { - const { t } = useTranslation(['protocol_setup', 'shared']) - const isOnDevice = useSelector(getIsOnDevice) - return createPortal( - isOnDevice ? ( - - - {t('multiple_of_most_modules')} - 2 temperature modules plugged into the usb ports - - - ) : ( - - - - - - {t('multiple_modules_explanation')} - - - {t('multiple_modules_learn_more')} - - - - {t('example')} - - - {t('multiple_modules_example')} - - 2 temperature modules plugged into the usb ports - - - {t('shared:close')} - - - - ), - getTopPortalEl() - ) -} diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/OT2MultipleModulesHelp.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/OT2MultipleModulesHelp.tsx new file mode 100644 index 00000000000..eaac0c079a6 --- /dev/null +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/OT2MultipleModulesHelp.tsx @@ -0,0 +1,123 @@ +import * as React from 'react' +import { createPortal } from 'react-dom' +import { useTranslation } from 'react-i18next' +import { + ALIGN_FLEX_END, + Box, + DIRECTION_COLUMN, + DIRECTION_ROW, + Flex, + Icon, + Link, + PrimaryButton, + SPACING, + StyledText, + TYPOGRAPHY, +} from '@opentrons/components' +import { getTopPortalEl } from '../../../../App/portal' +import { Banner } from '../../../../atoms/Banner' +import { LegacyModal } from '../../../../molecules/LegacyModal' +import multipleModuleHelp from '../../../../assets/images/Moam_modal_image.png' + +const HOW_TO_MULTIPLE_MODULES_HREF = + 'https://support.opentrons.com/s/article/Using-modules-of-the-same-type-on-the-OT-2' + +export function OT2MultipleModulesHelp(): JSX.Element { + const { t } = useTranslation(['protocol_setup', 'shared']) + const [ + showMultipleModulesModal, + setShowMultipleModulesModal, + ] = React.useState(false) + + const onCloseClick = (): void => { + setShowMultipleModulesModal(false) + } + return ( + <> + + setShowMultipleModulesModal(true)} + closeButton={ + + {t('learn_more')} + + } + > + + + {t('multiple_modules')} + + {t('view_moam')} + + + + {showMultipleModulesModal + ? createPortal( + + + + + + {t('multiple_modules_explanation')} + + + {t('multiple_modules_learn_more')} + + + + {t('example')} + + + + {t('multiple_modules_example')} + + + 2 temperature modules plugged into the usb ports + + + {t('shared:close')} + + + , + getTopPortalEl() + ) + : null} + + ) +} diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupFixtureList.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupFixtureList.tsx index 4e65fd3759d..b8ad582af17 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupFixtureList.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupFixtureList.tsx @@ -16,8 +16,11 @@ import { TYPOGRAPHY, } from '@opentrons/components' import { + FLEX_MODULE_ADDRESSABLE_AREAS, + FLEX_ROBOT_TYPE, SINGLE_SLOT_FIXTURES, getCutoutDisplayName, + getDeckDefFromRobotType, getFixtureDisplayName, } from '@opentrons/shared-data' import { StatusLabel } from '../../../../atoms/StatusLabel' @@ -27,73 +30,47 @@ import { NotConfiguredModal } from './NotConfiguredModal' import { getFixtureImage } from './utils' import { DeckFixtureSetupInstructionsModal } from '../../../DeviceDetailsDeckConfiguration/DeckFixtureSetupInstructionsModal' +import type { DeckDefinition } from '@opentrons/shared-data' import type { CutoutConfigAndCompatibility } from '../../../../resources/deck_configuration/hooks' interface SetupFixtureListProps { deckConfigCompatibility: CutoutConfigAndCompatibility[] } - +/** + * List items of all "non-module" fixtures e.g. staging slot, waste chute, trash bin... + * @param props + * @returns JSX.Element + */ export const SetupFixtureList = (props: SetupFixtureListProps): JSX.Element => { const { deckConfigCompatibility } = props - const { t, i18n } = useTranslation('protocol_setup') + const deckDef = getDeckDefFromRobotType(FLEX_ROBOT_TYPE) return ( <> - - - {i18n.format(t('fixture_name'), 'capitalize')} - - - {t('location')} - - - {t('status')} - - - - {deckConfigCompatibility.map(cutoutConfigAndCompatibility => { - return ( - - ) - })} - + {deckConfigCompatibility.map(cutoutConfigAndCompatibility => { + return cutoutConfigAndCompatibility.requiredAddressableAreas.some(raa => + FLEX_MODULE_ADDRESSABLE_AREAS.includes(raa) + ) ? null : ( // don't list modules here, they're covered by SetupModuleList + + ) + })} ) } -interface FixtureListItemProps extends CutoutConfigAndCompatibility {} +interface FixtureListItemProps extends CutoutConfigAndCompatibility { + deckDef: DeckDefinition +} export function FixtureListItem({ cutoutId, cutoutFixtureId, compatibleCutoutFixtureIds, missingLabwareDisplayName, + deckDef, }: FixtureListItemProps): JSX.Element { const { t } = useTranslation('protocol_setup') @@ -155,6 +132,7 @@ export function FixtureListItem({ setShowLocationConflictModal(false)} cutoutId={cutoutId} + deckDef={deckDef} missingLabwareDisplayName={missingLabwareDisplayName} requiredFixtureId={compatibleCutoutFixtureIds[0]} /> diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupModulesList.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupModulesList.tsx index 8f0879e9609..cf258c2bc00 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupModulesList.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupModulesList.tsx @@ -27,11 +27,11 @@ import { HEATERSHAKER_MODULE_TYPE, HEATERSHAKER_MODULE_V1, MAGNETIC_BLOCK_V1, + OT2_ROBOT_TYPE, TC_MODULE_LOCATION_OT2, TC_MODULE_LOCATION_OT3, } from '@opentrons/shared-data' -import { Banner } from '../../../../atoms/Banner' import { TertiaryButton } from '../../../../atoms/buttons' import { StatusLabel } from '../../../../atoms/StatusLabel' import { Tooltip } from '../../../../atoms/Tooltip' @@ -48,7 +48,7 @@ import { useRunCalibrationStatus, } from '../../hooks' import { LocationConflictModal } from './LocationConflictModal' -import { MultipleModulesModal } from './MultipleModulesModal' +import { OT2MultipleModulesHelp } from './OT2MultipleModulesHelp' import { UnMatchedModuleWarning } from './UnMatchedModuleWarning' import { getModuleImage } from './utils' @@ -70,7 +70,6 @@ interface SetupModulesListProps { export const SetupModulesList = (props: SetupModulesListProps): JSX.Element => { const { robotName, runId } = props - const { t } = useTranslation('protocol_setup') const moduleRenderInfoForProtocolById = useModuleRenderInfoForProtocolById( runId ) @@ -85,125 +84,53 @@ export const SetupModulesList = (props: SetupModulesListProps): JSX.Element => { const calibrationStatus = useRunCalibrationStatus(robotName, runId) - const [ - showMultipleModulesModal, - setShowMultipleModulesModal, - ] = React.useState(false) - const moduleModels = map( moduleRenderInfoForProtocolById, ({ moduleDef }) => moduleDef.model ) - - const hasADuplicateModule = new Set(moduleModels).size !== moduleModels.length - + const showOT2MoamHelp = + robotModel === OT2_ROBOT_TYPE && + new Set(moduleModels).size !== moduleModels.length return ( <> - {showMultipleModulesModal ? ( - setShowMultipleModulesModal(false)} - /> - ) : null} - {hasADuplicateModule ? ( - - setShowMultipleModulesModal(true)} - closeButton={ - - {t('learn_more')} - - } - > - - - {t('multiple_modules')} - - {t('view_moam')} - - - - ) : null} + {showOT2MoamHelp ? : null} {remainingAttachedModules.length !== 0 && missingModuleIds.length !== 0 ? ( ) : null} - - - {t('module_name')} - - - {t('location')} - - - {t('status')} - - - - {map( - moduleRenderInfoForProtocolById, - ({ - moduleDef, - attachedModuleMatch, - slotName, - moduleId, - conflictedFixture, - }) => { - return ( - - ) - } - )} - + + {map( + moduleRenderInfoForProtocolById, + ({ + moduleDef, + attachedModuleMatch, + slotName, + moduleId, + conflictedFixture, + }) => { + return ( + + ) + } + )} ) } @@ -358,13 +285,13 @@ export function ModulesListItem({ onCloseClick={() => setShowLocationConflictModal(false)} cutoutId={cutoutIdForSlotName} requiredModule={moduleModel} + deckDef={deckDef} /> ) : null} {showModuleWizard && attachedModuleMatch != null ? ( setShowModuleWizard(false)} - initialSlotName={slotName} isPrepCommandLoading={isCommandMutationLoading} prepCommandErrorMessage={ prepCommandErrorMessage === '' ? undefined : prepCommandErrorMessage @@ -404,13 +331,26 @@ export function ModulesListItem({ {subText}
    - - {getModuleType(moduleModel) === 'thermocyclerModuleType' - ? isFlex - ? TC_MODULE_LOCATION_OT3 - : TC_MODULE_LOCATION_OT2 - : slotName} - + + + {getModuleType(moduleModel) === 'thermocyclerModuleType' + ? isFlex + ? TC_MODULE_LOCATION_OT3 + : TC_MODULE_LOCATION_OT2 + : slotName} + + {attachedModuleMatch?.usbPort.port != null ? ( + + {t('usb_port_number', { + port: attachedModuleMatch.usbPort.port, + })} + + ) : null} + { onCloseClick: vi.fn(), cutoutId: 'cutoutB3', requiredFixtureId: TRASH_BIN_ADAPTER_FIXTURE, + deckDef: ot3StandardDeckV5 as any, } + vi.mocked(useModulesQuery).mockReturnValue({ data: { data: [] } } as any) vi.mocked(useDeckConfigurationQuery).mockReturnValue({ data: [mockFixture], } as UseQueryResult) @@ -64,18 +69,23 @@ describe('LocationConflictModal', () => { expect(mockUpdate).toHaveBeenCalled() }) it('should render the modal information for a module fixture conflict', () => { + vi.mocked(useModulesQuery).mockReturnValue({ + data: { data: [mockHeaterShaker] }, + } as any) props = { onCloseClick: vi.fn(), cutoutId: 'cutoutB3', requiredModule: 'heaterShakerModuleV1', + deckDef: ot3StandardDeckV5 as any, } render(props) screen.getByText('Protocol specifies') screen.getByText('Currently configured') - screen.getByText('Heater-Shaker Module GEN1') fireEvent.click(screen.getByRole('button', { name: 'Cancel' })) expect(props.onCloseClick).toHaveBeenCalled() fireEvent.click(screen.getByRole('button', { name: 'Update deck' })) + screen.getByText('Heater-Shaker Module GEN1 in USB-1') + fireEvent.click(screen.getByRole('button', { name: 'add' })) expect(mockUpdate).toHaveBeenCalled() }) it('should render the modal information for a single slot fixture conflict', () => { @@ -92,6 +102,7 @@ describe('LocationConflictModal', () => { cutoutId: 'cutoutB1', requiredFixtureId: SINGLE_RIGHT_SLOT_FIXTURE, missingLabwareDisplayName: 'a tiprack', + deckDef: ot3StandardDeckV5 as any, } render(props) screen.getByText('Deck location conflict') diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/MultipleModuleModal.test.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/OT2MultipleModulesHelp.test.tsx similarity index 60% rename from app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/MultipleModuleModal.test.tsx rename to app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/OT2MultipleModulesHelp.test.tsx index 532ab57c39b..984dc1e57e5 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/MultipleModuleModal.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/OT2MultipleModulesHelp.test.tsx @@ -5,31 +5,30 @@ import { describe, it, beforeEach, vi, expect } from 'vitest' import { renderWithProviders } from '../../../../../__testing-utils__' import { i18n } from '../../../../../i18n' import { getIsOnDevice } from '../../../../../redux/config' -import { MultipleModulesModal } from '../MultipleModulesModal' +import { OT2MultipleModulesHelp } from '../OT2MultipleModulesHelp' vi.mock('../../../../../redux/config') -const render = (props: React.ComponentProps) => { - return renderWithProviders(, { +const render = () => + renderWithProviders(, { i18nInstance: i18n, })[0] -} -describe('MultipleModulesModal', () => { - let props: React.ComponentProps +describe('OT2MultipleModulesHelp', () => { beforeEach(() => { - props = { onCloseClick: vi.fn() } vi.mocked(getIsOnDevice).mockReturnValue(false) }) it('should render the correct header', () => { - render(props) + render() + fireEvent.click(screen.getByText('Learn more')) screen.getByRole('heading', { name: 'Setting up multiple modules of the same type', }) }) it('should render the correct body', () => { - render(props) + render() + fireEvent.click(screen.getByText('Learn more')) screen.getByText( 'To use more than one of the same module in a protocol, you first need to plug in the module that’s called first in your protocol to the lowest numbered USB port on the robot. Continue in the same manner with additional modules.' ) @@ -40,7 +39,8 @@ describe('MultipleModulesModal', () => { screen.getByAltText('2 temperature modules plugged into the usb ports') }) it('should render a link to the learn more page', () => { - render(props) + render() + fireEvent.click(screen.getByText('Learn more')) expect( screen .getByRole('link', { @@ -51,23 +51,13 @@ describe('MultipleModulesModal', () => { 'https://support.opentrons.com/s/article/Using-modules-of-the-same-type-on-the-OT-2' ) }) - it('should call onCloseClick when the close button is pressed', () => { - render(props) - expect(props.onCloseClick).not.toHaveBeenCalled() + it('should call close info modal when the close button is pressed', () => { + render() + fireEvent.click(screen.getByText('Learn more')) const closeButton = screen.getByRole('button', { name: 'close' }) fireEvent.click(closeButton) - expect(props.onCloseClick).toHaveBeenCalled() - }) - it('should render the correct text and img for on device display', () => { - vi.mocked(getIsOnDevice).mockReturnValue(true) - render(props) - screen.getByText( - 'You can use multiples of most module types within a single Python protocol by connecting and loading the modules in a specific order. The robot will initialize the matching module attached to the lowest numbered port first, regardless of what deck slot it occupies.' - ) - const img = screen.getByRole('img') - expect(img.getAttribute('src')).toBe( - '/app/src/assets/images/on-device-display/multiple_modules_modal.png' - ) - screen.getByAltText('2 temperature modules plugged into the usb ports') + expect( + screen.queryByText('Setting up multiple modules of the same type') + ).toBeNull() }) }) diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupFixtureList.test.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupFixtureList.test.tsx index 69813bdbd8f..2aba1928899 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupFixtureList.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupFixtureList.test.tsx @@ -81,11 +81,8 @@ describe('SetupFixtureList', () => { ) }) - it('should render the headers and a fixture with configured status', () => { + it('should a fixture with configured status', () => { render(props) - screen.getByText('Fixture') - screen.getByText('Location') - screen.getByText('Status') screen.getByText('Waste chute with staging area slot') screen.getByRole('button', { name: 'View setup instructions' }) screen.getByText('D3') diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesList.test.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesList.test.tsx index 05df2fc9cef..b784e25d0c9 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesList.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesList.test.tsx @@ -3,7 +3,11 @@ import { when } from 'vitest-when' import { fireEvent, screen, waitFor } from '@testing-library/react' import { describe, it, beforeEach, expect, vi } from 'vitest' import { renderWithProviders } from '../../../../../__testing-utils__' -import { STAGING_AREA_RIGHT_SLOT_FIXTURE } from '@opentrons/shared-data' +import { + FLEX_ROBOT_TYPE, + OT2_ROBOT_TYPE, + STAGING_AREA_RIGHT_SLOT_FIXTURE, +} from '@opentrons/shared-data' import { i18n } from '../../../../../i18n' import { mockMagneticModule as mockMagneticModuleFixture, @@ -20,16 +24,17 @@ import { ModuleWizardFlows } from '../../../../ModuleWizardFlows' import { useIsFlex, useModuleRenderInfoForProtocolById, - useRunHasStarted, useUnmatchedModulesForProtocol, useRunCalibrationStatus, + useRobot, } from '../../../hooks' -import { MultipleModulesModal } from '../MultipleModulesModal' +import { OT2MultipleModulesHelp } from '../OT2MultipleModulesHelp' import { UnMatchedModuleWarning } from '../UnMatchedModuleWarning' import { SetupModulesList } from '../SetupModulesList' import { LocationConflictModal } from '../LocationConflictModal' import type { ModuleModel, ModuleType } from '@opentrons/shared-data' +import type { DiscoveredRobot } from '../../../../../redux/discovery/types' vi.mock('@opentrons/react-api-client') vi.mock('../../../hooks') @@ -37,7 +42,7 @@ vi.mock('../LocationConflictModal') vi.mock('../UnMatchedModuleWarning') vi.mock('../../../../ModuleCard/ModuleSetupModal') vi.mock('../../../../ModuleWizardFlows') -vi.mock('../MultipleModulesModal') +vi.mock('../OT2MultipleModulesHelp') vi.mock('../../../../../resources/runs') vi.mock('../../../../../redux/config') @@ -92,6 +97,9 @@ describe('SetupModulesList', () => { robotName: ROBOT_NAME, runId: RUN_ID, } + when(vi.mocked(useRobot)) + .calledWith(ROBOT_NAME) + .thenReturn({ robotModel: FLEX_ROBOT_TYPE } as DiscoveredRobot) mockChainLiveCommands = vi.fn() mockChainLiveCommands.mockResolvedValue(null) vi.mocked(ModuleSetupModal).mockReturnValue(
    mockModuleSetupModal
    ) @@ -118,15 +126,6 @@ describe('SetupModulesList', () => { ) }) - it('should render the list view headers', () => { - when(useRunHasStarted).calledWith(RUN_ID).thenReturn(false) - when(useModuleRenderInfoForProtocolById).calledWith(RUN_ID).thenReturn({}) - render(props) - screen.getByText('Module') - screen.getByText('Location') - screen.getByText('Status') - }) - it('should render a magnetic module that is connected', () => { vi.mocked(useModuleRenderInfoForProtocolById).mockReturnValue({ [mockMagneticModule.moduleId]: { @@ -301,8 +300,13 @@ describe('SetupModulesList', () => { screen.getByText('Connected') }) - it('should render the MoaM component when Moam is attached', () => { - vi.mocked(MultipleModulesModal).mockReturnValue(
    mock Moam modal
    ) + it('should render the MoaM component when Moam is attached and robot is OT2', () => { + when(vi.mocked(useRobot)) + .calledWith(ROBOT_NAME) + .thenReturn({ robotModel: OT2_ROBOT_TYPE } as DiscoveredRobot) + vi.mocked(OT2MultipleModulesHelp).mockReturnValue( +
    mock Moam modal
    + ) when(useUnmatchedModulesForProtocol) .calledWith(ROBOT_NAME, RUN_ID) .thenReturn({ @@ -355,8 +359,6 @@ describe('SetupModulesList', () => { }, }) render(props) - const help = screen.getByTestId('Banner_close-button') - fireEvent.click(help) screen.getByText('mock Moam modal') }) it('should render the module unmatching banner', () => { diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/index.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/index.tsx index 4e9afd58604..f1e06c2471a 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/index.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/index.tsx @@ -7,6 +7,10 @@ import { SPACING, useHoverTooltip, PrimaryButton, + DIRECTION_ROW, + JUSTIFY_SPACE_BETWEEN, + StyledText, + TYPOGRAPHY, } from '@opentrons/components' import { useToggleGroup } from '../../../../molecules/ToggleGroup/useToggleGroup' @@ -46,7 +50,7 @@ export const SetupModuleAndDeck = ({ hasModules, protocolAnalysis, }: SetupModuleAndDeckProps): JSX.Element => { - const { t } = useTranslation('protocol_setup') + const { t, i18n } = useTranslation('protocol_setup') const [selectedValue, toggleGroup] = useToggleGroup( t('list_view'), t('map_view') @@ -75,14 +79,51 @@ export const SetupModuleAndDeck = ({ {toggleGroup} {selectedValue === t('list_view') ? ( <> - {hasModules ? ( - - ) : null} - {requiredDeckConfigCompatibility.length > 0 ? ( - - ) : null} + + + {i18n.format(t('deck_hardware'), 'capitalize')} + + + {t('location')} + + + {t('status')} + + + + {hasModules ? ( + + ) : null} + {requiredDeckConfigCompatibility.length > 0 ? ( + + ) : null} + ) : ( diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/utils.ts b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/utils.ts index 10bf9b5148d..b0702fccdf9 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/utils.ts +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/utils.ts @@ -1,5 +1,10 @@ import { + HEATERSHAKER_MODULE_V1_FIXTURE, + MAGNETIC_BLOCK_V1_FIXTURE, STAGING_AREA_RIGHT_SLOT_FIXTURE, + TEMPERATURE_MODULE_V2_FIXTURE, + THERMOCYCLER_V2_FRONT_FIXTURE, + THERMOCYCLER_V2_REAR_FIXTURE, TRASH_BIN_ADAPTER_FIXTURE, WASTE_CHUTE_ONLY_FIXTURES, WASTE_CHUTE_STAGING_AREA_FIXTURES, @@ -48,6 +53,16 @@ export function getFixtureImage(cutoutFixtureId: CutoutFixtureId): string { return wasteChuteStagingArea } else if (cutoutFixtureId === TRASH_BIN_ADAPTER_FIXTURE) { return trashBin + } else if (cutoutFixtureId === THERMOCYCLER_V2_REAR_FIXTURE) { + return thermoModuleGen2 + } else if (cutoutFixtureId === THERMOCYCLER_V2_FRONT_FIXTURE) { + return thermoModuleGen2 + } else if (cutoutFixtureId === HEATERSHAKER_MODULE_V1_FIXTURE) { + return heaterShakerModule + } else if (cutoutFixtureId === TEMPERATURE_MODULE_V2_FIXTURE) { + return temperatureModule + } else if (cutoutFixtureId === MAGNETIC_BLOCK_V1_FIXTURE) { + return magneticBlockGen1 } else { return 'Error: unknown fixture' } diff --git a/app/src/organisms/Devices/ProtocolRun/utils/__tests__/getLabwareRenderInfo.test.ts b/app/src/organisms/Devices/ProtocolRun/utils/__tests__/getLabwareRenderInfo.test.ts index f96bacc93b6..0da562e9549 100644 --- a/app/src/organisms/Devices/ProtocolRun/utils/__tests__/getLabwareRenderInfo.test.ts +++ b/app/src/organisms/Devices/ProtocolRun/utils/__tests__/getLabwareRenderInfo.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect } from 'vitest' -import { transfer_settings, ot2DeckDefV4 } from '@opentrons/shared-data' +import { transfer_settings, ot2DeckDefV5 } from '@opentrons/shared-data' import { getLabwareRenderInfo } from '../getLabwareRenderInfo' import type { CompletedProtocolAnalysis, @@ -8,7 +8,7 @@ import type { } from '@opentrons/shared-data' const protocolWithMagTempTC = (transfer_settings as unknown) as CompletedProtocolAnalysis -const standardDeckDef = ot2DeckDefV4 as any +const standardDeckDef = ot2DeckDefV5 as any describe('getLabwareRenderInfo', () => { it('should gather labware coordinates', () => { diff --git a/app/src/organisms/Devices/ProtocolRun/utils/__tests__/getProtocolModulesInfo.test.ts b/app/src/organisms/Devices/ProtocolRun/utils/__tests__/getProtocolModulesInfo.test.ts index cd6b5d06408..93528250b0d 100644 --- a/app/src/organisms/Devices/ProtocolRun/utils/__tests__/getProtocolModulesInfo.test.ts +++ b/app/src/organisms/Devices/ProtocolRun/utils/__tests__/getProtocolModulesInfo.test.ts @@ -2,7 +2,7 @@ import { describe, it, expect } from 'vitest' import { transfer_settings, multiple_temp_modules, - ot2DeckDefV4, + ot2DeckDefV5, getModuleDef2, ProtocolAnalysisOutput, LoadedLabware, @@ -174,7 +174,7 @@ const protocolWithMultipleTemps = ({ }, ] as LoadedModule[], } as unknown) as ProtocolAnalysisOutput -const standardDeckDef = ot2DeckDefV4 as any +const standardDeckDef = ot2DeckDefV5 as any describe('getProtocolModulesInfo', () => { it('should gather protocol module info for temp, mag, and tc', () => { diff --git a/app/src/organisms/Devices/hooks/__tests__/useModuleRenderInfoForProtocolById.test.tsx b/app/src/organisms/Devices/hooks/__tests__/useModuleRenderInfoForProtocolById.test.tsx index 11b744f57a2..540b1532799 100644 --- a/app/src/organisms/Devices/hooks/__tests__/useModuleRenderInfoForProtocolById.test.tsx +++ b/app/src/organisms/Devices/hooks/__tests__/useModuleRenderInfoForProtocolById.test.tsx @@ -1,10 +1,11 @@ import { renderHook } from '@testing-library/react' import { vi, it, expect, describe, beforeEach } from 'vitest' import { when } from 'vitest-when' -import { UseQueryResult } from 'react-query' import { - STAGING_AREA_RIGHT_SLOT_FIXTURE, + TEMPERATURE_MODULE_TYPE, + TEMPERATURE_MODULE_V2, + TEMPERATURE_MODULE_V2_FIXTURE, heater_shaker_commands_with_results_key, } from '@opentrons/shared-data' import { useMostRecentCompletedAnalysis } from '../../../LabwarePositionCheck/useMostRecentCompletedAnalysis' @@ -13,7 +14,6 @@ import { useDeckConfigurationQuery } from '@opentrons/react-api-client' import { getProtocolModulesInfo } from '../../ProtocolRun/utils/getProtocolModulesInfo' import { - mockMagneticModuleGen2, mockTemperatureModuleGen2, mockThermocycler, } from '../../../../redux/modules/__fixtures__' @@ -30,6 +30,8 @@ import type { ModuleType, ProtocolAnalysisOutput, } from '@opentrons/shared-data' +import type { UseQueryResult } from 'react-query' +import type { AttachedModule } from '../../../../redux/modules/types' vi.mock('@opentrons/react-api-client') vi.mock('../../ProtocolRun/utils/getProtocolModulesInfo') @@ -53,25 +55,28 @@ const PROTOCOL_DETAILS = { protocolKey: 'fakeProtocolKey', } -const mockMagneticModuleDefinition = { - moduleId: 'someMagneticModule', - model: 'magneticModuleV2' as ModuleModel, - type: 'magneticModuleType' as ModuleType, - labwareOffset: { x: 5, y: 5, z: 5 }, - cornerOffsetFromSlot: { x: 1, y: 1, z: 1 }, - dimensions: { - xDimension: 100, - yDimension: 100, - footprintXDimension: 50, - footprintYDimension: 50, - labwareInterfaceXDimension: 80, - labwareInterfaceYDimension: 120, +const mockAttachedTempMod: AttachedModule = { + id: 'temp_mod_1', + moduleModel: TEMPERATURE_MODULE_V2, + moduleType: TEMPERATURE_MODULE_TYPE, + serialNumber: 'abc123', + hardwareRevision: 'heatershaker_v4.0', + firmwareVersion: 'v2.0.0', + hasAvailableUpdate: true, + data: { + currentTemperature: 40, + targetTemperature: null, + status: 'idle', + }, + usbPort: { + path: '/dev/ot_module_heatershaker0', + port: 1, + portGroup: 'unknown', + hub: false, }, - twoDimensionalRendering: { children: [] }, } const mockTemperatureModuleDefinition = { - moduleId: 'someMagneticModule', model: 'temperatureModuleV2' as ModuleModel, type: 'temperatureModuleType' as ModuleType, labwareOffset: { x: 5, y: 5, z: 5 }, @@ -87,19 +92,6 @@ const mockTemperatureModuleDefinition = { twoDimensionalRendering: { children: [] }, } -const MAGNETIC_MODULE_INFO = { - moduleId: 'magneticModuleId', - x: 0, - y: 0, - z: 0, - moduleDef: mockMagneticModuleDefinition as any, - nestedLabwareDef: null, - nestedLabwareId: null, - nestedLabwareDisplayName: null, - protocolLoadOrder: 0, - slotName: 'D1', -} - const TEMPERATURE_MODULE_INFO = { moduleId: 'temperatureModuleId', x: 0, @@ -111,11 +103,12 @@ const TEMPERATURE_MODULE_INFO = { nestedLabwareDisplayName: null, protocolLoadOrder: 0, slotName: 'D1', -} +} as any const mockCutoutConfig: CutoutConfig = { cutoutId: 'cutoutD1', - cutoutFixtureId: STAGING_AREA_RIGHT_SLOT_FIXTURE, + cutoutFixtureId: TEMPERATURE_MODULE_V2_FIXTURE, + opentronsModuleSerialNumber: 'abc123', } describe('useModuleRenderInfoForProtocolById hook', () => { @@ -123,8 +116,8 @@ describe('useModuleRenderInfoForProtocolById hook', () => { vi.mocked(useDeckConfigurationQuery).mockReturnValue({ data: [mockCutoutConfig], } as UseQueryResult) + vi.mocked(useAttachedModules).mockReturnValue([mockAttachedTempMod]) vi.mocked(useAttachedModules).mockReturnValue([ - mockMagneticModuleGen2, mockTemperatureModuleGen2, mockThermocycler, ]) @@ -134,10 +127,7 @@ describe('useModuleRenderInfoForProtocolById hook', () => { when(vi.mocked(useMostRecentCompletedAnalysis)) .calledWith('1') .thenReturn(PROTOCOL_DETAILS.protocolData as any) - vi.mocked(getProtocolModulesInfo).mockReturnValue([ - TEMPERATURE_MODULE_INFO, - MAGNETIC_MODULE_INFO, - ]) + vi.mocked(getProtocolModulesInfo).mockReturnValue([TEMPERATURE_MODULE_INFO]) }) it('should return no module render info when protocol details not found', () => { @@ -155,13 +145,8 @@ describe('useModuleRenderInfoForProtocolById hook', () => { useModuleRenderInfoForProtocolById('1', true) ) expect(result.current).toStrictEqual({ - magneticModuleId: { - conflictedFixture: mockCutoutConfig, - attachedModuleMatch: mockMagneticModuleGen2, - ...MAGNETIC_MODULE_INFO, - }, temperatureModuleId: { - conflictedFixture: mockCutoutConfig, + conflictedFixture: null, attachedModuleMatch: mockTemperatureModuleGen2, ...TEMPERATURE_MODULE_INFO, }, diff --git a/app/src/organisms/Devices/hooks/useModuleRenderInfoForProtocolById.ts b/app/src/organisms/Devices/hooks/useModuleRenderInfoForProtocolById.ts index 6ca57b24c4a..e606e846d53 100644 --- a/app/src/organisms/Devices/hooks/useModuleRenderInfoForProtocolById.ts +++ b/app/src/organisms/Devices/hooks/useModuleRenderInfoForProtocolById.ts @@ -1,12 +1,10 @@ import { checkModuleCompatibility, FLEX_ROBOT_TYPE, - getCutoutIdForSlotName, + getCutoutFixturesForModuleModel, + getCutoutIdsFromModuleSlotName, getDeckDefFromRobotType, - MAGNETIC_BLOCK_TYPE, - SINGLE_SLOT_FIXTURES, - STAGING_AREA_RIGHT_SLOT_FIXTURE, - THERMOCYCLER_MODULE_TYPE, + OT2_ROBOT_TYPE, } from '@opentrons/shared-data' import { useDeckConfigurationQuery } from '@opentrons/react-api-client' @@ -35,7 +33,7 @@ export function useModuleRenderInfoForProtocolById( pollModules?: boolean ): ModuleRenderInfoById { const robotProtocolAnalysis = useMostRecentCompletedAnalysis(runId) - const { data: deckConfig } = useDeckConfigurationQuery({ + const { data: deckConfig = [] } = useDeckConfigurationQuery({ refetchInterval: REFETCH_INTERVAL_5000_MS, }) const storedProtocolAnalysis = useStoredProtocolAnalysis(runId) @@ -45,50 +43,57 @@ export function useModuleRenderInfoForProtocolById( }) if (protocolAnalysis == null) return {} - const deckDef = getDeckDefFromRobotType( - protocolAnalysis.robotType ?? FLEX_ROBOT_TYPE - ) + const assumedRobotType = protocolAnalysis.robotType ?? FLEX_ROBOT_TYPE + const deckDef = getDeckDefFromRobotType(assumedRobotType) const protocolModulesInfo = getProtocolModulesInfo(protocolAnalysis, deckDef) const protocolModulesInfoInLoadOrder = protocolModulesInfo.sort( (modA, modB) => modA.protocolLoadOrder - modB.protocolLoadOrder ) + + const robotSupportsModuleConfig = assumedRobotType !== OT2_ROBOT_TYPE let matchedAmod: AttachedModule[] = [] const allModuleRenderInfo = protocolModulesInfoInLoadOrder.map( protocolMod => { + const moduleFixtures = getCutoutFixturesForModuleModel( + protocolMod.moduleDef.model, + deckDef + ) + const moduleCutoutIds = getCutoutIdsFromModuleSlotName( + protocolMod.slotName, + moduleFixtures, + deckDef + ) const compatibleAttachedModule = attachedModules.find( attachedMod => + // first check module model compatibility checkModuleCompatibility( attachedMod.moduleModel, protocolMod.moduleDef.model - ) && !matchedAmod.find(m => m === attachedMod) + ) && + // then check that the module hasn't already been matched + !matchedAmod.some( + m => m.serialNumber === attachedMod.serialNumber + ) && + // then if robotType supports configurable modules check the deck config has a + // a module with the expected serial number in the expected location + (!robotSupportsModuleConfig || + deckConfig.some( + ({ cutoutId, opentronsModuleSerialNumber }) => + attachedMod.serialNumber === opentronsModuleSerialNumber && + moduleCutoutIds.includes(cutoutId) + )) ) ?? null - const cutoutIdForSlotName = getCutoutIdForSlotName( - protocolMod.slotName, - deckDef - ) - - const isMagneticBlockModule = - protocolMod.moduleDef.moduleType === MAGNETIC_BLOCK_TYPE - - const isThermocycler = - protocolMod.moduleDef.moduleType === THERMOCYCLER_MODULE_TYPE - const conflictedFixture = deckConfig?.find( - fixture => - (fixture.cutoutId === cutoutIdForSlotName || - // special-case A1 for the thermocycler to require a single slot fixture - (isThermocycler && fixture.cutoutId === 'cutoutA1')) && - fixture.cutoutFixtureId != null && - // do not generate a conflict for single slot fixtures, because modules are not yet fixtures - !SINGLE_SLOT_FIXTURES.includes(fixture.cutoutFixtureId) && - // special case the magnetic module because unlike other modules it sits in a slot that can also be provided by a staging area fixture - (!isMagneticBlockModule || - fixture.cutoutFixtureId !== STAGING_AREA_RIGHT_SLOT_FIXTURE) + ({ cutoutId, cutoutFixtureId }) => + moduleCutoutIds.includes(cutoutId) && + !moduleFixtures.some(({ id }) => cutoutFixtureId === id) && + // if robotType supports module config, don't treat module fixture as conflict + (!robotSupportsModuleConfig || compatibleAttachedModule == null) ) ?? null if (compatibleAttachedModule !== null) { diff --git a/app/src/organisms/InterventionModal/__tests__/utils.test.ts b/app/src/organisms/InterventionModal/__tests__/utils.test.ts index b14f510a29f..eca5086cd9d 100644 --- a/app/src/organisms/InterventionModal/__tests__/utils.test.ts +++ b/app/src/organisms/InterventionModal/__tests__/utils.test.ts @@ -2,7 +2,7 @@ import deepClone from 'lodash/cloneDeep' import { describe, it, expect, vi, beforeEach } from 'vitest' import { getSlotHasMatingSurfaceUnitVector, - ot2DeckDefV4, + ot2DeckDefV5, } from '@opentrons/shared-data' import { @@ -137,7 +137,7 @@ describe('getRunLabwareRenderInfo', () => { const res = getRunLabwareRenderInfo( mockRunData, mockLabwareDefinitionsByUri, - ot2DeckDefV4 as any + ot2DeckDefV5 as any ) const labwareInfo = res[0] expect(labwareInfo).toBeTruthy() @@ -154,7 +154,7 @@ describe('getRunLabwareRenderInfo', () => { const res = getRunLabwareRenderInfo( mockRunData, mockLabwareDefinitionsByUri, - ot2DeckDefV4 as any + ot2DeckDefV5 as any ) expect(res).toHaveLength(1) // the offdeck labware still gets added because the mating surface doesn't exist for offdeck labware }) @@ -163,7 +163,7 @@ describe('getRunLabwareRenderInfo', () => { const res = getRunLabwareRenderInfo( mockRunData, mockLabwareDefinitionsByUri, - ot2DeckDefV4 as any + ot2DeckDefV5 as any ) expect(res).toHaveLength(2) const labwareInfo = res.find( @@ -172,7 +172,7 @@ describe('getRunLabwareRenderInfo', () => { expect(labwareInfo).toBeTruthy() expect(labwareInfo?.x).toEqual(0) expect(labwareInfo?.y).toEqual( - ot2DeckDefV4.cornerOffsetFromOrigin[1] - + ot2DeckDefV5.cornerOffsetFromOrigin[1] - mockLabwareDefinition.dimensions.yDimension ) }) @@ -189,7 +189,7 @@ describe('getRunLabwareRenderInfo', () => { const res = getRunLabwareRenderInfo( { labware: [mockBadSlotLabware] } as any, mockLabwareDefinitionsByUri, - ot2DeckDefV4 as any + ot2DeckDefV5 as any ) expect(res[0].x).toEqual(0) @@ -207,7 +207,7 @@ describe('getCurrentRunModuleRenderInfo', () => { it('returns run module render info with nested labware', () => { const res = getRunModuleRenderInfo( mockRunData, - ot2DeckDefV4 as any, + ot2DeckDefV5 as any, mockLabwareDefinitionsByUri ) const moduleInfo = res[0] @@ -228,7 +228,7 @@ describe('getCurrentRunModuleRenderInfo', () => { const res = getRunModuleRenderInfo( mockRunDataNoNesting, - ot2DeckDefV4 as any, + ot2DeckDefV5 as any, mockLabwareDefinitionsByUri ) @@ -245,7 +245,7 @@ describe('getCurrentRunModuleRenderInfo', () => { const res = getRunModuleRenderInfo( mockRunDataWithTC, - ot2DeckDefV4 as any, + ot2DeckDefV5 as any, mockLabwareDefinitionsByUri ) @@ -270,7 +270,7 @@ describe('getCurrentRunModuleRenderInfo', () => { const res = getRunModuleRenderInfo( mockRunDataWithBadModuleSlot, - ot2DeckDefV4 as any, + ot2DeckDefV5 as any, mockLabwareDefinitionsByUri ) diff --git a/app/src/organisms/ModuleWizardFlows/AttachProbe.tsx b/app/src/organisms/ModuleWizardFlows/AttachProbe.tsx index a4a324dc933..4a8f70656e4 100644 --- a/app/src/organisms/ModuleWizardFlows/AttachProbe.tsx +++ b/app/src/organisms/ModuleWizardFlows/AttachProbe.tsx @@ -1,12 +1,6 @@ import * as React from 'react' import { css } from 'styled-components' -import attachProbe1 from '../../assets/videos/pipette-wizard-flows/Pipette_Attach_Probe_1.webm' -import attachProbe8 from '../../assets/videos/pipette-wizard-flows/Pipette_Attach_Probe_8.webm' -import attachProbe96 from '../../assets/videos/pipette-wizard-flows/Pipette_Attach_Probe_96.webm' import { Trans, useTranslation } from 'react-i18next' -import { useDeckConfigurationQuery } from '@opentrons/react-api-client' -import { WASTE_CHUTE_CUTOUT, CreateCommand, LEFT } from '@opentrons/shared-data' -import { InProgressModal } from '../../molecules/InProgressModal/InProgressModal' import { Flex, RESPONSIVENESS, @@ -14,13 +8,26 @@ import { StyledText, TYPOGRAPHY, } from '@opentrons/components' +import { LEFT, WASTE_CHUTE_FIXTURES } from '@opentrons/shared-data' +import attachProbe1 from '../../assets/videos/pipette-wizard-flows/Pipette_Attach_Probe_1.webm' +import attachProbe8 from '../../assets/videos/pipette-wizard-flows/Pipette_Attach_Probe_8.webm' +import attachProbe96 from '../../assets/videos/pipette-wizard-flows/Pipette_Attach_Probe_96.webm' +import { InProgressModal } from '../../molecules/InProgressModal/InProgressModal' +import type { + CreateCommand, + DeckConfiguration, + CutoutId, + CutoutFixtureId, +} from '@opentrons/shared-data' import { Banner } from '../../atoms/Banner' import { GenericWizardTile } from '../../molecules/GenericWizardTile' import type { ModuleCalibrationWizardStepProps } from './types' interface AttachProbeProps extends ModuleCalibrationWizardStepProps { adapterId: string | null + deckConfig: DeckConfiguration + fixtureIdByCutoutId: { [cutoutId in CutoutId]?: CutoutFixtureId } } const BODY_STYLE = css` @@ -42,7 +49,8 @@ export const AttachProbe = (props: AttachProbeProps): JSX.Element | null => { attachedModule, attachedPipette, isOnDevice, - slotName, + deckConfig, + fixtureIdByCutoutId, } = props const { t, i18n } = useTranslation([ 'module_wizard_flows', @@ -65,12 +73,11 @@ export const AttachProbe = (props: AttachProbeProps): JSX.Element | null => { probeLocation = t('pipette_wizard_flows:ninety_six_probe_location') break } - const wasteChuteConflict = - slotName === 'C3' && attachedPipette.data.channels === 96 - const deckConfig = useDeckConfigurationQuery().data - const isWasteChuteOnDeck = - deckConfig?.find(fixture => fixture.cutoutId === WASTE_CHUTE_CUTOUT) ?? - false + const wasteChuteConflictWith96Channel = + 'cutoutC3' in fixtureIdByCutoutId && attachedPipette.data.channels === 96 + const isWasteChuteOnDeck = deckConfig.some(cc => + WASTE_CHUTE_FIXTURES.includes(cc.cutoutFixtureId) + ) const pipetteAttachProbeVid = ( @@ -101,7 +108,7 @@ export const AttachProbe = (props: AttachProbeProps): JSX.Element | null => { /> - {wasteChuteConflict && ( + {wasteChuteConflictWith96Channel && ( void } @@ -50,8 +51,8 @@ export const PlaceAdapter = (props: PlaceAdapterProps): JSX.Element | null => { const { proceed, goBack, + deckConfig, attachedModule, - slotName, chainRunCommands, setErrorMessage, setCreatedAdapterId, @@ -60,6 +61,11 @@ export const PlaceAdapter = (props: PlaceAdapterProps): JSX.Element | null => { } = props const { t } = useTranslation('module_wizard_flows') const mount = attachedPipette.mount + const cutoutId = deckConfig.find( + cc => cc.opentronsModuleSerialNumber === attachedModule.serialNumber + )?.cutoutId + const slotName = + cutoutId != null ? FLEX_SINGLE_SLOT_BY_CUTOUT_ID[cutoutId] : null const handleOnClick = (): void => { const calibrationAdapterLoadName = getCalibrationAdapterLoadName( attachedModule.moduleModel @@ -70,15 +76,19 @@ export const PlaceAdapter = (props: PlaceAdapterProps): JSX.Element | null => { ) return } + if (slotName == null) { + console.error( + `could not load module ${attachedModule.moduleModel} into location ${slotName}` + ) + return + } const calibrationAdapterId = uuidv4() const commands: CreateCommand[] = [ { commandType: 'loadModule', params: { - location: { - slotName: slotName, - }, + location: { slotName }, model: attachedModule.moduleModel, moduleId: attachedModule.id, }, @@ -97,15 +107,21 @@ export const PlaceAdapter = (props: PlaceAdapterProps): JSX.Element | null => { { commandType: 'calibration/moveToMaintenancePosition', params: { - mount: mount, + mount, maintenancePosition: 'attachInstrument', }, }, ] chainRunCommands?.(commands, false) - .then(() => setCreatedAdapterId(calibrationAdapterId)) - .then(() => proceed()) - .catch((e: Error) => setErrorMessage(e.message)) + .then(() => { + setCreatedAdapterId(calibrationAdapterId) + }) + .then(() => { + proceed() + }) + .catch((e: Error) => { + setErrorMessage(e.message) + }) } const moduleType = attachedModule.moduleType diff --git a/app/src/organisms/ModuleWizardFlows/SelectLocation.tsx b/app/src/organisms/ModuleWizardFlows/SelectLocation.tsx index 6a694959079..38a44b96219 100644 --- a/app/src/organisms/ModuleWizardFlows/SelectLocation.tsx +++ b/app/src/organisms/ModuleWizardFlows/SelectLocation.tsx @@ -1,12 +1,19 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' import { css } from 'styled-components' +import { useUpdateDeckConfigurationMutation } from '@opentrons/react-api-client' import { - FLEX_ROBOT_TYPE, - getDeckDefFromRobotType, getModuleDisplayName, THERMOCYCLER_MODULE_TYPE, - CutoutConfig, + getDeckDefFromRobotType, + FLEX_ROBOT_TYPE, + getCutoutIdsFromModuleSlotName, + getCutoutFixturesForModuleModel, + SINGLE_CENTER_SLOT_FIXTURE, + SINGLE_CENTER_CUTOUTS, + SINGLE_LEFT_SLOT_FIXTURE, + SINGLE_RIGHT_CUTOUTS, + SINGLE_RIGHT_SLOT_FIXTURE, } from '@opentrons/shared-data' import { DeckLocationSelect, @@ -19,6 +26,13 @@ import { import { Banner } from '../../atoms/Banner' import { GenericWizardTile } from '../../molecules/GenericWizardTile' import type { ModuleCalibrationWizardStepProps } from './types' +import type { + CutoutConfig, + DeckConfiguration, + CutoutFixtureId, + CutoutId, + ModuleLocation, +} from '@opentrons/shared-data' export const BODY_STYLE = css` ${TYPOGRAPHY.pRegular}; @@ -29,9 +43,10 @@ export const BODY_STYLE = css` } ` interface SelectLocationProps extends ModuleCalibrationWizardStepProps { - setSlotName: React.Dispatch> availableSlotNames: string[] occupiedCutouts: CutoutConfig[] + deckConfig: DeckConfiguration + fixtureIdByCutoutId: { [cutoutId in CutoutId]?: CutoutFixtureId } } export const SelectLocation = ( props: SelectLocationProps @@ -39,17 +54,21 @@ export const SelectLocation = ( const { proceed, attachedModule, - slotName, - setSlotName, + deckConfig, availableSlotNames, occupiedCutouts, + fixtureIdByCutoutId, } = props const { t } = useTranslation('module_wizard_flows') const moduleName = getModuleDisplayName(attachedModule.moduleModel) const handleOnClick = (): void => { proceed() } + const { updateDeckConfiguration } = useUpdateDeckConfigurationMutation() const deckDef = getDeckDefFromRobotType(FLEX_ROBOT_TYPE) + const cutoutConfig = deckConfig.find( + cc => cc.opentronsModuleSerialNumber === attachedModule.serialNumber + ) const bodyText = ( <> @@ -61,14 +80,61 @@ export const SelectLocation = ( ) + const handleSetLocation = (loc: ModuleLocation): void => { + const moduleFixtures = getCutoutFixturesForModuleModel( + attachedModule.moduleModel, + deckDef + ) + const selectedCutoutIds = getCutoutIdsFromModuleSlotName( + loc.slotName, + moduleFixtures, + deckDef + ) + if ( + selectedCutoutIds.every( + selectedCutoutId => !(selectedCutoutId in fixtureIdByCutoutId) + ) + ) { + updateDeckConfiguration( + deckConfig.map(cc => { + if (cc.cutoutId in fixtureIdByCutoutId) { + let replacementFixtureId: CutoutFixtureId = SINGLE_LEFT_SLOT_FIXTURE + if (SINGLE_CENTER_CUTOUTS.includes(cc.cutoutId)) { + replacementFixtureId = SINGLE_CENTER_SLOT_FIXTURE + } else if (SINGLE_RIGHT_CUTOUTS.includes(cc.cutoutId)) { + replacementFixtureId = SINGLE_RIGHT_SLOT_FIXTURE + } + return { + ...cc, + cutoutFixtureId: replacementFixtureId, + opentronsModuleSerialNumber: undefined, + } + } else if (selectedCutoutIds.includes(cc.cutoutId)) { + return { + ...cc, + cutoutFixtureId: + Object.values(fixtureIdByCutoutId)[0] ?? + moduleFixtures[0]?.id ?? + cc.cutoutFixtureId, + opentronsModuleSerialNumber: attachedModule.serialNumber, + } + } else { + return cc + } + }) + ) + } + } return ( setSlotName(loc.slotName)} + selectedLocation={{ + slotName: cutoutConfig?.cutoutId.replace('cutout', '') ?? '', + }} + setSelectedLocation={handleSetLocation} availableSlotNames={availableSlotNames} occupiedCutouts={occupiedCutouts} isThermocycler={ @@ -80,9 +146,9 @@ export const SelectLocation = ( bodyText={bodyText} proceedButtonText={t('confirm_location')} proceed={handleOnClick} - proceedIsDisabled={slotName == null} + proceedIsDisabled={cutoutConfig == null} disableProceedReason={ - slotName == null + cutoutConfig == null ? 'Current deck configuration prevents module placement' : undefined } diff --git a/app/src/organisms/ModuleWizardFlows/index.tsx b/app/src/organisms/ModuleWizardFlows/index.tsx index be36d681950..f3196b54fbc 100644 --- a/app/src/organisms/ModuleWizardFlows/index.tsx +++ b/app/src/organisms/ModuleWizardFlows/index.tsx @@ -13,6 +13,10 @@ import { getModuleDisplayName, FLEX_CUTOUT_BY_SLOT_ID, SINGLE_SLOT_FIXTURES, + getFixtureIdByCutoutIdFromModuleSlotName, + getCutoutFixturesForModuleModel, + getDeckDefFromRobotType, + FLEX_ROBOT_TYPE, } from '@opentrons/shared-data' import { LegacyModalShell } from '../../molecules/LegacyModal' import { getTopPortalEl } from '../../App/portal' @@ -45,7 +49,6 @@ interface ModuleWizardFlowsProps { attachedModule: AttachedModule closeFlow: () => void isPrepCommandLoading: boolean - initialSlotName?: string onComplete?: () => void prepCommandErrorMessage?: string } @@ -57,7 +60,6 @@ export const ModuleWizardFlows = ( ): JSX.Element | null => { const { attachedModule, - initialSlotName, isPrepCommandLoading, closeFlow, onComplete, @@ -72,12 +74,25 @@ export const ModuleWizardFlows = ( : attachedPipettes.right const moduleCalibrationSteps = getModuleCalibrationSteps() + const deckDef = getDeckDefFromRobotType(FLEX_ROBOT_TYPE) const deckConfig = useDeckConfigurationQuery().data ?? [] + const moduleCutoutConfig = deckConfig.find( + cc => cc.opentronsModuleSerialNumber === attachedModule.serialNumber + ) + // mapping of cutoutId's occupied by the target module and their cutoutFixtureId's per cutout + const fixtureIdByCutoutId = + moduleCutoutConfig != null + ? getFixtureIdByCutoutIdFromModuleSlotName( + moduleCutoutConfig.cutoutId.replace('cutout', ''), + getCutoutFixturesForModuleModel(attachedModule.moduleModel, deckDef), + deckDef + ) + : {} const occupiedCutouts = deckConfig.filter( - (fixture: CutoutConfig) => + (cutoutConfig: CutoutConfig) => !SINGLE_SLOT_FIXTURES.includes( - fixture.cutoutFixtureId as SingleSlotCutoutFixtureId - ) + cutoutConfig.cutoutFixtureId as SingleSlotCutoutFixtureId + ) && !Object.keys(fixtureIdByCutoutId).includes(cutoutConfig.cutoutId) ) const availableSlotNames = FLEX_SLOT_NAMES_BY_MOD_TYPE[ @@ -90,9 +105,6 @@ export const ModuleWizardFlows = ( ) ) ?? [] - const [slotName, setSlotName] = React.useState( - initialSlotName != null ? initialSlotName : availableSlotNames?.[0] ?? null - ) const [currentStepIndex, setCurrentStepIndex] = React.useState(0) const totalStepCount = moduleCalibrationSteps.length - 1 const currentStep = moduleCalibrationSteps?.[currentStepIndex] @@ -247,7 +259,6 @@ export const ModuleWizardFlows = ( errorMessage, isOnDevice, attachedModule, - slotName, isExiting, } @@ -304,8 +315,9 @@ export const ModuleWizardFlows = ( {...currentStep} {...calibrateBaseProps} availableSlotNames={availableSlotNames} - setSlotName={setSlotName} + deckConfig={deckConfig} occupiedCutouts={occupiedCutouts} + fixtureIdByCutoutId={fixtureIdByCutoutId} /> ) } else if (currentStep.section === SECTIONS.PLACE_ADAPTER) { @@ -313,6 +325,7 @@ export const ModuleWizardFlows = ( ) @@ -321,7 +334,9 @@ export const ModuleWizardFlows = ( ) } else if (currentStep.section === SECTIONS.DETACH_PROBE) { diff --git a/app/src/organisms/ModuleWizardFlows/types.ts b/app/src/organisms/ModuleWizardFlows/types.ts index f2b2764b12c..df6020e9b36 100644 --- a/app/src/organisms/ModuleWizardFlows/types.ts +++ b/app/src/organisms/ModuleWizardFlows/types.ts @@ -24,7 +24,6 @@ export interface ModuleCalibrationWizardStepProps { attachedPipette: PipetteInformation errorMessage: string | null setErrorMessage: (message: string | null) => void - slotName: string isOnDevice: boolean | null } diff --git a/app/src/organisms/ProtocolSetupDeckConfiguration/__tests__/ProtocolSetupDeckConfiguration.test.tsx b/app/src/organisms/ProtocolSetupDeckConfiguration/__tests__/ProtocolSetupDeckConfiguration.test.tsx index ee60d409fab..892df022b5f 100644 --- a/app/src/organisms/ProtocolSetupDeckConfiguration/__tests__/ProtocolSetupDeckConfiguration.test.tsx +++ b/app/src/organisms/ProtocolSetupDeckConfiguration/__tests__/ProtocolSetupDeckConfiguration.test.tsx @@ -6,6 +6,7 @@ import { describe, it, vi, beforeEach, expect, afterEach } from 'vitest' import { BaseDeck } from '@opentrons/components' import { useDeckConfigurationQuery, + useModulesQuery, useUpdateDeckConfigurationMutation, } from '@opentrons/react-api-client' @@ -19,6 +20,7 @@ import type { CompletedProtocolAnalysis, DeckConfiguration, } from '@opentrons/shared-data' +import { Modules } from '@opentrons/api-client' vi.mock('@opentrons/components/src/hardware-sim/BaseDeck/index') vi.mock('@opentrons/react-api-client') @@ -72,6 +74,9 @@ describe('ProtocolSetupDeckConfiguration', () => { vi.mocked(useDeckConfigurationQuery).mockReturnValue(({ data: [], } as unknown) as UseQueryResult) + vi.mocked(useModulesQuery).mockReturnValue(({ + data: { data: [] }, + } as unknown) as UseQueryResult) }) afterEach(() => { diff --git a/app/src/organisms/ProtocolSetupLabware/__tests__/ProtocolSetupLabware.test.tsx b/app/src/organisms/ProtocolSetupLabware/__tests__/ProtocolSetupLabware.test.tsx index 57d7981c138..b44a314983a 100644 --- a/app/src/organisms/ProtocolSetupLabware/__tests__/ProtocolSetupLabware.test.tsx +++ b/app/src/organisms/ProtocolSetupLabware/__tests__/ProtocolSetupLabware.test.tsx @@ -8,7 +8,7 @@ import { useCreateLiveCommandMutation, useModulesQuery, } from '@opentrons/react-api-client' -import { ot3StandardDeckV4 as ot3StandardDeckDef } from '@opentrons/shared-data' +import { ot3StandardDeckV5 as ot3StandardDeckDef } from '@opentrons/shared-data' import { renderWithProviders } from '../../../__testing-utils__' import { i18n } from '../../../i18n' diff --git a/app/src/organisms/ProtocolSetupLabware/index.tsx b/app/src/organisms/ProtocolSetupLabware/index.tsx index bdb68944a67..3bc1ad62c56 100644 --- a/app/src/organisms/ProtocolSetupLabware/index.tsx +++ b/app/src/organisms/ProtocolSetupLabware/index.tsx @@ -108,6 +108,7 @@ export function ProtocolSetupLabware({ mostRecentAnalysis != null ? getProtocolModulesInfo(mostRecentAnalysis, deckDef) : [] + const attachedProtocolModuleMatches = getAttachedProtocolModuleMatches( attachedModules, protocolModulesInfo diff --git a/app/src/organisms/ProtocolSetupModulesAndDeck/FixtureTable.tsx b/app/src/organisms/ProtocolSetupModulesAndDeck/FixtureTable.tsx index e2dbb107379..23d490af287 100644 --- a/app/src/organisms/ProtocolSetupModulesAndDeck/FixtureTable.tsx +++ b/app/src/organisms/ProtocolSetupModulesAndDeck/FixtureTable.tsx @@ -16,6 +16,7 @@ import { } from '@opentrons/components' import { getCutoutDisplayName, + getDeckDefFromRobotType, getFixtureDisplayName, getSimplestDeckConfigForProtocol, SINGLE_SLOT_FIXTURES, @@ -30,6 +31,7 @@ import type { CompletedProtocolAnalysis, CutoutFixtureId, CutoutId, + DeckDefinition, RobotType, } from '@opentrons/shared-data' import type { SetupScreens } from '../../pages/ProtocolSetup' @@ -59,6 +61,7 @@ export function FixtureTable({ robotType, mostRecentAnalysis ) + const deckDef = getDeckDefFromRobotType(robotType) const requiredDeckConfigCompatibility = getRequiredDeckConfig( deckConfigCompatibility @@ -96,6 +99,7 @@ export function FixtureTable({ setSetupScreen={setSetupScreen} setCutoutId={setCutoutId} setProvidedFixtureOptions={setProvidedFixtureOptions} + deckDef={deckDef} /> ) })} @@ -108,6 +112,7 @@ interface FixtureTableItemProps extends CutoutConfigAndCompatibility { setSetupScreen: React.Dispatch> setCutoutId: (cutoutId: CutoutId) => void setProvidedFixtureOptions: (providedFixtureOptions: CutoutFixtureId[]) => void + deckDef: DeckDefinition } function FixtureTableItem({ @@ -119,6 +124,7 @@ function FixtureTableItem({ setSetupScreen, setCutoutId, setProvidedFixtureOptions, + deckDef, }: FixtureTableItemProps): JSX.Element { const { t, i18n } = useTranslation('protocol_setup') @@ -183,6 +189,7 @@ function FixtureTableItem({ requiredFixtureId={compatibleCutoutFixtureIds[0]} isOnDevice={true} missingLabwareDisplayName={missingLabwareDisplayName} + deckDef={deckDef} /> ) : null} > } export function ModuleTable(props: ModuleTableProps): JSX.Element { - const { - attachedProtocolModuleMatches, - deckDef, - protocolModulesInfo, - runId, - setShowMultipleModulesModal, - } = props + const { attachedProtocolModuleMatches, deckDef, runId } = props const { t } = useTranslation('protocol_setup') @@ -95,16 +85,6 @@ export function ModuleTable(props: ModuleTableProps): JSX.Element { {t('status')} {attachedProtocolModuleMatches.map(module => { - // check for duplicate module model in list of modules for protocol - const isDuplicateModuleModel = protocolModulesInfo - // filter out current module - .filter(otherModule => otherModule.moduleId !== module.moduleId) - // check for existence of another module of same model - .some( - otherModule => - otherModule.moduleDef.model === module.moduleDef.model - ) - const cutoutIdForSlotName = getCutoutIdForSlotName( module.slotName, deckDef @@ -134,14 +114,13 @@ export function ModuleTable(props: ModuleTableProps): JSX.Element { ) })} @@ -156,24 +135,22 @@ interface ModuleTableItemProps { continuePastCommandFailure: boolean ) => Promise conflictedFixture: CutoutConfig | null - isDuplicateModuleModel: boolean isLoading: boolean module: AttachedProtocolModuleMatch prepCommandErrorMessage: string setPrepCommandErrorMessage: React.Dispatch> - setShowMultipleModulesModal: React.Dispatch> + deckDef: DeckDefinition } function ModuleTableItem({ - isDuplicateModuleModel, module, - setShowMultipleModulesModal, calibrationStatus, chainLiveCommands, isLoading, prepCommandErrorMessage, setPrepCommandErrorMessage, conflictedFixture, + deckDef, }: ModuleTableItemProps): JSX.Element { const { i18n, t } = useTranslation(['protocol_setup', 'module_wizard_flows']) @@ -216,7 +193,6 @@ function ModuleTableItem({ background={false} iconName="connection-status" /> - {isDuplicateModuleModel ? : null} ) if (conflictedFixture != null) { @@ -231,7 +207,9 @@ function ModuleTableItem({ setShowLocationConflictModal(true)} + onClick={() => { + setShowLocationConflictModal(true) + }} /> ) @@ -246,17 +224,12 @@ function ModuleTableItem({ module.attachedModuleMatch?.moduleOffset?.last_modified != null ) { moduleStatus = ( - <> - - {isDuplicateModuleModel ? ( - - ) : null} - + ) } else if ( isModuleReady && @@ -285,8 +258,9 @@ function ModuleTableItem({ {showModuleWizard && module.attachedModuleMatch != null ? ( setShowModuleWizard(false)} - initialSlotName={module.slotName} + closeFlow={() => { + setShowModuleWizard(false) + }} isPrepCommandLoading={isLoading} prepCommandErrorMessage={ prepCommandErrorMessage === '' ? undefined : prepCommandErrorMessage @@ -295,9 +269,12 @@ function ModuleTableItem({ ) : null} {showLocationConflictModal && conflictedFixture != null ? ( setShowLocationConflictModal(false)} + onCloseClick={() => { + setShowLocationConflictModal(false) + }} cutoutId={conflictedFixture.cutoutId} requiredModule={module.moduleDef.model} + deckDef={deckDef} isOnDevice={true} /> ) : null} @@ -313,12 +290,9 @@ function ModuleTableItem({ : COLORS.yellow35 } borderRadius={BORDERS.borderRadius8} - cursor={isDuplicateModuleModel ? 'pointer' : 'inherit'} + cursor="inherit" gridGap={SPACING.spacing24} padding={`${SPACING.spacing16} ${SPACING.spacing24}`} - onClick={() => - isDuplicateModuleModel ? setShowMultipleModulesModal(true) : null - } > diff --git a/app/src/organisms/ProtocolSetupModulesAndDeck/index.tsx b/app/src/organisms/ProtocolSetupModulesAndDeck/index.tsx index 6b72d6bf6ba..3a03a91c9c6 100644 --- a/app/src/organisms/ProtocolSetupModulesAndDeck/index.tsx +++ b/app/src/organisms/ProtocolSetupModulesAndDeck/index.tsx @@ -13,7 +13,6 @@ import { FloatingActionButton } from '../../atoms/buttons' import { InlineNotification } from '../../atoms/InlineNotification' import { ChildNavigation } from '../../organisms/ChildNavigation' import { useAttachedModules } from '../../organisms/Devices/hooks' -import { MultipleModulesModal } from '../../organisms/Devices/ProtocolRun/SetupModuleAndDeck/MultipleModulesModal' import { getProtocolModulesInfo } from '../../organisms/Devices/ProtocolRun/utils/getProtocolModulesInfo' import { useMostRecentCompletedAnalysis } from '../../organisms/LabwarePositionCheck/useMostRecentCompletedAnalysis' import { @@ -48,10 +47,6 @@ export function ProtocolSetupModulesAndDeck({ }: ProtocolSetupModulesAndDeckProps): JSX.Element { const { i18n, t } = useTranslation('protocol_setup') - const [ - showMultipleModulesModal, - setShowMultipleModulesModal, - ] = React.useState(false) const [ showSetupInstructionsModal, setShowSetupInstructionsModal, @@ -93,11 +88,6 @@ export function ProtocolSetupModulesAndDeck({ <> {createPortal( <> - {showMultipleModulesModal ? ( - setShowMultipleModulesModal(false)} - /> - ) : null} {showSetupInstructionsModal ? ( ) : null} (false) + const deckDef = getDeckDefFromRobotType(FLEX_ROBOT_TYPE) const deckConfig = useDeckConfigurationQuery().data ?? [] const { updateDeckConfiguration } = useUpdateDeckConfigurationMutation() @@ -66,19 +75,53 @@ export function DeckConfigurationEditor(): JSX.Element { setShowConfigurationModal(true) } - const handleClickRemove = (cutoutId: CutoutId): void => { - setCurrentDeckConfig(prevDeckConfig => - prevDeckConfig.map(fixture => - fixture.cutoutId === cutoutId + const handleClickRemove = ( + cutoutId: CutoutId, + cutoutFixtureId: CutoutFixtureId + ): void => { + let replacementFixtureId: CutoutFixtureId = SINGLE_CENTER_SLOT_FIXTURE + if (SINGLE_RIGHT_CUTOUTS.includes(cutoutId)) { + replacementFixtureId = SINGLE_RIGHT_SLOT_FIXTURE + } else if (SINGLE_LEFT_CUTOUTS.includes(cutoutId)) { + replacementFixtureId = SINGLE_LEFT_SLOT_FIXTURE + } + + const fixtureGroup = + deckDef.cutoutFixtures.find(cf => cf.id === cutoutFixtureId) + ?.fixtureGroup ?? {} + + let newDeckConfig = deckConfig + if (cutoutId in fixtureGroup) { + const groupMap = + fixtureGroup[cutoutId]?.find(group => + Object.entries(group).every(([cId, cfId]) => + deckConfig.find( + config => + config.cutoutId === cId && config.cutoutFixtureId === cfId + ) + ) + ) ?? {} + newDeckConfig = deckConfig.map(cutoutConfig => + cutoutConfig.cutoutId in groupMap ? { - ...fixture, - cutoutFixtureId: SINGLE_RIGHT_CUTOUTS.includes(cutoutId) - ? SINGLE_RIGHT_SLOT_FIXTURE - : SINGLE_LEFT_SLOT_FIXTURE, + ...cutoutConfig, + cutoutFixtureId: replacementFixtureId, + opentronsModuleSerialNumber: undefined, } - : fixture + : cutoutConfig ) - ) + } else { + newDeckConfig = deckConfig.map(cutoutConfig => + cutoutConfig.cutoutId === cutoutId + ? { + ...cutoutConfig, + cutoutFixtureId: replacementFixtureId, + opentronsModuleSerialNumber: undefined, + } + : cutoutConfig + ) + } + updateDeckConfiguration(newDeckConfig) } const handleClickConfirm = (): void => { diff --git a/app/src/pages/ProtocolSetup/__tests__/ProtocolSetup.test.tsx b/app/src/pages/ProtocolSetup/__tests__/ProtocolSetup.test.tsx index b4b5af7e0c8..030e3c1a9a0 100644 --- a/app/src/pages/ProtocolSetup/__tests__/ProtocolSetup.test.tsx +++ b/app/src/pages/ProtocolSetup/__tests__/ProtocolSetup.test.tsx @@ -20,7 +20,7 @@ import { getDeckDefFromRobotType, FLEX_ROBOT_TYPE, STAGING_AREA_RIGHT_SLOT_FIXTURE, - flexDeckDefV4, + flexDeckDefV5, } from '@opentrons/shared-data' import { i18n } from '../../../i18n' @@ -229,14 +229,14 @@ describe('ProtocolSetup', () => { .calledWith(RUN_ID) .thenReturn(CREATED_AT) when(vi.mocked(getProtocolModulesInfo)) - .calledWith(mockEmptyAnalysis, flexDeckDefV4 as any) + .calledWith(mockEmptyAnalysis, flexDeckDefV5 as any) .thenReturn([]) when(vi.mocked(getUnmatchedModulesForProtocol)) .calledWith([], []) .thenReturn({ missingModuleIds: [], remainingAttachedModules: [] }) when(vi.mocked(getDeckDefFromRobotType)) .calledWith('OT-3 Standard') - .thenReturn(flexDeckDefV4 as any) + .thenReturn(flexDeckDefV5 as any) when(vi.mocked(useNotifyRunQuery)) .calledWith(RUN_ID, { staleTime: Infinity }) .thenReturn({ @@ -320,7 +320,7 @@ describe('ProtocolSetup', () => { data: mockRobotSideAnalysis, } as any) when(vi.mocked(getProtocolModulesInfo)) - .calledWith(mockRobotSideAnalysis, flexDeckDefV4 as any) + .calledWith(mockRobotSideAnalysis, flexDeckDefV5 as any) .thenReturn(mockProtocolModuleInfo) when(vi.mocked(getUnmatchedModulesForProtocol)) .calledWith([], mockProtocolModuleInfo) @@ -337,7 +337,7 @@ describe('ProtocolSetup', () => { when(vi.mocked(getProtocolModulesInfo)) .calledWith( { ...mockRobotSideAnalysis, liquids: mockLiquids }, - flexDeckDefV4 as any + flexDeckDefV5 as any ) .thenReturn(mockProtocolModuleInfo) when(vi.mocked(getUnmatchedModulesForProtocol)) @@ -364,7 +364,7 @@ describe('ProtocolSetup', () => { ...mockRobotSideAnalysis, runTimeParameters: mockRunTimeParameterData, }, - flexDeckDefV4 as any + flexDeckDefV5 as any ) .thenReturn(mockProtocolModuleInfo) when(vi.mocked(getUnmatchedModulesForProtocol)) diff --git a/app/src/pages/Protocols/hooks/__tests__/hooks.test.tsx b/app/src/pages/Protocols/hooks/__tests__/hooks.test.tsx index aa8d9f07e8a..7827c82175f 100644 --- a/app/src/pages/Protocols/hooks/__tests__/hooks.test.tsx +++ b/app/src/pages/Protocols/hooks/__tests__/hooks.test.tsx @@ -261,7 +261,7 @@ describe('useRequiredProtocolLabware', () => { }) }) -describe('useMissingProtocolHardware', () => { +describe.only('useMissingProtocolHardware', () => { let wrapper: React.FunctionComponent<{ children: React.ReactNode }> beforeEach(() => { vi.mocked(useInstrumentsQuery).mockReturnValue({ @@ -343,14 +343,6 @@ describe('useMissingProtocolHardware', () => { connected: false, hasSlotConflict: true, }, - { - hardwareType: 'fixture', - cutoutFixtureId: 'singleRightSlot', - location: { - cutout: 'cutoutD3', - }, - hasSlotConflict: true, - }, ], conflictedSlots: ['D3'], }) @@ -374,6 +366,21 @@ describe('useMissingProtocolHardware', () => { data: { data: [mockHeaterShaker] }, isLoading: false, } as any) + vi.mocked(useDeckConfigurationQuery).mockReturnValue({ + data: [ + omitBy( + FLEX_SIMPLEST_DECK_CONFIG, + ({ cutoutId }) => cutoutId === 'cutoutD3' + ), + { + cutoutId: 'cutoutD3', + cutoutFixtureId: 'heaterShakerModuleV1', + opentronsModuleSerialNumber: mockHeaterShaker.serialNumber, + }, + ], + isLoading: false, + } as any) + const { result } = renderHook( () => useMissingProtocolHardware(PROTOCOL_ANALYSIS.id), { wrapper } @@ -384,7 +391,7 @@ describe('useMissingProtocolHardware', () => { conflictedSlots: [], }) }) - it('should return conflicting slot when module location is configured with something other than single slot fixture', () => { + it('should return conflicting slot when module location is configured with something other than module fixture', () => { vi.mocked(useInstrumentsQuery).mockReturnValue({ data: { data: [ @@ -425,11 +432,10 @@ describe('useMissingProtocolHardware', () => { expect(result.current).toEqual({ missingProtocolHardware: [ { - hardwareType: 'fixture', - cutoutFixtureId: 'singleRightSlot', - location: { - cutout: 'cutoutD3', - }, + hardwareType: 'module', + moduleModel: 'heaterShakerModuleV1', + slot: 'D3', + connected: false, hasSlotConflict: true, }, ], diff --git a/app/src/pages/Protocols/hooks/index.ts b/app/src/pages/Protocols/hooks/index.ts index 964103dc5c5..22e049f4ca8 100644 --- a/app/src/pages/Protocols/hooks/index.ts +++ b/app/src/pages/Protocols/hooks/index.ts @@ -9,10 +9,12 @@ import { import { FLEX_ROBOT_TYPE, FLEX_SINGLE_SLOT_ADDRESSABLE_AREAS, - SINGLE_SLOT_FIXTURES, getCutoutIdForSlotName, getDeckDefFromRobotType, RunTimeParameter, + getCutoutFixtureIdsForModuleModel, + getCutoutFixturesForModuleModel, + FLEX_MODULE_ADDRESSABLE_AREAS, } from '@opentrons/shared-data' import { getLabwareSetupItemGroups } from '../utils' import { getProtocolUsesGripper } from '../../../organisms/ProtocolSetupInstruments/utils' @@ -28,7 +30,6 @@ import type { RobotType, } from '@opentrons/shared-data' import type { LabwareSetupItem } from '../utils' -import type { AttachedModule } from '@opentrons/api-client' interface ProtocolPipette { hardwareType: 'pipette' @@ -105,29 +106,38 @@ export const useRequiredProtocolHardwareFromAnalysis = ( ] : [] - const handleModuleConnectionCheckFor = ( - attachedModules: AttachedModule[], - model: ModuleModel - ): boolean => { - const ASSUME_ALWAYS_CONNECTED_MODULES = ['magneticBlockV1'] - - return !ASSUME_ALWAYS_CONNECTED_MODULES.includes(model) - ? attachedModules.some(m => m.moduleModel === model) - : true - } - const requiredModules: ProtocolModule[] = analysis.modules.map( ({ location, model }) => { + const cutoutIdForSlotName = getCutoutIdForSlotName( + location.slotName, + deckDef + ) + const moduleFixtures = getCutoutFixturesForModuleModel(model, deckDef) + + const configuredModuleSerialNumber = + deckConfig.find( + ({ cutoutId, cutoutFixtureId }) => + cutoutId === cutoutIdForSlotName && + moduleFixtures.map(mf => mf.id).includes(cutoutFixtureId) + )?.opentronsModuleSerialNumber ?? null + const isConnected = moduleFixtures.every( + mf => mf.expectOpentronsModuleSerialNumber + ) + ? attachedModules.some( + m => + m.moduleModel === model && + m.serialNumber === configuredModuleSerialNumber + ) + : true return { hardwareType: 'module', moduleModel: model, slot: location.slotName, - connected: handleModuleConnectionCheckFor(attachedModules, model), + connected: isConnected, hasSlotConflict: deckConfig.some( ({ cutoutId, cutoutFixtureId }) => cutoutId === getCutoutIdForSlotName(location.slotName, deckDef) && - cutoutFixtureId != null && - !SINGLE_SLOT_FIXTURES.includes(cutoutFixtureId) + cutoutFixtureId !== getCutoutFixtureIdsForModuleModel(model)[0] ), } } @@ -161,16 +171,22 @@ export const useRequiredProtocolHardwareFromAnalysis = ( } ) - const requiredFixtures = requiredDeckConfigCompatibility.map( - ({ cutoutFixtureId, cutoutId, compatibleCutoutFixtureIds }) => ({ + const requiredFixtures = requiredDeckConfigCompatibility + // filter out all module fixtures as they're handled in the requiredModules section via hardwareType === 'module' + .filter( + ({ requiredAddressableAreas }) => + !FLEX_MODULE_ADDRESSABLE_AREAS.some(modAA => + requiredAddressableAreas.includes(modAA) + ) + ) + .map(({ cutoutFixtureId, cutoutId, compatibleCutoutFixtureIds }) => ({ hardwareType: 'fixture' as const, cutoutFixtureId: compatibleCutoutFixtureIds[0], location: { cutout: cutoutId }, hasSlotConflict: cutoutFixtureId != null && !compatibleCutoutFixtureIds.includes(cutoutFixtureId), - }) - ) + })) return { requiredProtocolHardware: [ @@ -278,9 +294,16 @@ const useMissingProtocolHardwareFromRequiredProtocolHardware = ( ), ...deckConfigCompatibility .filter( - ({ cutoutFixtureId, compatibleCutoutFixtureIds }) => + ({ + cutoutFixtureId, + compatibleCutoutFixtureIds, + requiredAddressableAreas, + }) => cutoutFixtureId != null && - !compatibleCutoutFixtureIds.some(id => id === cutoutFixtureId) + !compatibleCutoutFixtureIds.some(id => id === cutoutFixtureId) && + !FLEX_MODULE_ADDRESSABLE_AREAS.some(modAA => + requiredAddressableAreas.includes(modAA) + ) // modules are already included via requiredProtocolHardware ) .map(({ compatibleCutoutFixtureIds, cutoutId }) => ({ hardwareType: 'fixture' as const, diff --git a/app/src/resources/deck_configuration/hooks.ts b/app/src/resources/deck_configuration/hooks.ts index 95b92e9f7dc..beae36d9821 100644 --- a/app/src/resources/deck_configuration/hooks.ts +++ b/app/src/resources/deck_configuration/hooks.ts @@ -7,13 +7,9 @@ import { getCutoutIdForAddressableArea, getDeckDefFromRobotType, getLabwareDisplayName, - SINGLE_LEFT_SLOT_FIXTURE, SINGLE_SLOT_FIXTURES, - THERMOCYCLER_MODULE_TYPE, } from '@opentrons/shared-data' -import { getProtocolModulesInfo } from '../../organisms/Devices/ProtocolRun/utils/getProtocolModulesInfo' - import type { CompletedProtocolAnalysis, CutoutConfigProtocolSpec, @@ -47,17 +43,6 @@ export function useDeckConfigurationCompatibility( ? getInitialAndMovedLabwareInSlots(protocolAnalysis) : [] - const protocolModulesInfo = - protocolAnalysis != null - ? getProtocolModulesInfo(protocolAnalysis, deckDef) - : [] - - const hasThermocycler = - protocolModulesInfo.find( - protocolMod => - protocolMod.moduleDef.moduleType === THERMOCYCLER_MODULE_TYPE - ) != null - return deckConfig.reduce( (acc, { cutoutId, cutoutFixtureId }) => { const fixturesThatMountToCutoutId = getCutoutFixturesForCutoutId( @@ -69,7 +54,6 @@ export function useDeckConfigurationCompatibility( getCutoutIdForAddressableArea(aa, fixturesThatMountToCutoutId) === cutoutId ) - const compatibleCutoutFixtureIds = fixturesThatMountToCutoutId .filter(cf => requiredAddressableAreasForCutoutId.every(aa => @@ -106,11 +90,7 @@ export function useDeckConfigurationCompatibility( cutoutId, cutoutFixtureId, requiredAddressableAreas: requiredAddressableAreasForCutoutId, - // Thermocycler requires an "empty" (single slot) fixture in A1 that is not referenced directly in protocol - compatibleCutoutFixtureIds: - hasThermocycler && cutoutId === 'cutoutA1' - ? [SINGLE_LEFT_SLOT_FIXTURE] - : compatibleCutoutFixtureIds, + compatibleCutoutFixtureIds, missingLabwareDisplayName, }, ] diff --git a/app/src/resources/deck_configuration/utils.ts b/app/src/resources/deck_configuration/utils.ts index 9efaeea3a57..5306b967d4b 100644 --- a/app/src/resources/deck_configuration/utils.ts +++ b/app/src/resources/deck_configuration/utils.ts @@ -25,8 +25,9 @@ export function getIsFixtureMismatch( deckConfigProtocolSpec: CutoutConfigAndCompatibility[] ): boolean { const isFixtureMismatch = !deckConfigProtocolSpec.every( - ({ cutoutFixtureId, compatibleCutoutFixtureIds }) => - isMatchedFixture(cutoutFixtureId, compatibleCutoutFixtureIds) + ({ cutoutFixtureId, compatibleCutoutFixtureIds }) => { + return isMatchedFixture(cutoutFixtureId, compatibleCutoutFixtureIds) + } ) return isFixtureMismatch } diff --git a/components/src/hardware-sim/DeckConfigurator/EmptyConfigFixture.tsx b/components/src/hardware-sim/DeckConfigurator/EmptyConfigFixture.tsx index fba7dfc57cf..d790f2e922d 100644 --- a/components/src/hardware-sim/DeckConfigurator/EmptyConfigFixture.tsx +++ b/components/src/hardware-sim/DeckConfigurator/EmptyConfigFixture.tsx @@ -8,10 +8,13 @@ import { RESPONSIVENESS } from '../../ui-style-constants' import { BORDERS, COLORS } from '../../helix-design-system' import { RobotCoordsForeignObject } from '../Deck/RobotCoordsForeignObject' import { + COLUMN_1_SINGLE_SLOT_FIXTURE_WIDTH, + COLUMN_2_SINGLE_SLOT_FIXTURE_WIDTH, + COLUMN_3_SINGLE_SLOT_FIXTURE_WIDTH, COLUMN_1_X_ADJUSTMENT, + COLUMN_2_X_ADJUSTMENT, COLUMN_3_X_ADJUSTMENT, FIXTURE_HEIGHT, - SINGLE_SLOT_FIXTURE_WIDTH, Y_ADJUSTMENT, } from './constants' @@ -39,22 +42,40 @@ export function EmptyConfigFixture( */ const [xSlotPosition = 0, ySlotPosition = 0] = standardSlotCutout?.position ?? [] - - const isColumnOne = - fixtureLocation === 'cutoutA1' || - fixtureLocation === 'cutoutB1' || - fixtureLocation === 'cutoutC1' || - fixtureLocation === 'cutoutD1' - const xAdjustment = isColumnOne - ? COLUMN_1_X_ADJUSTMENT - : COLUMN_3_X_ADJUSTMENT - const x = xSlotPosition + xAdjustment + let x = xSlotPosition + let width = 0 + switch (fixtureLocation) { + case 'cutoutA1': + case 'cutoutB1': + case 'cutoutC1': + case 'cutoutD1': { + x = xSlotPosition + COLUMN_1_X_ADJUSTMENT + width = COLUMN_1_SINGLE_SLOT_FIXTURE_WIDTH + break + } + case 'cutoutA2': + case 'cutoutB2': + case 'cutoutC2': + case 'cutoutD2': { + x = xSlotPosition + COLUMN_2_X_ADJUSTMENT + width = COLUMN_2_SINGLE_SLOT_FIXTURE_WIDTH + break + } + case 'cutoutA3': + case 'cutoutB3': + case 'cutoutC3': + case 'cutoutD3': { + x = xSlotPosition + COLUMN_3_X_ADJUSTMENT + width = COLUMN_3_SINGLE_SLOT_FIXTURE_WIDTH + break + } + } const y = ySlotPosition + Y_ADJUSTMENT return ( void +} + +const HEATER_SHAKER_MODULE_FIXTURE_DISPLAY_NAME = 'Heater Shaker Module' + +export function HeaterShakerFixture( + props: HeaterShakerFixtureProps +): JSX.Element { + const { + deckDefinition, + handleClickRemove, + fixtureLocation, + cutoutFixtureId, + } = props + + const cutoutDef = deckDefinition.locations.cutouts.find( + cutout => cutout.id === fixtureLocation + ) + + /** + * deck definition cutout position is the position of the single slot located within that cutout + * so, to get the position of the cutout itself we must add an adjustment to the slot position + * the adjustment for x is different for right side/left side + */ + const [xSlotPosition = 0, ySlotPosition = 0] = cutoutDef?.position ?? [] + + const isColumnOne = + fixtureLocation === 'cutoutA1' || + fixtureLocation === 'cutoutB1' || + fixtureLocation === 'cutoutC1' || + fixtureLocation === 'cutoutD1' + const xAdjustment = isColumnOne + ? COLUMN_1_X_ADJUSTMENT + : COLUMN_3_X_ADJUSTMENT + const x = xSlotPosition + xAdjustment + + const y = ySlotPosition + Y_ADJUSTMENT + + return ( + + handleClickRemove(fixtureLocation, cutoutFixtureId) + : () => {} + } + > + + {HEATER_SHAKER_MODULE_FIXTURE_DISPLAY_NAME} + + {handleClickRemove != null ? ( + + ) : null} + + + ) +} diff --git a/components/src/hardware-sim/DeckConfigurator/MagneticBlockFixture.tsx b/components/src/hardware-sim/DeckConfigurator/MagneticBlockFixture.tsx new file mode 100644 index 00000000000..a9f1485c2bd --- /dev/null +++ b/components/src/hardware-sim/DeckConfigurator/MagneticBlockFixture.tsx @@ -0,0 +1,130 @@ +import * as React from 'react' + +import { Icon } from '../../icons' +import { Btn, Text } from '../../primitives' +import { TYPOGRAPHY } from '../../ui-style-constants' +import { COLORS } from '../../helix-design-system' +import { RobotCoordsForeignObject } from '../Deck/RobotCoordsForeignObject' +import { + COLUMN_1_SINGLE_SLOT_FIXTURE_WIDTH, + COLUMN_2_SINGLE_SLOT_FIXTURE_WIDTH, + COLUMN_3_SINGLE_SLOT_FIXTURE_WIDTH, + COLUMN_1_X_ADJUSTMENT, + COLUMN_2_X_ADJUSTMENT, + COLUMN_3_X_ADJUSTMENT, + FIXTURE_HEIGHT, + Y_ADJUSTMENT, + CONFIG_STYLE_EDITABLE, + CONFIG_STYLE_READ_ONLY, + STAGING_AREA_FIXTURE_WIDTH, +} from './constants' + +import type { + CutoutFixtureId, + CutoutId, + DeckDefinition, +} from '@opentrons/shared-data' + +interface MagneticBlockFixtureProps { + deckDefinition: DeckDefinition + fixtureLocation: CutoutId + cutoutFixtureId: CutoutFixtureId + handleClickRemove?: ( + fixtureLocation: CutoutId, + cutoutFixtureId: CutoutFixtureId + ) => void + hasStagingArea?: boolean +} + +const MAGNETIC_BLOCK_FIXTURE_DISPLAY_NAME = 'Mag Block' +const STAGING_AREA_WITH_MAGNETIC_BLOCK_DISPLAY_NAME = 'Mag + staging' + +export function MagneticBlockFixture( + props: MagneticBlockFixtureProps +): JSX.Element { + const { + deckDefinition, + fixtureLocation, + handleClickRemove, + cutoutFixtureId, + hasStagingArea, + } = props + + const standardSlotCutout = deckDefinition.locations.cutouts.find( + cutout => cutout.id === fixtureLocation + ) + + /** + * deck definition cutout position is the position of the single slot located within that cutout + * so, to get the position of the cutout itself we must add an adjustment to the slot position + * the adjustment for x is different for right side/left side + */ + const [xSlotPosition = 0, ySlotPosition = 0] = + standardSlotCutout?.position ?? [] + let x = xSlotPosition + let width = 0 + let displayName = hasStagingArea + ? STAGING_AREA_WITH_MAGNETIC_BLOCK_DISPLAY_NAME + : MAGNETIC_BLOCK_FIXTURE_DISPLAY_NAME + switch (fixtureLocation) { + case 'cutoutA1': + case 'cutoutB1': + case 'cutoutC1': + case 'cutoutD1': { + x = xSlotPosition + COLUMN_1_X_ADJUSTMENT + width = COLUMN_1_SINGLE_SLOT_FIXTURE_WIDTH + break + } + case 'cutoutA2': + case 'cutoutB2': + case 'cutoutC2': + case 'cutoutD2': { + x = xSlotPosition + COLUMN_2_X_ADJUSTMENT + width = COLUMN_2_SINGLE_SLOT_FIXTURE_WIDTH + displayName = 'Mag' + break + } + case 'cutoutA3': + case 'cutoutB3': + case 'cutoutC3': + case 'cutoutD3': { + x = xSlotPosition + COLUMN_3_X_ADJUSTMENT + width = hasStagingArea + ? STAGING_AREA_FIXTURE_WIDTH + : COLUMN_3_SINGLE_SLOT_FIXTURE_WIDTH + break + } + } + + const y = ySlotPosition + Y_ADJUSTMENT + + return ( + + handleClickRemove(fixtureLocation, cutoutFixtureId) + : () => {} + } + > + {displayName} + {handleClickRemove != null ? ( + + ) : null} + + + ) +} diff --git a/components/src/hardware-sim/DeckConfigurator/StagingAreaConfigFixture.tsx b/components/src/hardware-sim/DeckConfigurator/StagingAreaConfigFixture.tsx index d1cd3af27d0..2ab3de6c3be 100644 --- a/components/src/hardware-sim/DeckConfigurator/StagingAreaConfigFixture.tsx +++ b/components/src/hardware-sim/DeckConfigurator/StagingAreaConfigFixture.tsx @@ -15,18 +15,31 @@ import { Y_ADJUSTMENT, } from './constants' -import type { CutoutId, DeckDefinition } from '@opentrons/shared-data' +import type { + CutoutFixtureId, + CutoutId, + DeckDefinition, +} from '@opentrons/shared-data' interface StagingAreaConfigFixtureProps { deckDefinition: DeckDefinition fixtureLocation: CutoutId - handleClickRemove?: (fixtureLocation: CutoutId) => void + cutoutFixtureId: CutoutFixtureId + handleClickRemove?: ( + fixtureLocation: CutoutId, + cutoutFixtureId: CutoutFixtureId + ) => void } export function StagingAreaConfigFixture( props: StagingAreaConfigFixtureProps ): JSX.Element { - const { deckDefinition, handleClickRemove, fixtureLocation } = props + const { + deckDefinition, + handleClickRemove, + fixtureLocation, + cutoutFixtureId, + } = props const stagingAreaCutout = deckDefinition.locations.cutouts.find( cutout => cutout.id === fixtureLocation @@ -60,7 +73,7 @@ export function StagingAreaConfigFixture( cursor={handleClickRemove != null ? 'pointer' : 'default'} onClick={ handleClickRemove != null - ? () => handleClickRemove(fixtureLocation) + ? () => handleClickRemove(fixtureLocation, cutoutFixtureId) : () => {} } > diff --git a/components/src/hardware-sim/DeckConfigurator/StaticFixture.tsx b/components/src/hardware-sim/DeckConfigurator/StaticFixture.tsx index a3722d51269..a0fcd1d97d1 100644 --- a/components/src/hardware-sim/DeckConfigurator/StaticFixture.tsx +++ b/components/src/hardware-sim/DeckConfigurator/StaticFixture.tsx @@ -6,7 +6,7 @@ import { RobotCoordsForeignObject } from '../Deck/RobotCoordsForeignObject' import { CONFIG_STYLE_READ_ONLY, FIXTURE_HEIGHT, - MIDDLE_SLOT_FIXTURE_WIDTH, + COLUMN_2_SINGLE_SLOT_FIXTURE_WIDTH, Y_ADJUSTMENT, COLUMN_2_X_ADJUSTMENT, } from './constants' @@ -41,7 +41,7 @@ export function StaticFixture(props: StaticFixtureProps): JSX.Element { return ( void +} + +export function TemperatureModuleFixture( + props: TemperatureModuleFixtureProps +): JSX.Element { + const { + deckDefinition, + handleClickRemove, + fixtureLocation, + cutoutFixtureId, + } = props + + const cutoutDef = deckDefinition.locations.cutouts.find( + cutout => cutout.id === fixtureLocation + ) + + /** + * deck definition cutout position is the position of the single slot located within that cutout + * so, to get the position of the cutout itself we must add an adjustment to the slot position + * the adjustment for x is different for right side/left side + */ + const [xSlotPosition = 0, ySlotPosition = 0] = cutoutDef?.position ?? [] + + const isColumnOne = + fixtureLocation === 'cutoutA1' || + fixtureLocation === 'cutoutB1' || + fixtureLocation === 'cutoutC1' || + fixtureLocation === 'cutoutD1' + const xAdjustment = isColumnOne + ? COLUMN_1_X_ADJUSTMENT + : COLUMN_3_X_ADJUSTMENT + const x = xSlotPosition + xAdjustment + + const y = ySlotPosition + Y_ADJUSTMENT + + return ( + + handleClickRemove(fixtureLocation, cutoutFixtureId) + : () => {} + } + > + + {TEMPERATURE_MODULE_FIXTURE_DISPLAY_NAME} + + {handleClickRemove != null ? ( + + ) : null} + + + ) +} diff --git a/components/src/hardware-sim/DeckConfigurator/ThermocyclerFixture.tsx b/components/src/hardware-sim/DeckConfigurator/ThermocyclerFixture.tsx new file mode 100644 index 00000000000..83369a736ad --- /dev/null +++ b/components/src/hardware-sim/DeckConfigurator/ThermocyclerFixture.tsx @@ -0,0 +1,88 @@ +import * as React from 'react' +import { Icon } from '../../icons' +import { Btn, Text } from '../../primitives' +import { TYPOGRAPHY } from '../../ui-style-constants' +import { COLORS } from '../../helix-design-system' +import { RobotCoordsForeignObject } from '../Deck/RobotCoordsForeignObject' +import { + COLUMN_1_X_ADJUSTMENT, + CONFIG_STYLE_EDITABLE, + CONFIG_STYLE_READ_ONLY, + COLUMN_3_SINGLE_SLOT_FIXTURE_WIDTH, + Y_ADJUSTMENT, + THERMOCYCLER_FIXTURE_HEIGHT, +} from './constants' + +import type { + CutoutFixtureId, + CutoutId, + DeckDefinition, +} from '@opentrons/shared-data' + +interface ThermocyclerFixtureProps { + deckDefinition: DeckDefinition + fixtureLocation: CutoutId + cutoutFixtureId: CutoutFixtureId + handleClickRemove?: ( + fixtureLocation: CutoutId, + cutoutFixtureId: CutoutFixtureId + ) => void +} + +const THERMOCYCLER_FIXTURE_DISPLAY_NAME = 'Thermocycler' + +export function ThermocyclerFixture( + props: ThermocyclerFixtureProps +): JSX.Element { + const { + deckDefinition, + handleClickRemove, + fixtureLocation, + cutoutFixtureId, + } = props + + const cutoutDef = deckDefinition.locations.cutouts.find( + cutout => cutout.id === fixtureLocation + ) + + /** + * deck definition cutout position is the position of the single slot located within that cutout + * so, to get the position of the cutout itself we must add an adjustment to the slot position + * the adjustment for x is different for right side/left side + */ + const [xSlotPosition = 0, ySlotPosition = 0] = cutoutDef?.position ?? [] + const x = xSlotPosition + COLUMN_1_X_ADJUSTMENT + const y = ySlotPosition + Y_ADJUSTMENT + + return ( + + handleClickRemove(fixtureLocation, cutoutFixtureId) + : () => {} + } + > + + {THERMOCYCLER_FIXTURE_DISPLAY_NAME} + + {handleClickRemove != null ? ( + + ) : null} + + + ) +} diff --git a/components/src/hardware-sim/DeckConfigurator/TrashBinConfigFixture.tsx b/components/src/hardware-sim/DeckConfigurator/TrashBinConfigFixture.tsx index 9a9a100d92f..5dd31a57750 100644 --- a/components/src/hardware-sim/DeckConfigurator/TrashBinConfigFixture.tsx +++ b/components/src/hardware-sim/DeckConfigurator/TrashBinConfigFixture.tsx @@ -11,23 +11,36 @@ import { CONFIG_STYLE_EDITABLE, CONFIG_STYLE_READ_ONLY, FIXTURE_HEIGHT, - SINGLE_SLOT_FIXTURE_WIDTH, + COLUMN_3_SINGLE_SLOT_FIXTURE_WIDTH, TRASH_BIN_DISPLAY_NAME, Y_ADJUSTMENT, } from './constants' -import type { CutoutId, DeckDefinition } from '@opentrons/shared-data' +import type { + CutoutFixtureId, + CutoutId, + DeckDefinition, +} from '@opentrons/shared-data' interface TrashBinConfigFixtureProps { deckDefinition: DeckDefinition fixtureLocation: CutoutId - handleClickRemove?: (fixtureLocation: CutoutId) => void + cutoutFixtureId: CutoutFixtureId + handleClickRemove?: ( + fixtureLocation: CutoutId, + cutoutFixtureId: CutoutFixtureId + ) => void } export function TrashBinConfigFixture( props: TrashBinConfigFixtureProps ): JSX.Element { - const { deckDefinition, handleClickRemove, fixtureLocation } = props + const { + deckDefinition, + handleClickRemove, + fixtureLocation, + cutoutFixtureId, + } = props const trashBinCutout = deckDefinition.locations.cutouts.find( cutout => cutout.id === fixtureLocation @@ -54,7 +67,7 @@ export function TrashBinConfigFixture( return ( handleClickRemove(fixtureLocation) + ? () => handleClickRemove(fixtureLocation, cutoutFixtureId) : () => {} } > diff --git a/components/src/hardware-sim/DeckConfigurator/WasteChuteConfigFixture.tsx b/components/src/hardware-sim/DeckConfigurator/WasteChuteConfigFixture.tsx index 1932fe3674b..d1e3c5959f2 100644 --- a/components/src/hardware-sim/DeckConfigurator/WasteChuteConfigFixture.tsx +++ b/components/src/hardware-sim/DeckConfigurator/WasteChuteConfigFixture.tsx @@ -11,17 +11,25 @@ import { CONFIG_STYLE_READ_ONLY, FIXTURE_HEIGHT, STAGING_AREA_FIXTURE_WIDTH, - SINGLE_SLOT_FIXTURE_WIDTH, + COLUMN_3_SINGLE_SLOT_FIXTURE_WIDTH, WASTE_CHUTE_DISPLAY_NAME, Y_ADJUSTMENT, } from './constants' -import type { CutoutId, DeckDefinition } from '@opentrons/shared-data' +import type { + CutoutFixtureId, + CutoutId, + DeckDefinition, +} from '@opentrons/shared-data' interface WasteChuteConfigFixtureProps { deckDefinition: DeckDefinition fixtureLocation: CutoutId - handleClickRemove?: (fixtureLocation: CutoutId) => void + cutoutFixtureId: CutoutFixtureId + handleClickRemove?: ( + fixtureLocation: CutoutId, + cutoutFixtureId: CutoutFixtureId + ) => void hasStagingAreas?: boolean } @@ -32,6 +40,7 @@ export function WasteChuteConfigFixture( deckDefinition, handleClickRemove, fixtureLocation, + cutoutFixtureId, hasStagingAreas = false, } = props @@ -52,7 +61,9 @@ export function WasteChuteConfigFixture( return ( handleClickRemove(fixtureLocation) + ? () => handleClickRemove(fixtureLocation, cutoutFixtureId) : () => {} } > diff --git a/components/src/hardware-sim/DeckConfigurator/constants.ts b/components/src/hardware-sim/DeckConfigurator/constants.ts index 388dcdedecc..4b47b9c5917 100644 --- a/components/src/hardware-sim/DeckConfigurator/constants.ts +++ b/components/src/hardware-sim/DeckConfigurator/constants.ts @@ -9,8 +9,10 @@ import { RESPONSIVENESS, SPACING } from '../../ui-style-constants' * Position is relative to deck definition slot positions and a custom stroke applied to the single slot fixture SVG */ export const FIXTURE_HEIGHT = 102.0 -export const MIDDLE_SLOT_FIXTURE_WIDTH = 158.5 -export const SINGLE_SLOT_FIXTURE_WIDTH = 243.5 +export const THERMOCYCLER_FIXTURE_HEIGHT = 290.0 +export const COLUMN_1_SINGLE_SLOT_FIXTURE_WIDTH = 243.5 +export const COLUMN_2_SINGLE_SLOT_FIXTURE_WIDTH = 159.0 +export const COLUMN_3_SINGLE_SLOT_FIXTURE_WIDTH = 243.5 export const STAGING_AREA_FIXTURE_WIDTH = 314.5 export const COLUMN_1_X_ADJUSTMENT = -100 diff --git a/components/src/hardware-sim/DeckConfigurator/index.tsx b/components/src/hardware-sim/DeckConfigurator/index.tsx index 8477b20e875..ad69bd6c36d 100644 --- a/components/src/hardware-sim/DeckConfigurator/index.tsx +++ b/components/src/hardware-sim/DeckConfigurator/index.tsx @@ -8,6 +8,11 @@ import { TRASH_BIN_ADAPTER_FIXTURE, WASTE_CHUTE_ONLY_FIXTURES, WASTE_CHUTE_STAGING_AREA_FIXTURES, + THERMOCYCLER_V2_FRONT_FIXTURE, + HEATERSHAKER_MODULE_V1_FIXTURE, + TEMPERATURE_MODULE_V2_FIXTURE, + MAGNETIC_BLOCK_V1_FIXTURE, + STAGING_AREA_SLOT_WITH_MAGNETIC_BLOCK_V1_FIXTURE, } from '@opentrons/shared-data' import { COLORS } from '../../helix-design-system' @@ -20,12 +25,23 @@ import { TrashBinConfigFixture } from './TrashBinConfigFixture' import { WasteChuteConfigFixture } from './WasteChuteConfigFixture' import { StaticFixture } from './StaticFixture' -import type { CutoutId, DeckConfiguration } from '@opentrons/shared-data' +import type { + CutoutFixtureId, + CutoutId, + DeckConfiguration, +} from '@opentrons/shared-data' +import { TemperatureModuleFixture } from './TemperatureModuleFixture' +import { HeaterShakerFixture } from './HeaterShakerFixture' +import { MagneticBlockFixture } from './MagneticBlockFixture' +import { ThermocyclerFixture } from './ThermocyclerFixture' interface DeckConfiguratorProps { deckConfig: DeckConfiguration handleClickAdd: (cutoutId: CutoutId) => void - handleClickRemove: (cutoutId: CutoutId) => void + handleClickRemove: ( + cutoutId: CutoutId, + cutoutFixtureId: CutoutFixtureId + ) => void lightFill?: string darkFill?: string readOnly?: boolean @@ -54,6 +70,10 @@ export function DeckConfigurator(props: DeckConfiguratorProps): JSX.Element { 'cutoutB1', 'cutoutC1', 'cutoutD1', + 'cutoutA2', + 'cutoutB2', + 'cutoutC2', + 'cutoutD2', 'cutoutA3', 'cutoutB3', 'cutoutC3', @@ -86,6 +106,22 @@ export function DeckConfigurator(props: DeckConfiguratorProps): JSX.Element { const trashBinFixtures = configurableDeckConfig.filter( ({ cutoutFixtureId }) => cutoutFixtureId === TRASH_BIN_ADAPTER_FIXTURE ) + const thermocyclerFixtures = configurableDeckConfig.filter( + ({ cutoutFixtureId }) => cutoutFixtureId === THERMOCYCLER_V2_FRONT_FIXTURE + ) + const heaterShakerFixtures = configurableDeckConfig.filter( + ({ cutoutFixtureId }) => cutoutFixtureId === HEATERSHAKER_MODULE_V1_FIXTURE + ) + const temperatureModuleFixtures = configurableDeckConfig.filter( + ({ cutoutFixtureId }) => cutoutFixtureId === TEMPERATURE_MODULE_V2_FIXTURE + ) + const magneticBlockFixtures = configurableDeckConfig.filter( + ({ cutoutFixtureId }) => + ([ + MAGNETIC_BLOCK_V1_FIXTURE, + STAGING_AREA_SLOT_WITH_MAGNETIC_BLOCK_V1_FIXTURE, + ] as CutoutFixtureId[]).includes(cutoutFixtureId) + ) return ( ))} - {stagingAreaFixtures.map(({ cutoutId }) => ( + {stagingAreaFixtures.map(({ cutoutId, cutoutFixtureId }) => ( ))} {emptyFixtures.map(({ cutoutId }) => ( @@ -121,29 +158,71 @@ export function DeckConfigurator(props: DeckConfiguratorProps): JSX.Element { fixtureLocation={cutoutId} /> ))} - {wasteChuteFixtures.map(({ cutoutId }) => ( + {wasteChuteFixtures.map(({ cutoutId, cutoutFixtureId }) => ( ))} - {wasteChuteStagingAreaFixtures.map(({ cutoutId }) => ( + {wasteChuteStagingAreaFixtures.map(({ cutoutId, cutoutFixtureId }) => ( ))} - {trashBinFixtures.map(({ cutoutId }) => ( + {trashBinFixtures.map(({ cutoutId, cutoutFixtureId }) => ( + ))} + {temperatureModuleFixtures.map(({ cutoutId, cutoutFixtureId }) => ( + + ))} + {heaterShakerFixtures.map(({ cutoutId, cutoutFixtureId }) => ( + + ))} + {magneticBlockFixtures.map(({ cutoutId, cutoutFixtureId }) => ( + + ))} + {thermocyclerFixtures.map(({ cutoutId, cutoutFixtureId }) => ( + ))} {additionalStaticFixtures?.map(staticFixture => ( diff --git a/components/src/hardware-sim/DeckSlotLocation/index.tsx b/components/src/hardware-sim/DeckSlotLocation/index.tsx index f40558219ec..fbe8431ea54 100644 --- a/components/src/hardware-sim/DeckSlotLocation/index.tsx +++ b/components/src/hardware-sim/DeckSlotLocation/index.tsx @@ -2,7 +2,7 @@ import * as React from 'react' import { getPositionFromSlotId, OT2_ROBOT_TYPE, - ot2DeckDefV4, + ot2DeckDefV5, } from '@opentrons/shared-data' import { SlotBase } from '../BaseDeck/SlotBase' @@ -40,7 +40,7 @@ export function LegacyDeckSlotLocation( if (robotType !== OT2_ROBOT_TYPE) return null - const slotDef = ot2DeckDefV4.locations.addressableAreas.find( + const slotDef = ot2DeckDefV5.locations.addressableAreas.find( s => s.id === slotName ) if (slotDef == null) { @@ -52,7 +52,7 @@ export function LegacyDeckSlotLocation( const slotPosition = getPositionFromSlotId( slotName, - (ot2DeckDefV4 as unknown) as DeckDefinition + (ot2DeckDefV5 as unknown) as DeckDefinition ) const isFixedTrash = slotName === 'fixedTrash' diff --git a/robot-server/robot_server/deck_configuration/defaults.py b/robot-server/robot_server/deck_configuration/defaults.py index a591e9798df..3ed9a5ed395 100644 --- a/robot-server/robot_server/deck_configuration/defaults.py +++ b/robot-server/robot_server/deck_configuration/defaults.py @@ -7,40 +7,64 @@ _for_flex = models.DeckConfigurationRequest.construct( cutoutFixtures=[ models.CutoutFixture.construct( - cutoutId="cutoutA1", cutoutFixtureId="singleLeftSlot" + cutoutId="cutoutA1", + cutoutFixtureId="singleLeftSlot", + opentronsModuleSerialNumber=None, ), models.CutoutFixture.construct( - cutoutId="cutoutB1", cutoutFixtureId="singleLeftSlot" + cutoutId="cutoutB1", + cutoutFixtureId="singleLeftSlot", + opentronsModuleSerialNumber=None, ), models.CutoutFixture.construct( - cutoutId="cutoutC1", cutoutFixtureId="singleLeftSlot" + cutoutId="cutoutC1", + cutoutFixtureId="singleLeftSlot", + opentronsModuleSerialNumber=None, ), models.CutoutFixture.construct( - cutoutId="cutoutD1", cutoutFixtureId="singleLeftSlot" + cutoutId="cutoutD1", + cutoutFixtureId="singleLeftSlot", + opentronsModuleSerialNumber=None, ), models.CutoutFixture.construct( - cutoutId="cutoutA2", cutoutFixtureId="singleCenterSlot" + cutoutId="cutoutA2", + cutoutFixtureId="singleCenterSlot", + opentronsModuleSerialNumber=None, ), models.CutoutFixture.construct( - cutoutId="cutoutB2", cutoutFixtureId="singleCenterSlot" + cutoutId="cutoutB2", + cutoutFixtureId="singleCenterSlot", + opentronsModuleSerialNumber=None, ), models.CutoutFixture.construct( - cutoutId="cutoutC2", cutoutFixtureId="singleCenterSlot" + cutoutId="cutoutC2", + cutoutFixtureId="singleCenterSlot", + opentronsModuleSerialNumber=None, ), models.CutoutFixture.construct( - cutoutId="cutoutD2", cutoutFixtureId="singleCenterSlot" + cutoutId="cutoutD2", + cutoutFixtureId="singleCenterSlot", + opentronsModuleSerialNumber=None, ), models.CutoutFixture.construct( - cutoutId="cutoutA3", cutoutFixtureId="trashBinAdapter" + cutoutId="cutoutA3", + cutoutFixtureId="trashBinAdapter", + opentronsModuleSerialNumber=None, ), models.CutoutFixture.construct( - cutoutId="cutoutB3", cutoutFixtureId="singleRightSlot" + cutoutId="cutoutB3", + cutoutFixtureId="singleRightSlot", + opentronsModuleSerialNumber=None, ), models.CutoutFixture.construct( - cutoutId="cutoutC3", cutoutFixtureId="singleRightSlot" + cutoutId="cutoutC3", + cutoutFixtureId="singleRightSlot", + opentronsModuleSerialNumber=None, ), models.CutoutFixture.construct( - cutoutId="cutoutD3", cutoutFixtureId="singleRightSlot" + cutoutId="cutoutD3", + cutoutFixtureId="singleRightSlot", + opentronsModuleSerialNumber=None, ), ] ) @@ -49,40 +73,64 @@ _for_ot2 = models.DeckConfigurationRequest.construct( cutoutFixtures=[ models.CutoutFixture.construct( - cutoutId="cutout1", cutoutFixtureId="singleStandardSlot" + cutoutId="cutout1", + cutoutFixtureId="singleStandardSlot", + opentronsModuleSerialNumber=None, ), models.CutoutFixture.construct( - cutoutId="cutout2", cutoutFixtureId="singleStandardSlot" + cutoutId="cutout2", + cutoutFixtureId="singleStandardSlot", + opentronsModuleSerialNumber=None, ), models.CutoutFixture.construct( - cutoutId="cutout3", cutoutFixtureId="singleStandardSlot" + cutoutId="cutout3", + cutoutFixtureId="singleStandardSlot", + opentronsModuleSerialNumber=None, ), models.CutoutFixture.construct( - cutoutId="cutout4", cutoutFixtureId="singleStandardSlot" + cutoutId="cutout4", + cutoutFixtureId="singleStandardSlot", + opentronsModuleSerialNumber=None, ), models.CutoutFixture.construct( - cutoutId="cutout5", cutoutFixtureId="singleStandardSlot" + cutoutId="cutout5", + cutoutFixtureId="singleStandardSlot", + opentronsModuleSerialNumber=None, ), models.CutoutFixture.construct( - cutoutId="cutout6", cutoutFixtureId="singleStandardSlot" + cutoutId="cutout6", + cutoutFixtureId="singleStandardSlot", + opentronsModuleSerialNumber=None, ), models.CutoutFixture.construct( - cutoutId="cutout7", cutoutFixtureId="singleStandardSlot" + cutoutId="cutout7", + cutoutFixtureId="singleStandardSlot", + opentronsModuleSerialNumber=None, ), models.CutoutFixture.construct( - cutoutId="cutout8", cutoutFixtureId="singleStandardSlot" + cutoutId="cutout8", + cutoutFixtureId="singleStandardSlot", + opentronsModuleSerialNumber=None, ), models.CutoutFixture.construct( - cutoutId="cutout9", cutoutFixtureId="singleStandardSlot" + cutoutId="cutout9", + cutoutFixtureId="singleStandardSlot", + opentronsModuleSerialNumber=None, ), models.CutoutFixture.construct( - cutoutId="cutout10", cutoutFixtureId="singleStandardSlot" + cutoutId="cutout10", + cutoutFixtureId="singleStandardSlot", + opentronsModuleSerialNumber=None, ), models.CutoutFixture.construct( - cutoutId="cutout11", cutoutFixtureId="singleStandardSlot" + cutoutId="cutout11", + cutoutFixtureId="singleStandardSlot", + opentronsModuleSerialNumber=None, ), models.CutoutFixture.construct( - cutoutId="cutout12", cutoutFixtureId="fixedTrashSlot" + cutoutId="cutout12", + cutoutFixtureId="fixedTrashSlot", + opentronsModuleSerialNumber=None, ), ] ) diff --git a/robot-server/robot_server/deck_configuration/models.py b/robot-server/robot_server/deck_configuration/models.py index f0d2a7cd6bd..b84b395a667 100644 --- a/robot-server/robot_server/deck_configuration/models.py +++ b/robot-server/robot_server/deck_configuration/models.py @@ -33,6 +33,13 @@ class CutoutFixture(pydantic.BaseModel): " [deck definition](https://github.com/Opentrons/opentrons/tree/edge/shared-data/deck)." ) ) + opentronsModuleSerialNumber: Optional[str] = pydantic.Field( + description=( + "The serial number of a module loaded as a fixture." + " [deck definition](https://github.com/Opentrons/opentrons/tree/edge/shared-data/deck)." + ), + default=None, + ) class DeckConfigurationRequest(pydantic.BaseModel): diff --git a/robot-server/robot_server/deck_configuration/router.py b/robot-server/robot_server/deck_configuration/router.py index 4e00a3d707e..6e9d68d9f1b 100644 --- a/robot-server/robot_server/deck_configuration/router.py +++ b/robot-server/robot_server/deck_configuration/router.py @@ -7,7 +7,7 @@ import fastapi from starlette.status import HTTP_422_UNPROCESSABLE_ENTITY -from opentrons_shared_data.deck.dev_types import DeckDefinitionV4 +from opentrons_shared_data.deck.dev_types import DeckDefinitionV5 from robot_server.errors.error_responses import ErrorBody from robot_server.hardware import get_deck_definition @@ -64,7 +64,7 @@ async def put_deck_configuration( # noqa: D103 request_body: RequestModel[models.DeckConfigurationRequest], store: DeckConfigurationStore = fastapi.Depends(get_deck_configuration_store), now: datetime = fastapi.Depends(get_current_time), - deck_definition: DeckDefinitionV4 = fastapi.Depends(get_deck_definition), + deck_definition: DeckDefinitionV5 = fastapi.Depends(get_deck_definition), ) -> PydanticResponse[ Union[ SimpleBody[models.DeckConfigurationResponse], diff --git a/robot-server/robot_server/deck_configuration/store.py b/robot-server/robot_server/deck_configuration/store.py index feffa539ec0..e892c91f7e5 100644 --- a/robot-server/robot_server/deck_configuration/store.py +++ b/robot-server/robot_server/deck_configuration/store.py @@ -54,7 +54,9 @@ async def set( path=self._path, cutout_fixture_placements=[ calibration_storage_types.CutoutFixturePlacement( - cutout_fixture_id=e.cutoutFixtureId, cutout_id=e.cutoutId + cutout_fixture_id=e.cutoutFixtureId, + cutout_id=e.cutoutId, + opentrons_module_serial_number=e.opentronsModuleSerialNumber, ) for e in request.cutoutFixtures ], @@ -71,7 +73,8 @@ async def get_deck_configuration(self) -> DeckConfigurationType: """Get the robot's current deck configuration in an expected typing.""" to_convert = await self.get() converted = [ - (item.cutoutId, item.cutoutFixtureId) for item in to_convert.cutoutFixtures + (item.cutoutId, item.cutoutFixtureId, item.opentronsModuleSerialNumber) + for item in to_convert.cutoutFixtures ] return converted @@ -102,6 +105,7 @@ async def _get_assuming_locked(self) -> models.DeckConfigurationResponse: models.CutoutFixture.construct( cutoutFixtureId=e.cutout_fixture_id, cutoutId=e.cutout_id, + opentronsModuleSerialNumber=e.opentrons_module_serial_number, ) for e in cutout_fixtures_from_storage ] diff --git a/robot-server/robot_server/deck_configuration/validation.py b/robot-server/robot_server/deck_configuration/validation.py index 0530a4f9271..a3c043f8f51 100644 --- a/robot-server/robot_server/deck_configuration/validation.py +++ b/robot-server/robot_server/deck_configuration/validation.py @@ -3,7 +3,7 @@ from collections import defaultdict from dataclasses import dataclass -from typing import DefaultDict, FrozenSet, List, Set, Tuple, Union +from typing import DefaultDict, FrozenSet, List, Set, Tuple, Union, Optional from opentrons_shared_data.deck import dev_types as deck_types @@ -14,6 +14,7 @@ class Placement: cutout_id: str cutout_fixture_id: str + opentrons_module_serial_number: Optional[str] @dataclass(frozen=True) @@ -43,22 +44,51 @@ class InvalidLocationError: @dataclass(frozen=True) class UnrecognizedCutoutFixtureError: - """When an cutout fixture has been mounted that's not defined by the deck definition.""" + """When a cutout fixture has been mounted that's not defined by the deck definition.""" cutout_fixture_id: str allowed_cutout_fixture_ids: FrozenSet[str] +@dataclass(frozen=True) +class InvalidSerialNumberError: + """When a module cutout fixture has been mounted but not given a serial number.""" + + cutout_id: str + cutout_fixture_id: str + + +@dataclass(frozen=True) +class UnexpectedSerialNumberError: + """When a cutout fixture that is not a module has been provided a serial number.""" + + cutout_id: str + cutout_fixture_id: str + opentrons_module_serial_number: str + + +@dataclass(frozen=True) +class MissingGroupFixtureError: + """When a member of a fixture group has been mounted but other required members of that group have not.""" + + cutout_id: str + cutout_fixture_id: str + missing_fixture_id: str + + ConfigurationError = Union[ UnoccupiedCutoutError, OvercrowdedCutoutError, InvalidLocationError, UnrecognizedCutoutFixtureError, + InvalidSerialNumberError, + UnexpectedSerialNumberError, + MissingGroupFixtureError, ] -def get_configuration_errors( - deck_definition: deck_types.DeckDefinitionV4, +def get_configuration_errors( # noqa: C901 + deck_definition: deck_types.DeckDefinitionV5, placements: List[Placement], ) -> Set[ConfigurationError]: """Return all the problems with the given deck configration. @@ -98,12 +128,55 @@ def get_configuration_errors( allowed_cutout_ids=allowed_cutout_ids, ) ) + if found_cutout_fixture[ + "expectOpentronsModuleSerialNumber" + ] is False and isinstance(placement.opentrons_module_serial_number, str): + errors.add( + UnexpectedSerialNumberError( + cutout_id=placement.cutout_id, + cutout_fixture_id=placement.cutout_fixture_id, + opentrons_module_serial_number=placement.opentrons_module_serial_number, + ) + ) + elif ( + found_cutout_fixture["expectOpentronsModuleSerialNumber"] is True + and placement.opentrons_module_serial_number is None + ): + errors.add( + InvalidSerialNumberError( + cutout_id=placement.cutout_id, + cutout_fixture_id=placement.cutout_fixture_id, + ) + ) + for cutout_id in found_cutout_fixture["fixtureGroup"]: + if cutout_id == placement.cutout_id: + map = found_cutout_fixture["fixtureGroup"][cutout_id] + member_found = False + for item in map: + for group_member_cutout_id in item: + group_member_fixture_id = item[group_member_cutout_id] + for deck_item in placements: + if ( + group_member_fixture_id + == deck_item.cutout_fixture_id + and group_member_cutout_id == deck_item.cutout_id + ): + member_found = True + if member_found is False: + errors.add( + MissingGroupFixtureError( + cutout_id=placement.cutout_id, + cutout_fixture_id=placement.cutout_fixture_id, + missing_fixture_id=group_member_fixture_id, + ) + ) + member_found = False return errors def _find_cutout_fixture( - deck_definition: deck_types.DeckDefinitionV4, cutout_fixture_id: str + deck_definition: deck_types.DeckDefinitionV5, cutout_fixture_id: str ) -> Union[deck_types.CutoutFixture, UnrecognizedCutoutFixtureError]: cutout_fixtures = deck_definition["cutoutFixtures"] try: diff --git a/robot-server/robot_server/deck_configuration/validation_mapping.py b/robot-server/robot_server/deck_configuration/validation_mapping.py index 10d9b65158a..1337218075c 100644 --- a/robot-server/robot_server/deck_configuration/validation_mapping.py +++ b/robot-server/robot_server/deck_configuration/validation_mapping.py @@ -10,7 +10,11 @@ def map_in(request: models.DeckConfigurationRequest) -> List[validation.Placement]: """Map a request from HTTP to internal types that can be validated.""" return [ - validation.Placement(cutout_id=p.cutoutId, cutout_fixture_id=p.cutoutFixtureId) + validation.Placement( + cutout_id=p.cutoutId, + cutout_fixture_id=p.cutoutFixtureId, + opentrons_module_serial_number=p.opentronsModuleSerialNumber, + ) for p in request.cutoutFixtures ] diff --git a/robot-server/robot_server/hardware.py b/robot-server/robot_server/hardware.py index c72a162b1be..2994248a302 100644 --- a/robot-server/robot_server/hardware.py +++ b/robot-server/robot_server/hardware.py @@ -381,9 +381,9 @@ async def get_deck_type() -> DeckType: async def get_deck_definition( deck_type: DeckType = Depends(get_deck_type), -) -> deck.dev_types.DeckDefinitionV4: +) -> deck.dev_types.DeckDefinitionV5: """Return this robot's deck definition.""" - return deck.load(deck_type, version=4) + return deck.load(deck_type, version=5) async def _postinit_ot2_tasks( diff --git a/robot-server/tests/deck_configuration/test_defaults.py b/robot-server/tests/deck_configuration/test_defaults.py index ec3bbed3c22..42aa3672f52 100644 --- a/robot-server/tests/deck_configuration/test_defaults.py +++ b/robot-server/tests/deck_configuration/test_defaults.py @@ -12,7 +12,7 @@ from robot_server.deck_configuration import validation_mapping -DECK_DEFINITION_VERSION: Final = 4 +DECK_DEFINITION_VERSION: Final = 5 @pytest.mark.parametrize( diff --git a/robot-server/tests/deck_configuration/test_validation.py b/robot-server/tests/deck_configuration/test_validation.py index 5aee74491da..24aecf9117a 100644 --- a/robot-server/tests/deck_configuration/test_validation.py +++ b/robot-server/tests/deck_configuration/test_validation.py @@ -7,22 +7,26 @@ def test_valid() -> None: """It should return an empty error list if the input is valid.""" - deck_definition = load_deck_definition("ot3_standard", version=4) + deck_definition = load_deck_definition("ot3_standard", version=5) cutout_fixtures = [ - subject.Placement(cutout_fixture_id=cutout_fixture_id, cutout_id=cutout_id) - for cutout_fixture_id, cutout_id in [ - ("singleLeftSlot", "cutoutA1"), - ("singleLeftSlot", "cutoutB1"), - ("singleLeftSlot", "cutoutC1"), - ("singleLeftSlot", "cutoutD1"), - ("singleCenterSlot", "cutoutA2"), - ("singleCenterSlot", "cutoutB2"), - ("singleCenterSlot", "cutoutC2"), - ("singleCenterSlot", "cutoutD2"), - ("stagingAreaRightSlot", "cutoutA3"), - ("singleRightSlot", "cutoutB3"), - ("stagingAreaRightSlot", "cutoutC3"), - ("singleRightSlot", "cutoutD3"), + subject.Placement( + cutout_fixture_id=cutout_fixture_id, + cutout_id=cutout_id, + opentrons_module_serial_number=opentrons_module_serial_number, + ) + for cutout_fixture_id, cutout_id, opentrons_module_serial_number in [ + ("singleLeftSlot", "cutoutA1", None), + ("singleLeftSlot", "cutoutB1", None), + ("singleLeftSlot", "cutoutC1", None), + ("singleLeftSlot", "cutoutD1", None), + ("singleCenterSlot", "cutoutA2", None), + ("singleCenterSlot", "cutoutB2", None), + ("singleCenterSlot", "cutoutC2", None), + ("singleCenterSlot", "cutoutD2", None), + ("stagingAreaRightSlot", "cutoutA3", None), + ("singleRightSlot", "cutoutB3", None), + ("stagingAreaRightSlot", "cutoutC3", None), + ("singleRightSlot", "cutoutD3", None), ] ] assert subject.get_configuration_errors(deck_definition, cutout_fixtures) == set() @@ -30,23 +34,27 @@ def test_valid() -> None: def test_invalid_empty_cutouts() -> None: """It should enforce that every cutout is occupied.""" - deck_definition = load_deck_definition("ot3_standard", version=4) + deck_definition = load_deck_definition("ot3_standard", version=5) cutout_fixtures = [ - subject.Placement(cutout_fixture_id=cutout_fixture_id, cutout_id=cutout_id) - for cutout_fixture_id, cutout_id in [ - ("singleLeftSlot", "cutoutA1"), - ("singleLeftSlot", "cutoutB1"), - ("singleLeftSlot", "cutoutC1"), - ("singleLeftSlot", "cutoutD1"), - ("singleCenterSlot", "cutoutA2"), - ("singleCenterSlot", "cutoutB2"), + subject.Placement( + cutout_fixture_id=cutout_fixture_id, + cutout_id=cutout_id, + opentrons_module_serial_number=opentrons_module_serial_number, + ) + for cutout_fixture_id, cutout_id, opentrons_module_serial_number in [ + ("singleLeftSlot", "cutoutA1", None), + ("singleLeftSlot", "cutoutB1", None), + ("singleLeftSlot", "cutoutC1", None), + ("singleLeftSlot", "cutoutD1", None), + ("singleCenterSlot", "cutoutA2", None), + ("singleCenterSlot", "cutoutB2", None), # Invalid because we haven't placed anything into cutout C2 or D2. - # ("singleCenterSlot", "cutoutC2"), - # ("singleCenterSlot", "cutoutD2"), - ("stagingAreaRightSlot", "cutoutA3"), - ("singleRightSlot", "cutoutB3"), - ("stagingAreaRightSlot", "cutoutC3"), - ("singleRightSlot", "cutoutD3"), + # ("singleCenterSlot", "cutoutC2", None), + # ("singleCenterSlot", "cutoutD2", None), + ("stagingAreaRightSlot", "cutoutA3", None), + ("singleRightSlot", "cutoutB3", None), + ("stagingAreaRightSlot", "cutoutC3", None), + ("singleRightSlot", "cutoutD3", None), ] ] assert subject.get_configuration_errors(deck_definition, cutout_fixtures) == { @@ -57,26 +65,30 @@ def test_invalid_empty_cutouts() -> None: def test_invalid_overcrowded_cutouts() -> None: """It should prevent you from putting multiple things into a single cutout.""" - deck_definition = load_deck_definition("ot3_standard", version=4) + deck_definition = load_deck_definition("ot3_standard", version=5) cutout_fixtures = [ - subject.Placement(cutout_fixture_id=cutout_fixture_id, cutout_id=cutout_id) - for cutout_fixture_id, cutout_id in [ - ("singleLeftSlot", "cutoutA1"), - ("singleLeftSlot", "cutoutB1"), - ("singleLeftSlot", "cutoutC1"), - ("singleLeftSlot", "cutoutD1"), - ("singleCenterSlot", "cutoutA2"), - ("singleCenterSlot", "cutoutB2"), - ("singleCenterSlot", "cutoutC2"), - ("singleCenterSlot", "cutoutD2"), - ("stagingAreaRightSlot", "cutoutA3"), - ("singleRightSlot", "cutoutB3"), + subject.Placement( + cutout_fixture_id=cutout_fixture_id, + cutout_id=cutout_id, + opentrons_module_serial_number=opentrons_module_serial_number, + ) + for cutout_fixture_id, cutout_id, opentrons_module_serial_number in [ + ("singleLeftSlot", "cutoutA1", None), + ("singleLeftSlot", "cutoutB1", None), + ("singleLeftSlot", "cutoutC1", None), + ("singleLeftSlot", "cutoutD1", None), + ("singleCenterSlot", "cutoutA2", None), + ("singleCenterSlot", "cutoutB2", None), + ("singleCenterSlot", "cutoutC2", None), + ("singleCenterSlot", "cutoutD2", None), + ("stagingAreaRightSlot", "cutoutA3", None), + ("singleRightSlot", "cutoutB3", None), # Invalid because we're placing two things in cutout C3... - ("stagingAreaRightSlot", "cutoutC3"), - ("stagingAreaRightSlot", "cutoutC3"), + ("stagingAreaRightSlot", "cutoutC3", None), + ("stagingAreaRightSlot", "cutoutC3", None), # ...and two things in cutout D3. - ("wasteChuteRightAdapterNoCover", "cutoutD3"), - ("singleRightSlot", "cutoutD3"), + ("wasteChuteRightAdapterNoCover", "cutoutD3", None), + ("singleRightSlot", "cutoutD3", None), ] ] assert subject.get_configuration_errors(deck_definition, cutout_fixtures) == { @@ -93,24 +105,28 @@ def test_invalid_overcrowded_cutouts() -> None: def test_invalid_cutout_for_fixture() -> None: """Each fixture must be placed in a location that's valid for that particular fixture.""" - deck_definition = load_deck_definition("ot3_standard", version=4) + deck_definition = load_deck_definition("ot3_standard", version=5) cutout_fixtures = [ - subject.Placement(cutout_fixture_id=cutout_fixture_id, cutout_id=cutout_id) - for cutout_fixture_id, cutout_id in [ - ("singleLeftSlot", "cutoutA1"), - ("singleLeftSlot", "cutoutB1"), - ("singleLeftSlot", "cutoutC1"), - ("singleLeftSlot", "cutoutD1"), - ("singleCenterSlot", "cutoutA2"), - ("singleCenterSlot", "cutoutB2"), + subject.Placement( + cutout_fixture_id=cutout_fixture_id, + cutout_id=cutout_id, + opentrons_module_serial_number=opentrons_module_serial_number, + ) + for cutout_fixture_id, cutout_id, opentrons_module_serial_number in [ + ("singleLeftSlot", "cutoutA1", None), + ("singleLeftSlot", "cutoutB1", None), + ("singleLeftSlot", "cutoutC1", None), + ("singleLeftSlot", "cutoutD1", None), + ("singleCenterSlot", "cutoutA2", None), + ("singleCenterSlot", "cutoutB2", None), # Invalid because wasteChuteRightAdapterNoCover can't be placed in cutout C2... - ("wasteChuteRightAdapterNoCover", "cutoutC2"), + ("wasteChuteRightAdapterNoCover", "cutoutC2", None), # ...nor can singleLeftSlot be placed in cutout D2. - ("singleLeftSlot", "cutoutD2"), - ("stagingAreaRightSlot", "cutoutA3"), - ("singleRightSlot", "cutoutB3"), - ("stagingAreaRightSlot", "cutoutC3"), - ("singleRightSlot", "cutoutD3"), + ("singleLeftSlot", "cutoutD2", None), + ("stagingAreaRightSlot", "cutoutA3", None), + ("singleRightSlot", "cutoutB3", None), + ("stagingAreaRightSlot", "cutoutC3", None), + ("singleRightSlot", "cutoutD3", None), ] ] assert subject.get_configuration_errors(deck_definition, cutout_fixtures) == { @@ -131,24 +147,28 @@ def test_invalid_cutout_for_fixture() -> None: def test_unrecognized_cutout() -> None: """It should raise a sensible error if you pass a totally nonexistent cutout.""" - deck_definition = load_deck_definition("ot3_standard", version=4) + deck_definition = load_deck_definition("ot3_standard", version=5) cutout_fixtures = [ - subject.Placement(cutout_fixture_id=cutout_fixture_id, cutout_id=cutout_id) - for cutout_fixture_id, cutout_id in [ - ("singleLeftSlot", "cutoutA1"), - ("singleLeftSlot", "cutoutB1"), - ("singleLeftSlot", "cutoutC1"), - ("singleLeftSlot", "cutoutD1"), - ("singleCenterSlot", "cutoutA2"), - ("singleCenterSlot", "cutoutB2"), - ("singleCenterSlot", "cutoutC2"), - ("singleCenterSlot", "cutoutD2"), - ("singleRightSlot", "cutoutA3"), - ("singleRightSlot", "cutoutB3"), - ("singleRightSlot", "cutoutC3"), - ("singleRightSlot", "cutoutD3"), + subject.Placement( + cutout_fixture_id=cutout_fixture_id, + cutout_id=cutout_id, + opentrons_module_serial_number=opentrons_module_serial_number, + ) + for cutout_fixture_id, cutout_id, opentrons_module_serial_number in [ + ("singleLeftSlot", "cutoutA1", None), + ("singleLeftSlot", "cutoutB1", None), + ("singleLeftSlot", "cutoutC1", None), + ("singleLeftSlot", "cutoutD1", None), + ("singleCenterSlot", "cutoutA2", None), + ("singleCenterSlot", "cutoutB2", None), + ("singleCenterSlot", "cutoutC2", None), + ("singleCenterSlot", "cutoutD2", None), + ("singleRightSlot", "cutoutA3", None), + ("singleRightSlot", "cutoutB3", None), + ("singleRightSlot", "cutoutC3", None), + ("singleRightSlot", "cutoutD3", None), # Invalid because "someUnrecognizedCutout" is not defined by the deck definition. - ("singleRightSlot", "someUnrecognizedCutout"), + ("singleRightSlot", "someUnrecognizedCutout", None), ] ] assert subject.get_configuration_errors(deck_definition, cutout_fixtures) == { @@ -164,23 +184,27 @@ def test_unrecognized_cutout() -> None: def test_unrecognized_cutout_fixture() -> None: """It should raise a sensible error if you pass a totally nonexistent cutout fixture.""" - deck_definition = load_deck_definition("ot3_standard", version=4) + deck_definition = load_deck_definition("ot3_standard", version=5) cutout_fixtures = [ - subject.Placement(cutout_fixture_id=cutout_fixture_id, cutout_id=cutout_id) - for cutout_fixture_id, cutout_id in [ - ("singleLeftSlot", "cutoutA1"), - ("singleLeftSlot", "cutoutB1"), - ("singleLeftSlot", "cutoutC1"), - ("singleLeftSlot", "cutoutD1"), - ("singleCenterSlot", "cutoutA2"), - ("singleCenterSlot", "cutoutB2"), - ("singleCenterSlot", "cutoutC2"), - ("singleCenterSlot", "cutoutD2"), - ("singleRightSlot", "cutoutA3"), - ("singleRightSlot", "cutoutB3"), - ("singleRightSlot", "cutoutC3"), + subject.Placement( + cutout_fixture_id=cutout_fixture_id, + cutout_id=cutout_id, + opentrons_module_serial_number=opentrons_module_serial_number, + ) + for cutout_fixture_id, cutout_id, opentrons_module_serial_number in [ + ("singleLeftSlot", "cutoutA1", None), + ("singleLeftSlot", "cutoutB1", None), + ("singleLeftSlot", "cutoutC1", None), + ("singleLeftSlot", "cutoutD1", None), + ("singleCenterSlot", "cutoutA2", None), + ("singleCenterSlot", "cutoutB2", None), + ("singleCenterSlot", "cutoutC2", None), + ("singleCenterSlot", "cutoutD2", None), + ("singleRightSlot", "cutoutA3", None), + ("singleRightSlot", "cutoutB3", None), + ("singleRightSlot", "cutoutC3", None), # Invalid because "someUnrecognizedCutoutFixture" is not defined by the deck definition. - ("someUnrecognizedCutoutFixture", "cutoutD3"), + ("someUnrecognizedCutoutFixture", "cutoutD3", None), ] ] assert subject.get_configuration_errors(deck_definition, cutout_fixtures) == { @@ -197,7 +221,115 @@ def test_unrecognized_cutout_fixture() -> None: "stagingAreaSlotWithWasteChuteRightAdapterCovered", "stagingAreaSlotWithWasteChuteRightAdapterNoCover", "trashBinAdapter", + "thermocyclerModuleV2Rear", + "thermocyclerModuleV2Front", + "heaterShakerModuleV1", + "temperatureModuleV2", + "magneticBlockV1", + "stagingAreaSlotWithMagneticBlockV1", ] ), ) } + + +def test_invalid_serial_number() -> None: + """It should raise a sensible error if you fail to provide a serial number for a fixture that requires one.""" + deck_definition = load_deck_definition("ot3_standard", version=5) + cutout_fixtures = [ + subject.Placement( + cutout_fixture_id=cutout_fixture_id, + cutout_id=cutout_id, + opentrons_module_serial_number=opentrons_module_serial_number, + ) + for cutout_fixture_id, cutout_id, opentrons_module_serial_number in [ + ("thermocyclerModuleV2Rear", "cutoutA1", "ABC"), + # Invalid, because the Thermocycler V2 Front fixture requires a serial number + ("thermocyclerModuleV2Front", "cutoutB1", None), + ("singleLeftSlot", "cutoutC1", None), + ("singleLeftSlot", "cutoutD1", None), + ("singleCenterSlot", "cutoutA2", None), + ("singleCenterSlot", "cutoutB2", None), + ("singleCenterSlot", "cutoutC2", None), + ("singleCenterSlot", "cutoutD2", None), + ("singleRightSlot", "cutoutA3", None), + ("singleRightSlot", "cutoutB3", None), + ("singleRightSlot", "cutoutC3", None), + ("singleRightSlot", "cutoutD3", None), + ] + ] + assert subject.get_configuration_errors(deck_definition, cutout_fixtures) == { + subject.InvalidSerialNumberError( + cutout_fixture_id="thermocyclerModuleV2Front", + cutout_id="cutoutB1", + ) + } + + +def test_unexpected_serial_number() -> None: + """It should raise a sensible error if you provide a serial number for a fixture that DOES NOT require one.""" + deck_definition = load_deck_definition("ot3_standard", version=5) + cutout_fixtures = [ + subject.Placement( + cutout_fixture_id=cutout_fixture_id, + cutout_id=cutout_id, + opentrons_module_serial_number=opentrons_module_serial_number, + ) + for cutout_fixture_id, cutout_id, opentrons_module_serial_number in [ + # Invalid, single slot fixtures do not have serial numbers + ("singleLeftSlot", "cutoutA1", "ABC"), + ("singleLeftSlot", "cutoutB1", None), + ("singleLeftSlot", "cutoutC1", None), + ("singleLeftSlot", "cutoutD1", None), + ("singleCenterSlot", "cutoutA2", None), + ("singleCenterSlot", "cutoutB2", None), + ("singleCenterSlot", "cutoutC2", None), + ("singleCenterSlot", "cutoutD2", None), + ("singleRightSlot", "cutoutA3", None), + ("singleRightSlot", "cutoutB3", None), + ("singleRightSlot", "cutoutC3", None), + ("singleRightSlot", "cutoutD3", None), + ] + ] + assert subject.get_configuration_errors(deck_definition, cutout_fixtures) == { + subject.UnexpectedSerialNumberError( + cutout_fixture_id="singleLeftSlot", + cutout_id="cutoutA1", + opentrons_module_serial_number="ABC", + ) + } + + +# new test to raise error if not all members of a fixture group are loaded into the deck config +def test_missing_group_fixture() -> None: + """It should raise a sensible error if you fail to provide all members of a fixture group in a deck configuration.""" + deck_definition = load_deck_definition("ot3_standard", version=5) + cutout_fixtures = [ + subject.Placement( + cutout_fixture_id=cutout_fixture_id, + cutout_id=cutout_id, + opentrons_module_serial_number=opentrons_module_serial_number, + ) + for cutout_fixture_id, cutout_id, opentrons_module_serial_number in [ + ("thermocyclerModuleV2Rear", "cutoutA1", "ABC"), + # Invalid, because the Thermocycler V2 Rear fixture above requires a Front fixture be loaded as well + ("singleLeftSlot", "cutoutB1", None), + ("singleLeftSlot", "cutoutC1", None), + ("singleLeftSlot", "cutoutD1", None), + ("singleCenterSlot", "cutoutA2", None), + ("singleCenterSlot", "cutoutB2", None), + ("singleCenterSlot", "cutoutC2", None), + ("singleCenterSlot", "cutoutD2", None), + ("singleRightSlot", "cutoutA3", None), + ("singleRightSlot", "cutoutB3", None), + ("singleRightSlot", "cutoutC3", None), + ("singleRightSlot", "cutoutD3", None), + ] + ] + assert subject.get_configuration_errors(deck_definition, cutout_fixtures) == { + subject.MissingGroupFixtureError( + cutout_fixture_id="thermocyclerModuleV2Rear", + cutout_id="cutoutA1", + missing_fixture_id="thermocyclerModuleV2Front", + ) + } diff --git a/robot-server/tests/integration/http_api/protocols/test_v8_json_upload_flex.tavern.yaml b/robot-server/tests/integration/http_api/protocols/test_v8_json_upload_flex.tavern.yaml index 991d88df87f..af2fc892b86 100644 --- a/robot-server/tests/integration/http_api/protocols/test_v8_json_upload_flex.tavern.yaml +++ b/robot-server/tests/integration/http_api/protocols/test_v8_json_upload_flex.tavern.yaml @@ -103,7 +103,7 @@ stages: definitionUri: opentrons/armadillo_96_wellplate_200ul_pcr_full_skirt/1 displayName: Sample Collection Plate location: - moduleId: magneticModuleId + moduleId: magneticBlockId - id: tipRackId loadName: opentrons_96_tiprack_1000ul definitionUri: opentrons/opentrons_96_tiprack_1000ul/1 @@ -117,9 +117,8 @@ stages: location: slotName: 'A3' modules: - - id: magneticModuleId - serialNumber: !anystr - model: magneticModuleV2 + - id: magneticBlockId + model: magneticBlockV1 location: slotName: 'D3' - id: temperatureModuleId @@ -159,15 +158,14 @@ stages: key: !anystr status: succeeded params: - model: magneticModuleV2 + model: magneticBlockV1 location: slotName: 'D3' - moduleId: magneticModuleId + moduleId: magneticBlockId result: - moduleId: magneticModuleId + moduleId: magneticBlockId definition: !anydict - model: magneticModuleV2 - serialNumber: !anystr + model: magneticBlockV1 notes: [] startedAt: !anystr completedAt: !anystr @@ -215,7 +213,7 @@ stages: status: succeeded params: location: - moduleId: magneticModuleId + moduleId: magneticBlockId loadName: armadillo_96_wellplate_200ul_pcr_full_skirt namespace: opentrons version: 1 @@ -365,7 +363,7 @@ stages: volume: 4.5 flowRate: 2.5 result: - position: { 'x': 341.205, 'y': 65.115, 'z': 84.3 } + position: { 'x': 342.38, 'y': 65.24, 'z': 40.05 } volume: 4.5 notes: [] startedAt: !anystr @@ -388,7 +386,7 @@ stages: radius: 1.0 speed: 42.0 result: - position: { 'x': 341.205, 'y': 65.115, 'z': 94.3 } + position: { 'x': 342.38, 'y': 65.24, 'z': 50.05 } notes: [] startedAt: !anystr completedAt: !anystr @@ -409,7 +407,7 @@ stages: z: 12.0 flowRate: 2.0 result: - position: { 'x': 341.205, 'y': 65.115, 'z': 95.3 } + position: { 'x': 342.38, 'y': 65.24, 'z': 51.05 } notes: [] startedAt: !anystr completedAt: !anystr @@ -448,7 +446,8 @@ stages: forceDirect: false speed: 12.3 result: - position: { 'x': 350.205, 'y': 65.115, 'z': 98.25 } + position: + { 'x': 351.38, 'y': 65.24, 'z': 54.0 } notes: [] startedAt: !anystr completedAt: !anystr @@ -470,7 +469,8 @@ stages: minimumZHeight: 35.0 forceDirect: true result: - position: { 'x': 352.205, 'y': 68.115, 'z': 93.3 } + position: + { 'x': 353.38, 'y': 68.24, 'z': 49.05 } notes: [] startedAt: !anystr completedAt: !anystr diff --git a/robot-server/tests/integration/http_api/runs/test_deck_slot_standardization.py b/robot-server/tests/integration/http_api/runs/test_deck_slot_standardization.py index aa39376cafe..4f065b9a59c 100644 --- a/robot-server/tests/integration/http_api/runs/test_deck_slot_standardization.py +++ b/robot-server/tests/integration/http_api/runs/test_deck_slot_standardization.py @@ -4,6 +4,7 @@ from pytest_lazyfixture import lazy_fixture # type: ignore[import-untyped] from ...robot_client import RobotClient +from ...conftest import _OT3_SESSION_SERVER_PORT @pytest.fixture @@ -58,6 +59,29 @@ async def test_deck_slot_standardization( """ module_model = "temperatureModuleV2" + deck_configuration_request = [ + {"cutoutFixtureId": "singleLeftSlot", "cutoutId": "cutoutA1"}, + {"cutoutFixtureId": "singleLeftSlot", "cutoutId": "cutoutB1"}, + {"cutoutFixtureId": "singleLeftSlot", "cutoutId": "cutoutC1"}, + { + "cutoutFixtureId": "temperatureModuleV2", + "cutoutId": "cutoutD1", + "opentronsModuleSerialNumber": "temp-1234", + }, + {"cutoutFixtureId": "singleCenterSlot", "cutoutId": "cutoutA2"}, + {"cutoutFixtureId": "singleCenterSlot", "cutoutId": "cutoutB2"}, + {"cutoutFixtureId": "singleCenterSlot", "cutoutId": "cutoutC2"}, + {"cutoutFixtureId": "singleCenterSlot", "cutoutId": "cutoutD2"}, + {"cutoutFixtureId": "singleRightSlot", "cutoutId": "cutoutA3"}, + {"cutoutFixtureId": "singleRightSlot", "cutoutId": "cutoutB3"}, + {"cutoutFixtureId": "singleRightSlot", "cutoutId": "cutoutC3"}, + {"cutoutFixtureId": "singleRightSlot", "cutoutId": "cutoutD3"}, + ] + if _OT3_SESSION_SERVER_PORT in robot_client.base_url: + await robot_client.put_deck_configuration( + req_body={"data": {"cutoutFixtures": deck_configuration_request}} + ) + labware_load_name = "armadillo_96_wellplate_200ul_pcr_full_skirt" labware_namespace = "opentrons" labware_version = 1 diff --git a/robot-server/tests/integration/robot_client.py b/robot-server/tests/integration/robot_client.py index 90869cdde92..c4511f8d315 100644 --- a/robot-server/tests/integration/robot_client.py +++ b/robot-server/tests/integration/robot_client.py @@ -312,6 +312,18 @@ async def delete_session(self, session_id: str) -> Response: response.raise_for_status() return response + async def put_deck_configuration( + self, + req_body: Dict[str, object], + ) -> Response: + """PUT /deck_configuration.""" + response = await self.httpx_client.put( + url=f"{self.base_url}/deck_configuration", + json=req_body, + ) + response.raise_for_status() + return response + async def poll_until_run_completes( robot_client: RobotClient, run_id: str, poll_interval: float = _RUN_POLL_INTERVAL diff --git a/shared-data/deck/definitions/5/ot2_short_trash.json b/shared-data/deck/definitions/5/ot2_short_trash.json new file mode 100644 index 00000000000..7d00f8d5773 --- /dev/null +++ b/shared-data/deck/definitions/5/ot2_short_trash.json @@ -0,0 +1,409 @@ +{ + "otId": "ot2_short_trash", + "schemaVersion": 5, + "cornerOffsetFromOrigin": [-115.65, -68.03, 0], + "dimensions": [624.3, 565.2, 0], + "metadata": { + "displayName": "OT-2 Short-Trash Deck", + "tags": ["ot2", "12 slots", "short trash"] + }, + "robot": { + "model": "OT-2 Standard" + }, + "locations": { + "addressableAreas": [ + { + "id": "1", + "areaType": "slot", + "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "matingSurfaceUnitVector": [-1, 1, -1], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Slot 1", + "compatibleModuleTypes": [ + "magneticModuleType", + "temperatureModuleType", + "heaterShakerModuleType" + ] + }, + { + "id": "2", + "areaType": "slot", + "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "matingSurfaceUnitVector": [-1, 1, -1], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Slot 2", + "compatibleModuleTypes": [ + "magneticModuleType", + "temperatureModuleType", + "heaterShakerModuleType" + ] + }, + { + "id": "3", + "areaType": "slot", + "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "matingSurfaceUnitVector": [-1, 1, -1], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Slot 3", + "compatibleModuleTypes": [ + "magneticModuleType", + "temperatureModuleType", + "heaterShakerModuleType" + ] + }, + { + "id": "4", + "areaType": "slot", + "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "matingSurfaceUnitVector": [-1, 1, -1], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Slot 4", + "compatibleModuleTypes": [ + "magneticModuleType", + "temperatureModuleType", + "heaterShakerModuleType" + ] + }, + { + "id": "5", + "areaType": "slot", + "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "matingSurfaceUnitVector": [-1, 1, -1], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Slot 5", + "compatibleModuleTypes": [ + "magneticModuleType", + "temperatureModuleType", + "heaterShakerModuleType" + ] + }, + { + "id": "6", + "areaType": "slot", + "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "matingSurfaceUnitVector": [-1, 1, -1], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Slot 6", + "compatibleModuleTypes": [ + "magneticModuleType", + "temperatureModuleType", + "heaterShakerModuleType" + ] + }, + { + "id": "7", + "areaType": "slot", + "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "matingSurfaceUnitVector": [-1, 1, -1], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Slot 7", + "compatibleModuleTypes": [ + "magneticModuleType", + "temperatureModuleType", + "thermocyclerModuleType", + "heaterShakerModuleType" + ] + }, + { + "id": "8", + "areaType": "slot", + "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "matingSurfaceUnitVector": [-1, 1, -1], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Slot 8", + "compatibleModuleTypes": [ + "magneticModuleType", + "temperatureModuleType", + "heaterShakerModuleType" + ] + }, + { + "id": "9", + "areaType": "slot", + "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "matingSurfaceUnitVector": [-1, 1, -1], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Slot 9", + "compatibleModuleTypes": [ + "magneticModuleType", + "temperatureModuleType", + "heaterShakerModuleType" + ] + }, + { + "id": "10", + "areaType": "slot", + "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "matingSurfaceUnitVector": [-1, 1, -1], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Slot 10", + "compatibleModuleTypes": [ + "magneticModuleType", + "temperatureModuleType", + "heaterShakerModuleType" + ] + }, + { + "id": "11", + "areaType": "slot", + "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "matingSurfaceUnitVector": [-1, 1, -1], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Slot 11", + "compatibleModuleTypes": [ + "magneticModuleType", + "temperatureModuleType", + "heaterShakerModuleType" + ] + }, + { + "id": "12", + "areaType": "slot", + "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Slot 12", + "compatibleModuleTypes": [] + }, + { + "id": "shortFixedTrash", + "areaType": "fixedTrash", + "offsetFromCutoutFixture": [29.285, -2.835, 0], + "boundingBox": { + "xDimension": 107.11, + "yDimension": 165.67, + "zDimension": 58 + }, + "displayName": "Slot 12/Short Fixed Trash", + "ableToDropTips": true + } + ], + "cutouts": [ + { + "id": "cutout1", + "position": [0.0, 0.0, 0.0], + "displayName": "Cutout 1" + }, + { + "id": "cutout2", + "position": [132.5, 0.0, 0.0], + "displayName": "Cutout 2" + }, + { + "id": "cutout3", + "position": [265.0, 0.0, 0.0], + "displayName": "Cutout 3" + }, + { + "id": "cutout4", + "position": [0.0, 90.5, 0.0], + "displayName": "Cutout 4" + }, + { + "id": "cutout5", + "position": [132.5, 90.5, 0.0], + "displayName": "Cutout 5" + }, + { + "id": "cutout6", + "position": [265.0, 90.5, 0.0], + "displayName": "Cutout 6" + }, + { + "id": "cutout7", + "position": [0.0, 181.0, 0.0], + "displayName": "Cutout 7" + }, + { + "id": "cutout8", + "position": [132.5, 181.0, 0.0], + "displayName": "Cutout 8" + }, + { + "id": "cutout9", + "position": [265.0, 181.0, 0.0], + "displayName": "Cutout 9" + }, + { + "id": "cutout10", + "position": [0.0, 271.5, 0.0], + "displayName": "Slot 10" + }, + { + "id": "cutout11", + "position": [132.5, 271.5, 0.0], + "displayName": "Cutout 11" + }, + { + "id": "cutout12", + "position": [265.0, 271.5, 0.0], + "displayName": "Cutout 12" + } + ], + "calibrationPoints": [ + { + "id": "1BLC", + "position": [12.13, 9.0, 0.0], + "displayName": "Slot 1 Bottom Left Cross" + }, + { + "id": "3BRC", + "position": [380.87, 9.0, 0.0], + "displayName": "Slot 3 Bottom Right Cross" + }, + { + "id": "7TLC", + "position": [12.13, 258.0, 0.0], + "displayName": "Slot 7 Top Left Cross" + }, + { + "id": "9TRC", + "position": [380.87, 258.0, 0.0], + "displayName": "Slot 9 Top Right Cross" + }, + { + "id": "10TLC", + "position": [12.13, 348.5, 0.0], + "displayName": "Slot 10 Top Left Cross" + }, + { + "id": "11TRC", + "position": [248.37, 348.5, 0.0], + "displayName": "Slot 11 Top Right Cross" + }, + { + "id": "1BLD", + "position": [12.13, 6.0, 0.0], + "displayName": "Slot 1 Bottom Left Dot" + }, + { + "id": "3BRD", + "position": [380.87, 6.0, 0.0], + "displayName": "Slot 3 Bottom Right Dot" + }, + { + "id": "7TLD", + "position": [12.13, 261.0, 0.0], + "displayName": "Slot 7 Top Left Dot" + }, + { + "id": "9TRD", + "position": [380.87, 261.0, 0.0], + "displayName": "Slot 9 Top Right Dot" + }, + { + "id": "10TLD", + "position": [12.13, 351.5, 0.0], + "displayName": "Slot 10 Top Left Dot" + }, + { + "id": "11TRD", + "position": [248.37, 351.5, 0.0], + "displayName": "Slot 11 Top Right Dot" + } + ], + "legacyFixtures": [ + { + "id": "fixedTrash", + "slot": "12", + "labware": "opentrons_1_trash_850ml_fixed", + "displayName": "Fixed Trash" + } + ] + }, + "cutoutFixtures": [ + { + "id": "singleStandardSlot", + "expectOpentronsModuleSerialNumber": false, + "mayMountTo": [ + "cutout1", + "cutout2", + "cutout3", + "cutout4", + "cutout5", + "cutout6", + "cutout7", + "cutout8", + "cutout9", + "cutout10", + "cutout11", + "cutout12" + ], + "displayName": "Standard Slot", + "providesAddressableAreas": { + "cutout1": ["1"], + "cutout2": ["2"], + "cutout3": ["3"], + "cutout4": ["4"], + "cutout5": ["5"], + "cutout6": ["6"], + "cutout7": ["7"], + "cutout8": ["8"], + "cutout9": ["9"], + "cutout10": ["10"], + "cutout11": ["11"], + "cutout12": ["12"] + }, + "fixtureGroup": {}, + "height": 0 + }, + { + "id": "fixedTrashSlot", + "expectOpentronsModuleSerialNumber": false, + "mayMountTo": ["cutout12"], + "displayName": "Fixed Trash", + "providesAddressableAreas": { + "cutout12": ["shortFixedTrash"] + }, + "fixtureGroup": {}, + "height": 58 + } + ] +} diff --git a/shared-data/deck/definitions/5/ot2_standard.json b/shared-data/deck/definitions/5/ot2_standard.json new file mode 100644 index 00000000000..0ccc1997c3e --- /dev/null +++ b/shared-data/deck/definitions/5/ot2_standard.json @@ -0,0 +1,409 @@ +{ + "otId": "ot2_standard", + "schemaVersion": 5, + "cornerOffsetFromOrigin": [-115.65, -68.03, 0], + "dimensions": [624.3, 565.2, 0], + "metadata": { + "displayName": "OT-2 Standard Deck", + "tags": ["ot2", "12 slots", "standard"] + }, + "robot": { + "model": "OT-2 Standard" + }, + "locations": { + "addressableAreas": [ + { + "id": "1", + "areaType": "slot", + "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "matingSurfaceUnitVector": [-1, 1, -1], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Slot 1", + "compatibleModuleTypes": [ + "magneticModuleType", + "temperatureModuleType", + "heaterShakerModuleType" + ] + }, + { + "id": "2", + "areaType": "slot", + "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "matingSurfaceUnitVector": [-1, 1, -1], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Slot 2", + "compatibleModuleTypes": [ + "magneticModuleType", + "temperatureModuleType", + "heaterShakerModuleType" + ] + }, + { + "id": "3", + "areaType": "slot", + "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "matingSurfaceUnitVector": [-1, 1, -1], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Slot 3", + "compatibleModuleTypes": [ + "magneticModuleType", + "temperatureModuleType", + "heaterShakerModuleType" + ] + }, + { + "id": "4", + "areaType": "slot", + "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "matingSurfaceUnitVector": [-1, 1, -1], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Slot 4", + "compatibleModuleTypes": [ + "magneticModuleType", + "temperatureModuleType", + "heaterShakerModuleType" + ] + }, + { + "id": "5", + "areaType": "slot", + "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "matingSurfaceUnitVector": [-1, 1, -1], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Slot 5", + "compatibleModuleTypes": [ + "magneticModuleType", + "temperatureModuleType", + "heaterShakerModuleType" + ] + }, + { + "id": "6", + "areaType": "slot", + "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "matingSurfaceUnitVector": [-1, 1, -1], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Slot 6", + "compatibleModuleTypes": [ + "magneticModuleType", + "temperatureModuleType", + "heaterShakerModuleType" + ] + }, + { + "id": "7", + "areaType": "slot", + "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "matingSurfaceUnitVector": [-1, 1, -1], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Slot 7", + "compatibleModuleTypes": [ + "magneticModuleType", + "temperatureModuleType", + "thermocyclerModuleType", + "heaterShakerModuleType" + ] + }, + { + "id": "8", + "areaType": "slot", + "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "matingSurfaceUnitVector": [-1, 1, -1], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Slot 8", + "compatibleModuleTypes": [ + "magneticModuleType", + "temperatureModuleType", + "heaterShakerModuleType" + ] + }, + { + "id": "9", + "areaType": "slot", + "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "matingSurfaceUnitVector": [-1, 1, -1], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Slot 9", + "compatibleModuleTypes": [ + "magneticModuleType", + "temperatureModuleType", + "heaterShakerModuleType" + ] + }, + { + "id": "10", + "areaType": "slot", + "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "matingSurfaceUnitVector": [-1, 1, -1], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Slot 10", + "compatibleModuleTypes": [ + "magneticModuleType", + "temperatureModuleType", + "heaterShakerModuleType" + ] + }, + { + "id": "11", + "areaType": "slot", + "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "matingSurfaceUnitVector": [-1, 1, -1], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Slot 11", + "compatibleModuleTypes": [ + "magneticModuleType", + "temperatureModuleType", + "heaterShakerModuleType" + ] + }, + { + "id": "12", + "areaType": "slot", + "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Slot 12", + "compatibleModuleTypes": [] + }, + { + "id": "fixedTrash", + "areaType": "fixedTrash", + "offsetFromCutoutFixture": [29.285, -2.835, 0], + "boundingBox": { + "xDimension": 107.11, + "yDimension": 165.67, + "zDimension": 82 + }, + "displayName": "Slot 12/Fixed Trash", + "ableToDropTips": true + } + ], + "cutouts": [ + { + "id": "cutout1", + "position": [0.0, 0.0, 0.0], + "displayName": "Cutout 1" + }, + { + "id": "cutout2", + "position": [132.5, 0.0, 0.0], + "displayName": "Cutout 2" + }, + { + "id": "cutout3", + "position": [265.0, 0.0, 0.0], + "displayName": "Cutout 3" + }, + { + "id": "cutout4", + "position": [0.0, 90.5, 0.0], + "displayName": "Cutout 4" + }, + { + "id": "cutout5", + "position": [132.5, 90.5, 0.0], + "displayName": "Cutout 5" + }, + { + "id": "cutout6", + "position": [265.0, 90.5, 0.0], + "displayName": "Cutout 6" + }, + { + "id": "cutout7", + "position": [0.0, 181.0, 0.0], + "displayName": "Cutout 7" + }, + { + "id": "cutout8", + "position": [132.5, 181.0, 0.0], + "displayName": "Cutout 8" + }, + { + "id": "cutout9", + "position": [265.0, 181.0, 0.0], + "displayName": "Cutout 9" + }, + { + "id": "cutout10", + "position": [0.0, 271.5, 0.0], + "displayName": "Slot 10" + }, + { + "id": "cutout11", + "position": [132.5, 271.5, 0.0], + "displayName": "Cutout 11" + }, + { + "id": "cutout12", + "position": [265.0, 271.5, 0.0], + "displayName": "Cutout 12" + } + ], + "calibrationPoints": [ + { + "id": "1BLC", + "position": [12.13, 9.0, 0.0], + "displayName": "Slot 1 Bottom Left Cross" + }, + { + "id": "3BRC", + "position": [380.87, 9.0, 0.0], + "displayName": "Slot 3 Bottom Right Cross" + }, + { + "id": "7TLC", + "position": [12.13, 258.0, 0.0], + "displayName": "Slot 7 Top Left Cross" + }, + { + "id": "9TRC", + "position": [380.87, 258.0, 0.0], + "displayName": "Slot 9 Top Right Cross" + }, + { + "id": "10TLC", + "position": [12.13, 348.5, 0.0], + "displayName": "Slot 10 Top Left Cross" + }, + { + "id": "11TRC", + "position": [248.37, 348.5, 0.0], + "displayName": "Slot 11 Top Right Cross" + }, + { + "id": "1BLD", + "position": [12.13, 6.0, 0.0], + "displayName": "Slot 1 Bottom Left Dot" + }, + { + "id": "3BRD", + "position": [380.87, 6.0, 0.0], + "displayName": "Slot 3 Bottom Right Dot" + }, + { + "id": "7TLD", + "position": [12.13, 261.0, 0.0], + "displayName": "Slot 7 Top Left Dot" + }, + { + "id": "9TRD", + "position": [380.87, 261.0, 0.0], + "displayName": "Slot 9 Top Right Dot" + }, + { + "id": "10TLD", + "position": [12.13, 351.5, 0.0], + "displayName": "Slot 10 Top Left Dot" + }, + { + "id": "11TRD", + "position": [248.37, 351.5, 0.0], + "displayName": "Slot 11 Top Right Dot" + } + ], + "legacyFixtures": [ + { + "id": "fixedTrash", + "slot": "12", + "labware": "opentrons_1_trash_1100ml_fixed", + "displayName": "Fixed Trash" + } + ] + }, + "cutoutFixtures": [ + { + "id": "singleStandardSlot", + "expectOpentronsModuleSerialNumber": false, + "mayMountTo": [ + "cutout1", + "cutout2", + "cutout3", + "cutout4", + "cutout5", + "cutout6", + "cutout7", + "cutout8", + "cutout9", + "cutout10", + "cutout11", + "cutout12" + ], + "displayName": "Standard Slot", + "providesAddressableAreas": { + "cutout1": ["1"], + "cutout2": ["2"], + "cutout3": ["3"], + "cutout4": ["4"], + "cutout5": ["5"], + "cutout6": ["6"], + "cutout7": ["7"], + "cutout8": ["8"], + "cutout9": ["9"], + "cutout10": ["10"], + "cutout11": ["11"], + "cutout12": ["12"] + }, + "fixtureGroup": {}, + "height": 0 + }, + { + "id": "fixedTrashSlot", + "expectOpentronsModuleSerialNumber": false, + "mayMountTo": ["cutout12"], + "displayName": "Fixed Trash", + "providesAddressableAreas": { + "cutout12": ["fixedTrash"] + }, + "fixtureGroup": {}, + "height": 82 + } + ] +} diff --git a/shared-data/deck/definitions/5/ot3_standard.json b/shared-data/deck/definitions/5/ot3_standard.json new file mode 100644 index 00000000000..85dcbf64792 --- /dev/null +++ b/shared-data/deck/definitions/5/ot3_standard.json @@ -0,0 +1,1042 @@ +{ + "otId": "ot3_standard", + "schemaVersion": 5, + "cornerOffsetFromOrigin": [-204.31, -76.59, 0], + "dimensions": [854.995, 581.74, 0], + "metadata": { + "displayName": "OT-3 Standard Deck", + "tags": ["ot3", "12 slots", "standard"] + }, + "robot": { + "model": "OT-3 Standard" + }, + "locations": { + "addressableAreas": [ + { + "id": "D1", + "areaType": "slot", + "matingSurfaceUnitVector": [-1, 1, -1], + "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Slot D1" + }, + { + "id": "D2", + "areaType": "slot", + "matingSurfaceUnitVector": [-1, 1, -1], + "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Slot D2" + }, + { + "id": "D3", + "areaType": "slot", + "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "matingSurfaceUnitVector": [-1, 1, -1], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Slot D3" + }, + { + "id": "C1", + "areaType": "slot", + "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "matingSurfaceUnitVector": [-1, 1, -1], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Slot C1" + }, + { + "id": "C2", + "areaType": "slot", + "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "matingSurfaceUnitVector": [-1, 1, -1], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Slot C2" + }, + { + "id": "C3", + "areaType": "slot", + "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "matingSurfaceUnitVector": [-1, 1, -1], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Slot C3" + }, + { + "id": "B1", + "areaType": "slot", + "offsetFromCutoutFixture": [0.0, 0, 0.0], + "matingSurfaceUnitVector": [-1, 1, -1], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Slot B1" + }, + { + "id": "B2", + "areaType": "slot", + "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "matingSurfaceUnitVector": [-1, 1, -1], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Slot B2" + }, + { + "id": "B3", + "areaType": "slot", + "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "matingSurfaceUnitVector": [-1, 1, -1], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Slot B3" + }, + { + "id": "A1", + "areaType": "slot", + "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "matingSurfaceUnitVector": [-1, 1, -1], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Slot A1" + }, + { + "id": "A2", + "areaType": "slot", + "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "matingSurfaceUnitVector": [-1, 1, -1], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Slot A2", + "compatibleModuleTypes": [] + }, + { + "id": "A3", + "areaType": "slot", + "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Slot A3", + "compatibleModuleTypes": [] + }, + { + "id": "A4", + "areaType": "stagingSlot", + "offsetFromCutoutFixture": [164.0, 0.0, 14.5], + "matingSurfaceUnitVector": [-1, 1, -1], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Slot A4", + "compatibleModuleTypes": [] + }, + { + "id": "B4", + "areaType": "stagingSlot", + "offsetFromCutoutFixture": [164.0, 0.0, 14.5], + "matingSurfaceUnitVector": [-1, 1, -1], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Slot B4", + "compatibleModuleTypes": [] + }, + { + "id": "C4", + "areaType": "stagingSlot", + "offsetFromCutoutFixture": [164.0, 0.0, 14.5], + "matingSurfaceUnitVector": [-1, 1, -1], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Slot C4", + "compatibleModuleTypes": [] + }, + { + "id": "D4", + "areaType": "stagingSlot", + "offsetFromCutoutFixture": [164.0, 0.0, 14.5], + "matingSurfaceUnitVector": [-1, 1, -1], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Slot D4", + "compatibleModuleTypes": [] + }, + { + "id": "movableTrashD1", + "areaType": "movableTrash", + "offsetFromCutoutFixture": [-90.25, 4, 0.0], + "boundingBox": { + "xDimension": 225, + "yDimension": 78, + "zDimension": 40 + }, + "displayName": "Trash Bin in D1", + "ableToDropTips": true + }, + { + "id": "movableTrashC1", + "areaType": "movableTrash", + "offsetFromCutoutFixture": [-90.25, 4, 0.0], + "boundingBox": { + "xDimension": 225, + "yDimension": 78, + "zDimension": 40 + }, + "displayName": "Trash Bin in C1", + "ableToDropTips": true + }, + { + "id": "movableTrashB1", + "areaType": "movableTrash", + "offsetFromCutoutFixture": [-90.25, 4, 0.0], + "boundingBox": { + "xDimension": 225, + "yDimension": 78, + "zDimension": 40 + }, + "displayName": "Trash Bin in B1", + "ableToDropTips": true + }, + { + "id": "movableTrashA1", + "areaType": "movableTrash", + "offsetFromCutoutFixture": [-90.25, 4, 0.0], + "boundingBox": { + "xDimension": 225, + "yDimension": 78, + "zDimension": 40 + }, + "displayName": "Trash Bin in A1", + "ableToDropTips": true + }, + { + "id": "movableTrashD3", + "areaType": "movableTrash", + "offsetFromCutoutFixture": [-6.25, 4, 0.0], + "boundingBox": { + "xDimension": 225, + "yDimension": 78, + "zDimension": 40 + }, + "displayName": "Trash Bin in D3", + "ableToDropTips": true + }, + { + "id": "movableTrashC3", + "areaType": "movableTrash", + "offsetFromCutoutFixture": [-6.25, 4, 0.0], + "boundingBox": { + "xDimension": 225, + "yDimension": 78, + "zDimension": 40 + }, + "displayName": "Trash Bin in C3", + "ableToDropTips": true + }, + { + "id": "movableTrashB3", + "areaType": "movableTrash", + "offsetFromCutoutFixture": [-6.25, 4, 0.0], + "boundingBox": { + "xDimension": 225, + "yDimension": 78, + "zDimension": 40 + }, + "displayName": "Trash Bin in B3", + "ableToDropTips": true + }, + { + "id": "movableTrashA3", + "areaType": "movableTrash", + "offsetFromCutoutFixture": [-6.25, 4, 0.0], + "boundingBox": { + "xDimension": 225, + "yDimension": 78, + "zDimension": 40 + }, + "displayName": "Trash Bin in A3", + "ableToDropTips": true + }, + { + "id": "1ChannelWasteChute", + "areaType": "wasteChute", + "offsetFromCutoutFixture": [64, 36, 114.5], + "boundingBox": { + "xDimension": 0, + "yDimension": 0, + "zDimension": 0 + }, + "displayName": "Waste Chute", + "ableToDropTips": true + }, + { + "id": "8ChannelWasteChute", + "areaType": "wasteChute", + "offsetFromCutoutFixture": [64, -27, 114.5], + "boundingBox": { + "xDimension": 0, + "yDimension": 63, + "zDimension": 0 + }, + "displayName": "Waste Chute", + "ableToDropTips": true + }, + { + "id": "96ChannelWasteChute", + "areaType": "wasteChute", + "offsetFromCutoutFixture": [14.445, -20.915, 114.5], + "boundingBox": { + "xDimension": 99, + "yDimension": 63, + "zDimension": 0 + }, + "displayName": "Waste Chute", + "ableToDropTips": true + }, + { + "id": "gripperWasteChute", + "areaType": "wasteChute", + "offsetFromCutoutFixture": [64, 29, 136.5], + "boundingBox": { + "xDimension": 0, + "yDimension": 0, + "zDimension": 0 + }, + "displayName": "Waste Chute", + "ableToDropLabware": true + }, + { + "id": "thermocyclerModuleV2", + "areaType": "thermocycler", + "offsetFromCutoutFixture": [-20.005, 67.96, 10.96], + "matingSurfaceUnitVector": [-1, 1, -1], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Thermocycler Module Slot" + }, + { + "id": "heaterShakerV1D1", + "areaType": "heaterShaker", + "offsetFromCutoutFixture": [0, 0, 18.95], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Heater Shaker in D1" + }, + { + "id": "heaterShakerV1C1", + "areaType": "heaterShaker", + "offsetFromCutoutFixture": [0.0, 0.0, 18.95], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Heater Shaker in C1" + }, + { + "id": "heaterShakerV1B1", + "areaType": "heaterShaker", + "offsetFromCutoutFixture": [0.0, 0.0, 18.95], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Heater Shaker in B1" + }, + { + "id": "heaterShakerV1A1", + "areaType": "heaterShaker", + "offsetFromCutoutFixture": [0.0, 0.0, 18.95], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Heater Shaker in A1" + }, + { + "id": "heaterShakerV1D3", + "areaType": "heaterShaker", + "offsetFromCutoutFixture": [0.0, 0.0, 18.95], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Heater Shaker in D3" + }, + { + "id": "heaterShakerV1C3", + "areaType": "heaterShaker", + "offsetFromCutoutFixture": [0.0, 0.0, 18.95], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Heater Shaker in C3" + }, + { + "id": "heaterShakerV1B3", + "areaType": "heaterShaker", + "offsetFromCutoutFixture": [0.0, 0.0, 18.95], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Heater Shaker in B3" + }, + { + "id": "heaterShakerV1A3", + "areaType": "heaterShaker", + "offsetFromCutoutFixture": [0.0, 0.0, 18.95], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Heater Shaker in A3" + }, + { + "id": "temperatureModuleV2D1", + "areaType": "temperatureModule", + "offsetFromCutoutFixture": [0.0, 0.0, 9.0], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Temperature Module in D1" + }, + { + "id": "temperatureModuleV2C1", + "areaType": "temperatureModule", + "offsetFromCutoutFixture": [0.0, 0.0, 9.0], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Temperature Module in C1" + }, + { + "id": "temperatureModuleV2B1", + "areaType": "temperatureModule", + "offsetFromCutoutFixture": [0.0, 0.0, 9.0], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Temperature Module in B1" + }, + { + "id": "temperatureModuleV2A1", + "areaType": "temperatureModule", + "offsetFromCutoutFixture": [0.0, 0.0, 9.0], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Temperature Module in A1" + }, + { + "id": "temperatureModuleV2D3", + "areaType": "temperatureModule", + "offsetFromCutoutFixture": [0.0, 0.0, 9.0], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Temperature Module in D3" + }, + { + "id": "temperatureModuleV2C3", + "areaType": "temperatureModule", + "offsetFromCutoutFixture": [0.0, 0.0, 9.0], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Temperature Module in C3" + }, + { + "id": "temperatureModuleV2B3", + "areaType": "temperatureModule", + "offsetFromCutoutFixture": [0.0, 0.0, 9.0], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Temperature Module in B3" + }, + { + "id": "temperatureModuleV2A3", + "areaType": "temperatureModule", + "offsetFromCutoutFixture": [0.0, 0.0, 9.0], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Temperature Module in A3" + }, + { + "id": "magneticBlockV1D1", + "areaType": "magneticBlock", + "offsetFromCutoutFixture": [0.0, 0.0, 38], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Magnetic Block in D1" + }, + { + "id": "magneticBlockV1C1", + "areaType": "magneticBlock", + "offsetFromCutoutFixture": [0.0, 0.0, 38], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Magnetic Block in C1" + }, + { + "id": "magneticBlockV1B1", + "areaType": "magneticBlock", + "offsetFromCutoutFixture": [0.0, 0.0, 38], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Magnetic Block in B1" + }, + { + "id": "magneticBlockV1A1", + "areaType": "magneticBlock", + "offsetFromCutoutFixture": [0.0, 0.0, 38], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Magnetic Block in A1" + }, + { + "id": "magneticBlockV1D2", + "areaType": "magneticBlock", + "offsetFromCutoutFixture": [0.0, 0.0, 38], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Magnetic Block in D2" + }, + { + "id": "magneticBlockV1C2", + "areaType": "magneticBlock", + "offsetFromCutoutFixture": [0.0, 0.0, 38], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Magnetic Block in C2" + }, + { + "id": "magneticBlockV1B2", + "areaType": "magneticBlock", + "offsetFromCutoutFixture": [0.0, 0.0, 38], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Magnetic Block in B2" + }, + { + "id": "magneticBlockV1A2", + "areaType": "magneticBlock", + "offsetFromCutoutFixture": [0.0, 0.0, 38], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Magnetic Block in A2" + }, + { + "id": "magneticBlockV1D3", + "areaType": "magneticBlock", + "offsetFromCutoutFixture": [0.0, 0.0, 38], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Magnetic Block in D3" + }, + { + "id": "magneticBlockV1C3", + "areaType": "magneticBlock", + "offsetFromCutoutFixture": [0.0, 0.0, 38], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Magnetic Block in C3" + }, + { + "id": "magneticBlockV1B3", + "areaType": "magneticBlock", + "offsetFromCutoutFixture": [0.0, 0.0, 38], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Magnetic Block in B3" + }, + { + "id": "magneticBlockV1A3", + "areaType": "magneticBlock", + "offsetFromCutoutFixture": [0.0, 0.0, 38], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Magnetic Block in A3" + } + ], + "cutouts": [ + { + "id": "cutoutD1", + "position": [0.0, 0.0, 0.0], + "displayName": "Cutout D1" + }, + { + "id": "cutoutD2", + "position": [164.0, 0.0, 0.0], + "displayName": "Cutout D2" + }, + { + "id": "cutoutD3", + "position": [328.0, 0.0, 0.0], + "displayName": "Cutout D3" + }, + { + "id": "cutoutC1", + "position": [0.0, 107, 0.0], + "displayName": "Cutout C1" + }, + { + "id": "cutoutC2", + "position": [164.0, 107, 0.0], + "displayName": "Cutout C2" + }, + { + "id": "cutoutC3", + "position": [328.0, 107, 0.0], + "displayName": "Cutout C3" + }, + { + "id": "cutoutB1", + "position": [0.0, 214.0, 0.0], + "displayName": "Cutout B1" + }, + { + "id": "cutoutB2", + "position": [164.0, 214.0, 0.0], + "displayName": "Cutout B2" + }, + { + "id": "cutoutB3", + "position": [328.0, 214.0, 0.0], + "displayName": "Cutout B3" + }, + { + "id": "cutoutA1", + "position": [0.0, 321.0, 0.0], + "displayName": "Cutout A1" + }, + { + "id": "cutoutA2", + "position": [164.0, 321.0, 0.0], + "displayName": "Cutout A2" + }, + { + "id": "cutoutA3", + "position": [328.0, 321.0, 0.0], + "displayName": "Cutout A3" + } + ], + "calibrationPoints": [], + "legacyFixtures": [ + { + "id": "fixedTrash", + "slot": "A3", + "labware": "opentrons_1_trash_3200ml_fixed", + "displayName": "Fixed Trash" + } + ] + }, + "cutoutFixtures": [ + { + "id": "singleLeftSlot", + "expectOpentronsModuleSerialNumber": false, + "mayMountTo": ["cutoutD1", "cutoutC1", "cutoutB1", "cutoutA1"], + "displayName": "Standard Slot Left", + "providesAddressableAreas": { + "cutoutD1": ["D1"], + "cutoutC1": ["C1"], + "cutoutB1": ["B1"], + "cutoutA1": ["A1"] + }, + "fixtureGroup": {}, + "height": 0 + }, + { + "id": "singleCenterSlot", + "expectOpentronsModuleSerialNumber": false, + "mayMountTo": ["cutoutD2", "cutoutC2", "cutoutB2", "cutoutA2"], + "displayName": "Standard Slot Center", + "providesAddressableAreas": { + "cutoutD2": ["D2"], + "cutoutC2": ["C2"], + "cutoutB2": ["B2"], + "cutoutA2": ["A2"] + }, + "fixtureGroup": {}, + "height": 0 + }, + { + "id": "singleRightSlot", + "expectOpentronsModuleSerialNumber": false, + "mayMountTo": ["cutoutD3", "cutoutC3", "cutoutB3", "cutoutA3"], + "displayName": "Standard Slot Right", + "providesAddressableAreas": { + "cutoutD3": ["D3"], + "cutoutC3": ["C3"], + "cutoutB3": ["B3"], + "cutoutA3": ["A3"] + }, + "fixtureGroup": {}, + "height": 0 + }, + { + "id": "stagingAreaRightSlot", + "expectOpentronsModuleSerialNumber": false, + "mayMountTo": ["cutoutD3", "cutoutC3", "cutoutB3", "cutoutA3"], + "displayName": "Staging Area Slot", + "providesAddressableAreas": { + "cutoutD3": ["D3", "D4"], + "cutoutC3": ["C3", "C4"], + "cutoutB3": ["B3", "B4"], + "cutoutA3": ["A3", "A4"] + }, + "fixtureGroup": {}, + "height": 0 + }, + { + "id": "trashBinAdapter", + "expectOpentronsModuleSerialNumber": false, + "mayMountTo": [ + "cutoutD1", + "cutoutC1", + "cutoutB1", + "cutoutA1", + "cutoutD3", + "cutoutC3", + "cutoutB3", + "cutoutA3" + ], + "displayName": "Slot With Movable Trash", + "providesAddressableAreas": { + "cutoutD1": ["movableTrashD1"], + "cutoutC1": ["movableTrashC1"], + "cutoutB1": ["movableTrashB1"], + "cutoutA1": ["movableTrashA1"], + "cutoutD3": ["movableTrashD3"], + "cutoutC3": ["movableTrashC3"], + "cutoutB3": ["movableTrashB3"], + "cutoutA3": ["movableTrashA3"] + }, + "fixtureGroup": {}, + "height": 40 + }, + { + "id": "wasteChuteRightAdapterCovered", + "expectOpentronsModuleSerialNumber": false, + "mayMountTo": ["cutoutD3"], + "displayName": "Waste Chute Adapter for 1 or 8 Channel Pipettes", + "providesAddressableAreas": { + "cutoutD3": ["1ChannelWasteChute", "8ChannelWasteChute"] + }, + "fixtureGroup": {}, + "height": 124.5 + }, + { + "id": "wasteChuteRightAdapterNoCover", + "expectOpentronsModuleSerialNumber": false, + "mayMountTo": ["cutoutD3"], + "displayName": "Waste Chute Adapter for 96 Channel Pipette or Gripper", + "providesAddressableAreas": { + "cutoutD3": [ + "1ChannelWasteChute", + "8ChannelWasteChute", + "96ChannelWasteChute", + "gripperWasteChute" + ] + }, + "fixtureGroup": {}, + "height": 124.5 + }, + { + "id": "stagingAreaSlotWithWasteChuteRightAdapterCovered", + "expectOpentronsModuleSerialNumber": false, + "mayMountTo": ["cutoutD3"], + "displayName": "Staging Slot With Waste Chute Adapter for 96 Channel Pipette or Gripper", + "providesAddressableAreas": { + "cutoutD3": ["1ChannelWasteChute", "8ChannelWasteChute", "D4"] + }, + "fixtureGroup": {}, + "height": 124.5 + }, + { + "id": "stagingAreaSlotWithWasteChuteRightAdapterNoCover", + "expectOpentronsModuleSerialNumber": false, + "mayMountTo": ["cutoutD3"], + "displayName": "Staging Slot With Waste Chute Adapter and Staging Area Slot", + "providesAddressableAreas": { + "cutoutD3": [ + "1ChannelWasteChute", + "8ChannelWasteChute", + "96ChannelWasteChute", + "gripperWasteChute", + "D4" + ] + }, + "fixtureGroup": {}, + "height": 124.5 + }, + { + "id": "thermocyclerModuleV2Rear", + "expectOpentronsModuleSerialNumber": true, + "mayMountTo": ["cutoutA1"], + "displayName": "Rear Slot portion of the Thermocycler Moduler", + "providesAddressableAreas": { + "cutoutA1": [] + }, + "fixtureGroup": { + "cutoutA1": [ + { + "cutoutA1": "thermocyclerModuleV2Rear", + "cutoutB1": "thermocyclerModuleV2Front" + } + ] + }, + "height": 72.35 + }, + { + "id": "thermocyclerModuleV2Front", + "expectOpentronsModuleSerialNumber": true, + "mayMountTo": ["cutoutB1"], + "displayName": "Front Slot portion of the Thermocycler Moduler", + "providesAddressableAreas": { + "cutoutB1": ["thermocyclerModuleV2"] + }, + "fixtureGroup": { + "cutoutB1": [ + { + "cutoutA1": "thermocyclerModuleV2Rear", + "cutoutB1": "thermocyclerModuleV2Front" + } + ] + }, + "height": 72.35 + }, + { + "id": "heaterShakerModuleV1", + "expectOpentronsModuleSerialNumber": true, + "mayMountTo": [ + "cutoutD1", + "cutoutC1", + "cutoutB1", + "cutoutA1", + "cutoutD3", + "cutoutC3", + "cutoutB3", + "cutoutA3" + ], + "displayName": "Slot With a Heater Shaker", + "providesAddressableAreas": { + "cutoutD1": ["heaterShakerV1D1"], + "cutoutC1": ["heaterShakerV1C1"], + "cutoutB1": ["heaterShakerV1B1"], + "cutoutA1": ["heaterShakerV1A1"], + "cutoutD3": ["heaterShakerV1D3"], + "cutoutC3": ["heaterShakerV1C3"], + "cutoutB3": ["heaterShakerV1B3"], + "cutoutA3": ["heaterShakerV1A3"] + }, + "fixtureGroup": {}, + "height": 18.95 + }, + { + "id": "temperatureModuleV2", + "expectOpentronsModuleSerialNumber": true, + "mayMountTo": [ + "cutoutD1", + "cutoutC1", + "cutoutB1", + "cutoutA1", + "cutoutD3", + "cutoutC3", + "cutoutB3", + "cutoutA3" + ], + "displayName": "Slot With a Temperature Module", + "providesAddressableAreas": { + "cutoutD1": ["temperatureModuleV2D1"], + "cutoutC1": ["temperatureModuleV2C1"], + "cutoutB1": ["temperatureModuleV2B1"], + "cutoutA1": ["temperatureModuleV2A1"], + "cutoutD3": ["temperatureModuleV2D3"], + "cutoutC3": ["temperatureModuleV2C3"], + "cutoutB3": ["temperatureModuleV2B3"], + "cutoutA3": ["temperatureModuleV2A3"] + }, + "fixtureGroup": {}, + "height": 9.0 + }, + { + "id": "magneticBlockV1", + "expectOpentronsModuleSerialNumber": false, + "mayMountTo": [ + "cutoutD1", + "cutoutC1", + "cutoutB1", + "cutoutA1", + "cutoutD2", + "cutoutC2", + "cutoutB2", + "cutoutA2", + "cutoutD3", + "cutoutC3", + "cutoutB3", + "cutoutA3" + ], + "displayName": "Slot With a Magnetic Block", + "providesAddressableAreas": { + "cutoutD1": ["magneticBlockV1D1"], + "cutoutC1": ["magneticBlockV1C1"], + "cutoutB1": ["magneticBlockV1B1"], + "cutoutA1": ["magneticBlockV1A1"], + "cutoutD2": ["magneticBlockV1D2"], + "cutoutC2": ["magneticBlockV1C2"], + "cutoutB2": ["magneticBlockV1B2"], + "cutoutA2": ["magneticBlockV1A2"], + "cutoutD3": ["magneticBlockV1D3"], + "cutoutC3": ["magneticBlockV1C3"], + "cutoutB3": ["magneticBlockV1B3"], + "cutoutA3": ["magneticBlockV1A3"] + }, + "fixtureGroup": {}, + "height": 38.0 + }, + { + "id": "stagingAreaSlotWithMagneticBlockV1", + "expectOpentronsModuleSerialNumber": false, + "mayMountTo": ["cutoutD3", "cutoutC3", "cutoutB3", "cutoutA3"], + "displayName": "Fixture that provides a Magnetic Block and a Staging Area Slot", + "providesAddressableAreas": { + "cutoutD3": ["magneticBlockV1D3", "D4"], + "cutoutC3": ["magneticBlockV1C3", "C4"], + "cutoutB3": ["magneticBlockV1B3", "B4"], + "cutoutA3": ["magneticBlockV1A3", "A4"] + }, + "fixtureGroup": {}, + "height": 38.0 + } + ], + "gripperOffsets": { + "default": { + "pickUpOffset": { + "x": 0, + "y": 0, + "z": 0 + }, + "dropOffset": { + "x": 0, + "y": 0, + "z": -0.75 + } + } + } +} diff --git a/shared-data/deck/index.ts b/shared-data/deck/index.ts index e308d7a17ad..7d68bdeebd9 100644 --- a/shared-data/deck/index.ts +++ b/shared-data/deck/index.ts @@ -8,11 +8,16 @@ import ot2StandardDeckV4 from './definitions/4/ot2_standard.json' import ot2ShortFixedTrashDeckV4 from './definitions/4/ot2_short_trash.json' import ot3StandardDeckV4 from './definitions/4/ot3_standard.json' +// v5 deck defs +import ot2StandardDeckV5 from './definitions/5/ot2_standard.json' +import ot2ShortFixedTrashDeckV5 from './definitions/5/ot2_short_trash.json' +import ot3StandardDeckV5 from './definitions/5/ot3_standard.json' + import deckExample from './fixtures/3/deckExample.json' import type { DeckDefinition } from '../js/types' -export * from './types/schemaV4' +export * from './types/schemaV5' export { ot2StandardDeckV3, @@ -21,13 +26,16 @@ export { ot2StandardDeckV4, ot2ShortFixedTrashDeckV4, ot3StandardDeckV4, + ot2StandardDeckV5, + ot2ShortFixedTrashDeckV5, + ot3StandardDeckV5, deckExample, } const latestDeckDefinitions = { - ot2StandardDeckV4, - ot2ShortFixedTrashDeckV4, - ot3StandardDeckV4, + ot2StandardDeckV5, + ot2ShortFixedTrashDeckV5, + ot3StandardDeckV5, } export function getDeckDefinitions(): Record { diff --git a/shared-data/deck/schemas/5.json b/shared-data/deck/schemas/5.json new file mode 100644 index 00000000000..da77152da13 --- /dev/null +++ b/shared-data/deck/schemas/5.json @@ -0,0 +1,338 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "opentronsDeckSchemaV5", + "definitions": { + "positiveNumber": { + "type": "number", + "minimum": 0 + }, + "xyzArray": { + "type": "array", + "description": "Array of 3 numbers, [x, y, z]", + "items": { "type": "number" }, + "minItems": 3, + "maxItems": 3 + }, + "coordinates": { + "type": "object", + "additionalProperties": false, + "required": ["x", "y", "z"], + "properties": { + "x": { + "type": "number" + }, + "y": { + "type": "number" + }, + "z": { + "type": "number" + } + } + }, + "unitVector": { + "type": "array", + "description": "Array of 3 unit directions, [x, y, z]", + "items": { + "type": "number", + "enum": [1, -1] + }, + "minItems": 3, + "maxItems": 3 + }, + "boundingBox": { + "type": "object", + "required": ["xDimension", "yDimension", "zDimension"], + "properties": { + "xDimension": { "$ref": "#/definitions/positiveNumber" }, + "yDimension": { "$ref": "#/definitions/positiveNumber" }, + "zDimension": { "$ref": "#/definitions/positiveNumber" } + } + } + }, + "description": "Deck specifications, where x,y,z (0,0,0) is at front the bottom left corner.", + "type": "object", + "additionalProperties": false, + "required": [ + "otId", + "schemaVersion", + "cornerOffsetFromOrigin", + "dimensions", + "metadata", + "robot", + "locations", + "cutoutFixtures" + ], + "properties": { + "otId": { + "description": "Unique internal ID generated using UUID", + "type": "string" + }, + "schemaVersion": { + "description": "Schema version of a deck is a single integer", + "enum": [5] + }, + "cornerOffsetFromOrigin": { + "$ref": "#/definitions/xyzArray", + "description": "Position of left-front-bottom corner of entire deck to robot coordinate system origin" + }, + "dimensions": { + "$ref": "#/definitions/xyzArray", + "description": "Outer dimensions of a deck bounding box" + }, + "metadata": { + "description": "Optional metadata about the Deck", + "type": "object", + "properties": { + "displayName": { + "description": "A short, human-readable name for the deck", + "type": "string" + }, + "tags": { + "description": "Tags to be used in searching for this deck", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "robot": { + "type": "object", + "required": ["model"], + "properties": { + "model": { + "description": "Model of the robot", + "type": "string", + "enum": ["OT-2 Standard", "OT-3 Standard"] + } + } + }, + "locations": { + "type": "object", + "required": [ + "addressableAreas", + "calibrationPoints", + "cutouts", + "legacyFixtures" + ], + "properties": { + "addressableAreas": { + "type": "array", + "items": { + "type": "object", + "description": "An addressable area is a named area in 3D space that the robot can interact with--for example, as a place to drop tips, or hold a labware.", + "required": [ + "id", + "areaType", + "offsetFromCutoutFixture", + "boundingBox", + "displayName" + ], + "properties": { + "id": { + "description": "Unique identifier for slot", + "type": "string" + }, + "areaType": { + "description": "The type of addressable area, defining allowed behavior.", + "type": "string", + "enum": [ + "slot", + "stagingSlot", + "movableTrash", + "fixedTrash", + "wasteChute" + ] + }, + "offsetFromCutoutFixture": { + "$ref": "#/definitions/xyzArray", + "description": "The offset from the origin of the cutout fixture that's providing this addressable area (which is currently identical to the position of the underlying cutout), to the -x, -y, -z corner of this addressable area's bounding box." + }, + "matingSurfaceUnitVector": { + "$ref": "#/definitions/unitVector", + "description": "An optional diagonal direction of force, defined by spring location, which governs the mating surface of objects placed in this addressable area. Meant to be used when this addressable area is a slot." + }, + "boundingBox": { + "description": "The active area (both pipettes can reach) of this addressable area.", + "$ref": "#/definitions/boundingBox" + }, + "displayName": { + "description": "A human-readable nickname for this area e.g. \"Slot A1\" or \"Trash Bin in A1\"", + "type": "string" + }, + "compatibleModuleTypes": { + "description": "OT-2 Only parameter. An array of module types that can be placed in this area. The module type names can be found in the moduleType field of a module definition.", + "type": "array", + "items": { + "type": "string" + } + }, + "ableToDropTips": { + "description": "Whether tips are allowed to be dropped into this area. If `true`, the top-center of the `boundingBox` should be a good location for the bottom-center of all the tips when they're dropped.", + "type": "boolean" + }, + "ableToDropLabware": { + "description": "Whether labware is allowed to be dropped (different from being placed) into this area. If `true`, the top-center of the `boundingBox` should be a good location for the bottom-center of the labware when it's dropped.", + "type": "boolean" + } + } + } + }, + "calibrationPoints": { + "type": "array", + "description": "Key points for deck calibration", + "items": { + "type": "object", + "required": ["id", "position", "displayName"], + "properties": { + "id": { + "description": "Unique identifier for calibration point", + "type": "string" + }, + "position": { + "$ref": "#/definitions/xyzArray" + }, + "displayName": { + "description": "An optional human-readable nickname for this point Eg \"Slot 3 Cross\" or \"Slot 1 Dot\"", + "type": "string" + } + } + } + }, + "cutouts": { + "type": "array", + "description": "The machined cutout slots on the deck surface.", + "items": { + "type": "object", + "required": ["id", "position", "displayName"], + "properties": { + "id": { + "description": "Unique identifier for the cutout", + "type": "string" + }, + "position": { + "description": "Absolute position of the cutout", + "$ref": "#/definitions/xyzArray" + }, + "displayName": { + "description": "An optional human-readable nickname for this cutout e.g. \"Cutout A1\"", + "type": "string" + } + } + } + }, + "legacyFixtures": { + "type": "array", + "description": "Fixed position objects on the deck.", + "items": { + "type": "object", + "required": ["id", "displayName"], + "properties": { + "id": { + "description": "Unique identifier for fixed object", + "type": "string" + }, + "labware": { + "description": "Valid labware loadName for fixed object", + "type": "string" + }, + "slot": { + "description": "Slot location of the fixed object", + "type": "string" + }, + "displayName": { + "description": "An optional human-readable nickname for this fixture Eg \"Tall Fixed Trash\" or \"Short Fixed Trash\"", + "type": "string" + } + } + } + } + } + }, + "cutoutFixtures": { + "type": "array", + "items": { + "description": "A cutout fixture is a physical thing that can be mounted onto one of the deck cutouts.", + "type": "object", + "required": [ + "id", + "expectOpentronsModuleSerialNumber", + "mayMountTo", + "displayName", + "providesAddressableAreas", + "fixtureGroup", + "height" + ], + "properties": { + "id": { + "description": "Unique identifier for the cutout fixture.", + "type": "string" + }, + "expectOpentronsModuleSerialNumber": { + "description": "Determines whether or not a fixture expects a serial number for a connected Opentrons Module.", + "type": "boolean" + }, + "mayMountTo": { + "description": "A list of compatible cutouts this fixture may be mounted to. These must match `id`s in `cutouts`.", + "type": "array", + "items": { + "type": "string" + } + }, + "displayName": { + "description": "A human-readable nickname for this area e.g. \"Standard Right Slot\" or \"Slot With Movable Trash\"", + "type": "string" + }, + "providesAddressableAreas": { + "description": "The addressable areas that this cutout fixture provides, when it's mounted. It can provide different addressable areas depending on where it's mounted. Keys must match values from this object's `mayMountTo`. Values must match `id`s from `addressableAreas`.", + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "fixtureGroup": { + "description": "The map of fixtures that must exist in the deck configuration if this fixture exists, with the mounting location acting as a key to determine the location of the rest of the group.", + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + }, + "height": { + "description": "The vertical distance (mm) from the cutout fixture's origin to its tallest physical feature that an instrument could collide with.", + "type": "number" + } + } + } + }, + "gripperOffsets": { + "type": "object", + "description": "Offsets to be added when calculating the coordinates a gripper should go to when picking up or dropping a labware on this deck.", + "properties": { + "default": { + "type": "object", + "properties": { + "pickUpOffset": { + "$ref": "#/definitions/coordinates", + "description": "Offset added to calculate pick-up coordinates of a labware placed on this deck." + }, + "dropOffset": { + "$ref": "#/definitions/coordinates", + "description": "Offset added to calculate drop coordinates of a labware placed on this deck." + } + }, + "required": ["pickUpOffset", "dropOffset"] + } + }, + "required": ["default"] + } + } +} diff --git a/shared-data/deck/types/schemaV5.ts b/shared-data/deck/types/schemaV5.ts new file mode 100644 index 00000000000..e763b893bde --- /dev/null +++ b/shared-data/deck/types/schemaV5.ts @@ -0,0 +1,141 @@ +export type FlexAddressableAreaName = + | 'A1' + | 'B1' + | 'C1' + | 'D1' + | 'A2' + | 'B2' + | 'C2' + | 'D2' + | 'A3' + | 'B3' + | 'C3' + | 'D3' + | 'A4' + | 'B4' + | 'C4' + | 'D4' + | 'movableTrashA1' + | 'movableTrashB1' + | 'movableTrashC1' + | 'movableTrashD1' + | 'movableTrashA3' + | 'movableTrashB3' + | 'movableTrashC3' + | 'movableTrashD3' + | '1ChannelWasteChute' + | '8ChannelWasteChute' + | '96ChannelWasteChute' + | 'gripperWasteChute' + | 'thermocyclerModuleV2' + | 'heaterShakerV1A1' + | 'heaterShakerV1B1' + | 'heaterShakerV1C1' + | 'heaterShakerV1D1' + | 'heaterShakerV1A3' + | 'heaterShakerV1B3' + | 'heaterShakerV1C3' + | 'heaterShakerV1D3' + | 'temperatureModuleV2A1' + | 'temperatureModuleV2B1' + | 'temperatureModuleV2C1' + | 'temperatureModuleV2D1' + | 'temperatureModuleV2A3' + | 'temperatureModuleV2B3' + | 'temperatureModuleV2C3' + | 'temperatureModuleV2D3' + | 'magneticBlockV1A1' + | 'magneticBlockV1B1' + | 'magneticBlockV1C1' + | 'magneticBlockV1D1' + | 'magneticBlockV1A2' + | 'magneticBlockV1B2' + | 'magneticBlockV1C2' + | 'magneticBlockV1D2' + | 'magneticBlockV1A3' + | 'magneticBlockV1B3' + | 'magneticBlockV1C3' + | 'magneticBlockV1D3' + +export type OT2AddressableAreaName = + | '1' + | '2' + | '3' + | '4' + | '5' + | '6' + | '7' + | '8' + | '9' + | '10' + | '11' + | '12' + | 'fixedTrash' + +export type AddressableAreaName = + | FlexAddressableAreaName + | OT2AddressableAreaName + +export type CutoutId = + | 'cutoutD1' + | 'cutoutD2' + | 'cutoutD3' + | 'cutoutC1' + | 'cutoutC2' + | 'cutoutC3' + | 'cutoutB1' + | 'cutoutB2' + | 'cutoutB3' + | 'cutoutA1' + | 'cutoutA2' + | 'cutoutA3' + +export type OT2CutoutId = + | 'cutout1' + | 'cutout2' + | 'cutout3' + | 'cutout4' + | 'cutout5' + | 'cutout6' + | 'cutout7' + | 'cutout8' + | 'cutout9' + | 'cutout10' + | 'cutout11' + | 'cutout12' + +export type SingleSlotCutoutFixtureId = + | 'singleLeftSlot' + | 'singleCenterSlot' + | 'singleRightSlot' + +export type StagingAreaRightSlotFixtureId = 'stagingAreaRightSlot' + +export type TrashBinAdapterCutoutFixtureId = 'trashBinAdapter' + +export type WasteChuteCutoutFixtureId = + | 'wasteChuteRightAdapterCovered' + | 'wasteChuteRightAdapterNoCover' + | 'stagingAreaSlotWithWasteChuteRightAdapterCovered' + | 'stagingAreaSlotWithWasteChuteRightAdapterNoCover' + +export type FlexModuleCutoutFixtureId = + | 'heaterShakerModuleV1' + | 'temperatureModuleV2' + | 'magneticBlockV1' + | 'stagingAreaSlotWithMagneticBlockV1' + | 'thermocyclerModuleV2Rear' + | 'thermocyclerModuleV2Front' + +export type OT2SingleStandardSlot = 'singleStandardSlot' + +export type OT2FixedTrashSlot = 'fixedTrashSlot' + +export type CutoutFixtureId = + | SingleSlotCutoutFixtureId + | StagingAreaRightSlotFixtureId + | TrashBinAdapterCutoutFixtureId + | WasteChuteCutoutFixtureId + | FlexModuleCutoutFixtureId + | OT2SingleStandardSlot + | OT2FixedTrashSlot diff --git a/shared-data/js/constants.ts b/shared-data/js/constants.ts index 1b944418e0e..aaef2eb2430 100644 --- a/shared-data/js/constants.ts +++ b/shared-data/js/constants.ts @@ -1,5 +1,5 @@ import type { CutoutFixtureId, CutoutId, AddressableAreaName } from '../deck' -import type { ModuleType } from './types' +import type { ModuleModel, ModuleType } from './types' // constants for dealing with robot coordinate system (eg in labwareTools) export const SLOT_LENGTH_MM = 127.76 // along X axis in robot coordinate system @@ -230,6 +230,16 @@ export const STAGING_AREA_CUTOUTS: CutoutId[] = [ 'cutoutD3', ] +export const TEMPERATURE_MODULE_CUTOUTS: CutoutId[] = [ + ...SINGLE_RIGHT_CUTOUTS, + ...SINGLE_LEFT_CUTOUTS, +] +export const HEATER_SHAKER_CUTOUTS: CutoutId[] = [ + ...SINGLE_RIGHT_CUTOUTS, + ...SINGLE_LEFT_CUTOUTS, +] +export const THERMOCYCLER_MODULE_CUTOUTS: CutoutId[] = ['cutoutA1', 'cutoutB1'] + export const WASTE_CHUTE_CUTOUT: 'cutoutD3' = 'cutoutD3' export const A1_ADDRESSABLE_AREA: 'A1' = 'A1' @@ -275,6 +285,98 @@ export const NINETY_SIX_CHANNEL_WASTE_CHUTE_ADDRESSABLE_AREA: '96ChannelWasteChu export const GRIPPER_WASTE_CHUTE_ADDRESSABLE_AREA: 'gripperWasteChute' = 'gripperWasteChute' +export const THERMOCYCLER_ADDRESSABLE_AREA: 'thermocyclerModuleV2' = + 'thermocyclerModuleV2' +export const HEATERSHAKER_A1_ADDRESSABLE_AREA: 'heaterShakerV1A1' = + 'heaterShakerV1A1' +export const HEATERSHAKER_B1_ADDRESSABLE_AREA: 'heaterShakerV1B1' = + 'heaterShakerV1B1' +export const HEATERSHAKER_C1_ADDRESSABLE_AREA: 'heaterShakerV1C1' = + 'heaterShakerV1C1' +export const HEATERSHAKER_D1_ADDRESSABLE_AREA: 'heaterShakerV1D1' = + 'heaterShakerV1D1' +export const HEATERSHAKER_A3_ADDRESSABLE_AREA: 'heaterShakerV1A3' = + 'heaterShakerV1A3' +export const HEATERSHAKER_B3_ADDRESSABLE_AREA: 'heaterShakerV1B3' = + 'heaterShakerV1B3' +export const HEATERSHAKER_C3_ADDRESSABLE_AREA: 'heaterShakerV1C3' = + 'heaterShakerV1C3' +export const HEATERSHAKER_D3_ADDRESSABLE_AREA: 'heaterShakerV1D3' = + 'heaterShakerV1D3' +export const TEMPERATURE_MODULE_A1_ADDRESSABLE_AREA: 'temperatureModuleV2A1' = + 'temperatureModuleV2A1' +export const TEMPERATURE_MODULE_B1_ADDRESSABLE_AREA: 'temperatureModuleV2B1' = + 'temperatureModuleV2B1' +export const TEMPERATURE_MODULE_C1_ADDRESSABLE_AREA: 'temperatureModuleV2C1' = + 'temperatureModuleV2C1' +export const TEMPERATURE_MODULE_D1_ADDRESSABLE_AREA: 'temperatureModuleV2D1' = + 'temperatureModuleV2D1' +export const TEMPERATURE_MODULE_A3_ADDRESSABLE_AREA: 'temperatureModuleV2A3' = + 'temperatureModuleV2A3' +export const TEMPERATURE_MODULE_B3_ADDRESSABLE_AREA: 'temperatureModuleV2B3' = + 'temperatureModuleV2B3' +export const TEMPERATURE_MODULE_C3_ADDRESSABLE_AREA: 'temperatureModuleV2C3' = + 'temperatureModuleV2C3' +export const TEMPERATURE_MODULE_D3_ADDRESSABLE_AREA: 'temperatureModuleV2D3' = + 'temperatureModuleV2D3' + +export const MAGNETIC_BLOCK_A1_ADDRESSABLE_AREA: 'magneticBlockV1A1' = + 'magneticBlockV1A1' +export const MAGNETIC_BLOCK_B1_ADDRESSABLE_AREA: 'magneticBlockV1B1' = + 'magneticBlockV1B1' +export const MAGNETIC_BLOCK_C1_ADDRESSABLE_AREA: 'magneticBlockV1C1' = + 'magneticBlockV1C1' +export const MAGNETIC_BLOCK_D1_ADDRESSABLE_AREA: 'magneticBlockV1D1' = + 'magneticBlockV1D1' +export const MAGNETIC_BLOCK_A2_ADDRESSABLE_AREA: 'magneticBlockV1A2' = + 'magneticBlockV1A2' +export const MAGNETIC_BLOCK_B2_ADDRESSABLE_AREA: 'magneticBlockV1B2' = + 'magneticBlockV1B2' +export const MAGNETIC_BLOCK_C2_ADDRESSABLE_AREA: 'magneticBlockV1C2' = + 'magneticBlockV1C2' +export const MAGNETIC_BLOCK_D2_ADDRESSABLE_AREA: 'magneticBlockV1D2' = + 'magneticBlockV1D2' +export const MAGNETIC_BLOCK_A3_ADDRESSABLE_AREA: 'magneticBlockV1A3' = + 'magneticBlockV1A3' +export const MAGNETIC_BLOCK_B3_ADDRESSABLE_AREA: 'magneticBlockV1B3' = + 'magneticBlockV1B3' +export const MAGNETIC_BLOCK_C3_ADDRESSABLE_AREA: 'magneticBlockV1C3' = + 'magneticBlockV1C3' +export const MAGNETIC_BLOCK_D3_ADDRESSABLE_AREA: 'magneticBlockV1D3' = + 'magneticBlockV1D3' + +export const FLEX_MODULE_ADDRESSABLE_AREAS: AddressableAreaName[] = [ + THERMOCYCLER_ADDRESSABLE_AREA, + HEATERSHAKER_A1_ADDRESSABLE_AREA, + HEATERSHAKER_B1_ADDRESSABLE_AREA, + HEATERSHAKER_C1_ADDRESSABLE_AREA, + HEATERSHAKER_D1_ADDRESSABLE_AREA, + HEATERSHAKER_A3_ADDRESSABLE_AREA, + HEATERSHAKER_B3_ADDRESSABLE_AREA, + HEATERSHAKER_C3_ADDRESSABLE_AREA, + HEATERSHAKER_D3_ADDRESSABLE_AREA, + TEMPERATURE_MODULE_A1_ADDRESSABLE_AREA, + TEMPERATURE_MODULE_B1_ADDRESSABLE_AREA, + TEMPERATURE_MODULE_C1_ADDRESSABLE_AREA, + TEMPERATURE_MODULE_D1_ADDRESSABLE_AREA, + TEMPERATURE_MODULE_A3_ADDRESSABLE_AREA, + TEMPERATURE_MODULE_B3_ADDRESSABLE_AREA, + TEMPERATURE_MODULE_C3_ADDRESSABLE_AREA, + TEMPERATURE_MODULE_D3_ADDRESSABLE_AREA, + MAGNETIC_BLOCK_A1_ADDRESSABLE_AREA, + MAGNETIC_BLOCK_B1_ADDRESSABLE_AREA, + MAGNETIC_BLOCK_C1_ADDRESSABLE_AREA, + MAGNETIC_BLOCK_D1_ADDRESSABLE_AREA, + MAGNETIC_BLOCK_A2_ADDRESSABLE_AREA, + MAGNETIC_BLOCK_B2_ADDRESSABLE_AREA, + MAGNETIC_BLOCK_C2_ADDRESSABLE_AREA, + MAGNETIC_BLOCK_D2_ADDRESSABLE_AREA, + MAGNETIC_BLOCK_A3_ADDRESSABLE_AREA, + MAGNETIC_BLOCK_B3_ADDRESSABLE_AREA, + MAGNETIC_BLOCK_C3_ADDRESSABLE_AREA, + MAGNETIC_BLOCK_D3_ADDRESSABLE_AREA, +] + export const ADDRESSABLE_AREA_1: '1' = '1' export const ADDRESSABLE_AREA_2: '2' = '2' export const ADDRESSABLE_AREA_3: '3' = '3' @@ -359,6 +461,30 @@ export const STAGING_AREA_SLOT_WITH_WASTE_CHUTE_RIGHT_ADAPTER_COVERED_FIXTURE: ' export const STAGING_AREA_SLOT_WITH_WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE: 'stagingAreaSlotWithWasteChuteRightAdapterNoCover' = 'stagingAreaSlotWithWasteChuteRightAdapterNoCover' +export const HEATERSHAKER_MODULE_V1_FIXTURE: 'heaterShakerModuleV1' = + 'heaterShakerModuleV1' +export const TEMPERATURE_MODULE_V2_FIXTURE: 'temperatureModuleV2' = + 'temperatureModuleV2' +export const MAGNETIC_BLOCK_V1_FIXTURE: 'magneticBlockV1' = 'magneticBlockV1' +export const STAGING_AREA_SLOT_WITH_MAGNETIC_BLOCK_V1_FIXTURE: 'stagingAreaSlotWithMagneticBlockV1' = + 'stagingAreaSlotWithMagneticBlockV1' +export const THERMOCYCLER_V2_REAR_FIXTURE: 'thermocyclerModuleV2Rear' = + 'thermocyclerModuleV2Rear' +export const THERMOCYCLER_V2_FRONT_FIXTURE: 'thermocyclerModuleV2Front' = + 'thermocyclerModuleV2Front' + +export const MODULE_FIXTURES_BY_MODEL: { + [moduleModel in ModuleModel]?: CutoutFixtureId[] +} = { + [HEATERSHAKER_MODULE_V1]: [HEATERSHAKER_MODULE_V1_FIXTURE], + [TEMPERATURE_MODULE_V2]: [TEMPERATURE_MODULE_V2_FIXTURE], + [MAGNETIC_BLOCK_V1]: [MAGNETIC_BLOCK_V1_FIXTURE], + [THERMOCYCLER_MODULE_V2]: [ + THERMOCYCLER_V2_REAR_FIXTURE, + THERMOCYCLER_V2_FRONT_FIXTURE, + ], +} + export const SINGLE_SLOT_FIXTURES: CutoutFixtureId[] = [ SINGLE_LEFT_SLOT_FIXTURE, SINGLE_CENTER_SLOT_FIXTURE, diff --git a/shared-data/js/deck/index.ts b/shared-data/js/deck/index.ts index 786325be5a7..fce6ffb05fe 100644 --- a/shared-data/js/deck/index.ts +++ b/shared-data/js/deck/index.ts @@ -1,5 +1,5 @@ -import flexDeckDefV4 from '../../deck/definitions/4/ot3_standard.json' -import ot2DeckDefV4 from '../../deck/definitions/4/ot2_standard.json' -import ot2DeckDefShortFixedTrashV4 from '../../deck/definitions/4/ot2_short_trash.json' +import flexDeckDefV5 from '../../deck/definitions/5/ot3_standard.json' +import ot2DeckDefV5 from '../../deck/definitions/5/ot2_standard.json' +import ot2DeckDefShortFixedTrashV5 from '../../deck/definitions/5/ot2_short_trash.json' -export { ot2DeckDefV4, ot2DeckDefShortFixedTrashV4, flexDeckDefV4 } +export { ot2DeckDefV5, ot2DeckDefShortFixedTrashV5, flexDeckDefV5 } diff --git a/shared-data/js/fixtures.ts b/shared-data/js/fixtures.ts index 7e2f117bca8..42a46b84a9f 100644 --- a/shared-data/js/fixtures.ts +++ b/shared-data/js/fixtures.ts @@ -6,9 +6,57 @@ import { WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE, STAGING_AREA_SLOT_WITH_WASTE_CHUTE_RIGHT_ADAPTER_COVERED_FIXTURE, STAGING_AREA_SLOT_WITH_WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE, + A1_ADDRESSABLE_AREA, + A2_ADDRESSABLE_AREA, + A3_ADDRESSABLE_AREA, + B1_ADDRESSABLE_AREA, + B2_ADDRESSABLE_AREA, + B3_ADDRESSABLE_AREA, + C1_ADDRESSABLE_AREA, + C2_ADDRESSABLE_AREA, + C3_ADDRESSABLE_AREA, + D1_ADDRESSABLE_AREA, + D2_ADDRESSABLE_AREA, + D3_ADDRESSABLE_AREA, + ADDRESSABLE_AREA_1, + ADDRESSABLE_AREA_2, + ADDRESSABLE_AREA_3, + ADDRESSABLE_AREA_4, + ADDRESSABLE_AREA_5, + ADDRESSABLE_AREA_6, + ADDRESSABLE_AREA_7, + ADDRESSABLE_AREA_8, + ADDRESSABLE_AREA_9, + ADDRESSABLE_AREA_10, + ADDRESSABLE_AREA_11, + HEATERSHAKER_MODULE_V1_FIXTURE, + HEATERSHAKER_MODULE_V1, + TEMPERATURE_MODULE_V2_FIXTURE, + TEMPERATURE_MODULE_V2, + MAGNETIC_BLOCK_V1_FIXTURE, + MAGNETIC_BLOCK_V1, + THERMOCYCLER_V2_REAR_FIXTURE, + THERMOCYCLER_MODULE_V2, + THERMOCYCLER_V2_FRONT_FIXTURE, + MODULE_FIXTURES_BY_MODEL, + STAGING_AREA_SLOT_WITH_MAGNETIC_BLOCK_V1_FIXTURE, } from './constants' -import type { CutoutFixtureId, CutoutId, OT2CutoutId } from '../deck' -import type { AddressableArea, CoordinateTuple, DeckDefinition } from './types' +import { getModuleDisplayName } from './modules' +import { getCutoutIdForSlotName } from './helpers' +import type { + AddressableAreaName, + CutoutFixtureId, + CutoutId, + OT2CutoutId, +} from '../deck' +import type { + AddressableArea, + CoordinateTuple, + CutoutFixture, + DeckDefinition, + ModuleModel, +} from './types' +import type { ModuleLocation } from '../command' export function getCutoutDisplayName(cutout: CutoutId): string { return cutout.replace('cutout', '') @@ -107,66 +155,162 @@ export function getAddressableAreaFromSlotId( ) } +export function getCutoutFixtureIdsForModuleModel( + moduleModel: ModuleModel +): CutoutFixtureId[] { + const moduleFixtures = MODULE_FIXTURES_BY_MODEL[moduleModel] + return moduleFixtures ?? [] +} + +export function getCutoutFixturesForModuleModel( + moduleModel: ModuleModel, + deckDef: DeckDefinition +): CutoutFixture[] { + const moduleFixtureIds = getCutoutFixtureIdsForModuleModel(moduleModel) + return moduleFixtureIds.reduce((acc, id) => { + const moduleFixture = deckDef.cutoutFixtures.find(cf => cf.id === id) + return moduleFixture != null ? [...acc, moduleFixture] : acc + }, []) +} + +export function getFixtureIdByCutoutIdFromModuleSlotName( + slotName: string, + moduleFixtures: CutoutFixture[], // cutout fixtures for a specific module model + deckDef: DeckDefinition +): { [cutoutId in CutoutId]?: CutoutFixtureId } { + const anchorCutoutId = getCutoutIdForSlotName(slotName, deckDef) + // find the first fixture for this specific module model that may mount to the cutout implied by the slotName + const anchorFixture = moduleFixtures.find(fixture => + fixture.mayMountTo.some(cutoutId => cutoutId === anchorCutoutId) + ) + if (anchorCutoutId != null && anchorFixture != null) { + const groupedFixtures = anchorFixture.fixtureGroup[anchorCutoutId] + return groupedFixtures?.[0] ?? { [anchorCutoutId]: anchorFixture.id } + } + return {} +} + +export function getCutoutIdsFromModuleSlotName( + slotName: string, + moduleFixtures: CutoutFixture[], // cutout fixtures for a specific module model + deckDef: DeckDefinition +): CutoutId[] { + const fixtureIdByCutoutId = getFixtureIdByCutoutIdFromModuleSlotName( + slotName, + moduleFixtures, + deckDef + ) + return Object.keys(fixtureIdByCutoutId) as CutoutId[] +} + +export function getAddressableAreaNamesFromLoadedModule( + moduleModel: ModuleModel, + slotName: ModuleLocation['slotName'], + deckDef: DeckDefinition +): AddressableAreaName[] { + const moduleFixtures = getCutoutFixturesForModuleModel(moduleModel, deckDef) + const cutoutIds = getCutoutIdsFromModuleSlotName( + slotName, + moduleFixtures, + deckDef + ) + return moduleFixtures.reduce((acc, cutoutFixture) => { + const providedAddressableAreas = cutoutIds.reduce( + (innerAcc, cutoutId) => { + const newAddressableAreas = + cutoutFixture?.providesAddressableAreas[cutoutId] ?? [] + return [...innerAcc, ...newAddressableAreas] + }, + [] + ) + return [...acc, ...providedAddressableAreas] + }, []) +} + export function getFixtureDisplayName( - cutoutFixtureId: CutoutFixtureId | null + cutoutFixtureId: CutoutFixtureId | null, + usbPortNumber?: number ): string { - if (cutoutFixtureId === STAGING_AREA_RIGHT_SLOT_FIXTURE) { - return 'Staging area slot' - } else if (cutoutFixtureId === TRASH_BIN_ADAPTER_FIXTURE) { - return 'Trash bin' - } else if (cutoutFixtureId === WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE) { - return 'Waste chute only' - } else if (cutoutFixtureId === WASTE_CHUTE_RIGHT_ADAPTER_COVERED_FIXTURE) { - return 'Waste chute only with cover' - } else if ( - cutoutFixtureId === - STAGING_AREA_SLOT_WITH_WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE - ) { - return 'Waste chute with staging area slot' - } else if ( - cutoutFixtureId === - STAGING_AREA_SLOT_WITH_WASTE_CHUTE_RIGHT_ADAPTER_COVERED_FIXTURE - ) { - return 'Waste chute with staging area slot and cover' - } else { - return 'Slot' + switch (cutoutFixtureId) { + case STAGING_AREA_RIGHT_SLOT_FIXTURE: + return 'Staging area slot' + case TRASH_BIN_ADAPTER_FIXTURE: + return 'Trash bin' + case WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE: + return 'Waste chute only' + case WASTE_CHUTE_RIGHT_ADAPTER_COVERED_FIXTURE: + return 'Waste chute only with cover' + case STAGING_AREA_SLOT_WITH_WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE: + return 'Waste chute with staging area slot' + case STAGING_AREA_SLOT_WITH_WASTE_CHUTE_RIGHT_ADAPTER_COVERED_FIXTURE: + return 'Waste chute with staging area slot and cover' + case HEATERSHAKER_MODULE_V1_FIXTURE: + return usbPortNumber != null + ? `${getModuleDisplayName( + HEATERSHAKER_MODULE_V1 + )} in USB-${usbPortNumber}` + : getModuleDisplayName(HEATERSHAKER_MODULE_V1) + case TEMPERATURE_MODULE_V2_FIXTURE: + return usbPortNumber != null + ? `${getModuleDisplayName( + TEMPERATURE_MODULE_V2 + )} in USB-${usbPortNumber}` + : getModuleDisplayName(TEMPERATURE_MODULE_V2) + case MAGNETIC_BLOCK_V1_FIXTURE: + return `${getModuleDisplayName(MAGNETIC_BLOCK_V1)}` + case STAGING_AREA_SLOT_WITH_MAGNETIC_BLOCK_V1_FIXTURE: + return `${getModuleDisplayName(MAGNETIC_BLOCK_V1)} with staging area slot` + case THERMOCYCLER_V2_REAR_FIXTURE: + return usbPortNumber != null + ? `${getModuleDisplayName( + THERMOCYCLER_MODULE_V2 + )} in USB-${usbPortNumber}` + : getModuleDisplayName(THERMOCYCLER_MODULE_V2) + case THERMOCYCLER_V2_FRONT_FIXTURE: + return usbPortNumber != null + ? `${getModuleDisplayName( + THERMOCYCLER_MODULE_V2 + )} in USB-${usbPortNumber}` + : getModuleDisplayName(THERMOCYCLER_MODULE_V2) + default: + return 'Slot' } } -const STANDARD_OT2_SLOTS = [ - '1', - '2', - '3', - '4', - '5', - '6', - '7', - '8', - '9', - '10', - '11', +const STANDARD_OT2_SLOTS: AddressableAreaName[] = [ + ADDRESSABLE_AREA_1, + ADDRESSABLE_AREA_2, + ADDRESSABLE_AREA_3, + ADDRESSABLE_AREA_4, + ADDRESSABLE_AREA_5, + ADDRESSABLE_AREA_6, + ADDRESSABLE_AREA_7, + ADDRESSABLE_AREA_8, + ADDRESSABLE_AREA_9, + ADDRESSABLE_AREA_10, + ADDRESSABLE_AREA_11, ] -const STANDARD_FLEX_SLOTS = [ - 'A1', - 'A2', - 'A3', - 'B1', - 'B2', - 'B3', - 'C1', - 'C2', - 'C3', - 'D1', - 'D2', - 'D3', +const STANDARD_FLEX_SLOTS: AddressableAreaName[] = [ + A1_ADDRESSABLE_AREA, + A2_ADDRESSABLE_AREA, + A3_ADDRESSABLE_AREA, + B1_ADDRESSABLE_AREA, + B2_ADDRESSABLE_AREA, + B3_ADDRESSABLE_AREA, + C1_ADDRESSABLE_AREA, + C2_ADDRESSABLE_AREA, + C3_ADDRESSABLE_AREA, + D1_ADDRESSABLE_AREA, + D2_ADDRESSABLE_AREA, + D3_ADDRESSABLE_AREA, ] export const isAddressableAreaStandardSlot = ( - addressableAreaId: string, + addressableAreaName: AddressableAreaName, deckDef: DeckDefinition ): boolean => (deckDef.robot.model === FLEX_ROBOT_TYPE ? STANDARD_FLEX_SLOTS : STANDARD_OT2_SLOTS - ).includes(addressableAreaId) + ).includes(addressableAreaName) diff --git a/shared-data/js/helpers/__tests__/getDeckDefFromLoadedLabware.test.ts b/shared-data/js/helpers/__tests__/getDeckDefFromLoadedLabware.test.ts index 9c7a1318e06..8e34261756b 100644 --- a/shared-data/js/helpers/__tests__/getDeckDefFromLoadedLabware.test.ts +++ b/shared-data/js/helpers/__tests__/getDeckDefFromLoadedLabware.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect } from 'vitest' -import ot2DeckDef from '../../../deck/definitions/4/ot2_standard.json' -import ot3DeckDef from '../../../deck/definitions/4/ot3_standard.json' +import ot2DeckDef from '../../../deck/definitions/5/ot2_standard.json' +import ot3DeckDef from '../../../deck/definitions/5/ot3_standard.json' import { getDeckDefFromRobotType } from '..' describe('getDeckDefFromRobotType', () => { diff --git a/shared-data/js/helpers/getAddressableAreasInProtocol.ts b/shared-data/js/helpers/getAddressableAreasInProtocol.ts index 1ca3013930a..81222777db2 100644 --- a/shared-data/js/helpers/getAddressableAreasInProtocol.ts +++ b/shared-data/js/helpers/getAddressableAreasInProtocol.ts @@ -1,5 +1,8 @@ import { MOVABLE_TRASH_A3_ADDRESSABLE_AREA } from '../constants' -import { getAddressableAreaFromSlotId } from '../fixtures' +import { + getAddressableAreaNamesFromLoadedModule, + getAddressableAreaFromSlotId, +} from '../fixtures' import type { AddressableAreaName } from '../../deck' import type { ProtocolAnalysisOutput } from '../../protocol' import type { CompletedProtocolAnalysis, DeckDefinition } from '../types' @@ -12,16 +15,15 @@ export function getAddressableAreasInProtocol( const addressableAreasFromCommands = commands.reduce( (acc, command) => { + const { commandType, params } = command if ( - command.commandType === 'moveLabware' && - command.params.newLocation !== 'offDeck' && - 'slotName' in command.params.newLocation && - !acc.includes( - command.params.newLocation.slotName as AddressableAreaName - ) + commandType === 'moveLabware' && + params.newLocation !== 'offDeck' && + 'slotName' in params.newLocation && + !acc.includes(params.newLocation.slotName as AddressableAreaName) ) { const addressableAreaName = getAddressableAreaFromSlotId( - command.params.newLocation.slotName, + params.newLocation.slotName, deckDef )?.id @@ -31,51 +33,61 @@ export function getAddressableAreasInProtocol( return [...acc, addressableAreaName] } } else if ( - command.commandType === 'moveLabware' && - command.params.newLocation !== 'offDeck' && - 'addressableAreaName' in command.params.newLocation && - !acc.includes(command.params.newLocation.addressableAreaName) + commandType === 'moveLabware' && + params.newLocation !== 'offDeck' && + 'addressableAreaName' in params.newLocation && + !acc.includes(params.newLocation.addressableAreaName) ) { - return [...acc, command.params.newLocation.addressableAreaName] + return [...acc, params.newLocation.addressableAreaName] } else if ( - (command.commandType === 'loadLabware' || - command.commandType === 'loadModule') && - command.params.location !== 'offDeck' && - 'slotName' in command.params.location && - !acc.includes(command.params.location.slotName as AddressableAreaName) + commandType === 'loadLabware' && + params.location !== 'offDeck' && + 'slotName' in params.location && + !acc.includes(params.location.slotName as AddressableAreaName) ) { const addressableAreaName = getAddressableAreaFromSlotId( - command.params.location.slotName, + params.location.slotName, deckDef )?.id // do not add addressable area name for legacy trash labware if ( addressableAreaName == null || - ('loadName' in command.params && - command.params.loadName === 'opentrons_1_trash_3200ml_fixed') + ('loadName' in params && + params.loadName === 'opentrons_1_trash_3200ml_fixed') ) { return acc } else { return [...acc, addressableAreaName] } } else if ( - command.commandType === 'loadLabware' && - command.params.location !== 'offDeck' && - 'addressableAreaName' in command.params.location && - !acc.includes(command.params.location.addressableAreaName) + commandType === 'loadModule' && + !acc.includes(params.location.slotName as AddressableAreaName) + ) { + const addressableAreaNames = getAddressableAreaNamesFromLoadedModule( + params.model, + params.location.slotName, + deckDef + ) + + return [...acc, ...addressableAreaNames] + } else if ( + commandType === 'loadLabware' && + params.location !== 'offDeck' && + 'addressableAreaName' in params.location && + !acc.includes(params.location.addressableAreaName) ) { - return [...acc, command.params.location.addressableAreaName] + return [...acc, params.location.addressableAreaName] } else if ( - command.commandType === 'moveToAddressableArea' && - !acc.includes(command.params.addressableAreaName) + commandType === 'moveToAddressableArea' && + !acc.includes(params.addressableAreaName) ) { - return [...acc, command.params.addressableAreaName] + return [...acc, params.addressableAreaName] } else if ( - command.commandType === 'moveToAddressableAreaForDropTip' && - !acc.includes(command.params.addressableAreaName) + commandType === 'moveToAddressableAreaForDropTip' && + !acc.includes(params.addressableAreaName) ) { - return [...acc, command.params.addressableAreaName] + return [...acc, params.addressableAreaName] } else { return acc } diff --git a/shared-data/js/helpers/getSimplestFlexDeckConfig.ts b/shared-data/js/helpers/getSimplestFlexDeckConfig.ts index e4017199156..65c4ccac3e5 100644 --- a/shared-data/js/helpers/getSimplestFlexDeckConfig.ts +++ b/shared-data/js/helpers/getSimplestFlexDeckConfig.ts @@ -2,7 +2,7 @@ import { FLEX_ROBOT_TYPE } from '../constants' import { getAddressableAreaFromSlotId } from '../fixtures' import { getAddressableAreasInProtocol, getDeckDefFromRobotType } from '.' -import type { AddressableAreaName, CutoutId } from '../../deck' +import type { AddressableAreaName, CutoutFixtureId, CutoutId } from '../../deck' import type { ProtocolAnalysisOutput } from '../../protocol' import type { CutoutConfig, @@ -10,6 +10,7 @@ import type { DeckDefinition, DeckConfiguration, CompletedProtocolAnalysis, + CutoutFixtureGroup, } from '../types' export interface CutoutConfigProtocolSpec extends CutoutConfig { @@ -111,7 +112,6 @@ export function getSimplestDeckConfigForProtocol( } return acc }, FLEX_SIMPLEST_DECK_CONFIG_PROTOCOL_SPEC) - return simplestDeckConfig } @@ -151,6 +151,15 @@ export function getCutoutIdForSlotName( return cutoutIdForSlotName } +export function getFixtureGroupForCutoutFixture( + cutoutFixtureId: CutoutFixtureId, + cutoutFixtures: CutoutFixture[] +): CutoutFixtureGroup { + return ( + cutoutFixtures.find(cf => cf.id === cutoutFixtureId)?.fixtureGroup ?? {} + ) +} + export function getCutoutIdForAddressableArea( addressableArea: AddressableAreaName, cutoutFixtures: CutoutFixture[] diff --git a/shared-data/js/helpers/index.ts b/shared-data/js/helpers/index.ts index 0cb4ec7d88a..a07d10472f6 100644 --- a/shared-data/js/helpers/index.ts +++ b/shared-data/js/helpers/index.ts @@ -1,8 +1,8 @@ import uniq from 'lodash/uniq' import { OPENTRONS_LABWARE_NAMESPACE } from '../constants' -import standardOt2DeckDef from '../../deck/definitions/4/ot2_standard.json' -import standardFlexDeckDef from '../../deck/definitions/4/ot3_standard.json' +import standardOt2DeckDef from '../../deck/definitions/5/ot2_standard.json' +import standardFlexDeckDef from '../../deck/definitions/5/ot3_standard.json' import type { DeckDefinition, LabwareDefinition2, diff --git a/shared-data/js/types.ts b/shared-data/js/types.ts index 318db1d04e4..ff956aefaf6 100644 --- a/shared-data/js/types.ts +++ b/shared-data/js/types.ts @@ -270,11 +270,17 @@ export interface DeckCalibrationPoint { displayName: string } +export type CutoutFixtureGroup = { + [cutoutId in CutoutId]?: Array<{ [cutoutId in CutoutId]?: CutoutFixtureId }> +} + export interface CutoutFixture { id: CutoutFixtureId mayMountTo: CutoutId[] displayName: string providesAddressableAreas: Record + expectOpentronsModuleSerialNumber: boolean + fixtureGroup: CutoutFixtureGroup height: number } @@ -722,7 +728,8 @@ export type StatusBarAnimations = StatusBarAnimation[] export interface CutoutConfig { cutoutId: CutoutId - cutoutFixtureId: CutoutFixtureId | null + cutoutFixtureId: CutoutFixtureId + opentronsModuleSerialNumber?: string } export type DeckConfiguration = CutoutConfig[] diff --git a/shared-data/protocol/fixtures/8/simpleFlexV8.json b/shared-data/protocol/fixtures/8/simpleFlexV8.json index 277d7e636fe..a188ab7c710 100644 --- a/shared-data/protocol/fixtures/8/simpleFlexV8.json +++ b/shared-data/protocol/fixtures/8/simpleFlexV8.json @@ -1220,8 +1220,8 @@ { "commandType": "loadModule", "params": { - "moduleId": "magneticModuleId", - "model": "magneticModuleV2", + "moduleId": "magneticBlockId", + "model": "magneticBlockV1", "location": { "slotName": "3" } } }, @@ -1254,7 +1254,7 @@ "namespace": "opentrons", "version": 1, "location": { - "moduleId": "magneticModuleId" + "moduleId": "magneticBlockId" }, "displayName": "Sample Collection Plate" } diff --git a/shared-data/python/opentrons_shared_data/deck/__init__.py b/shared-data/python/opentrons_shared_data/deck/__init__.py index e922d905ec2..24d56ad730e 100644 --- a/shared-data/python/opentrons_shared_data/deck/__init__.py +++ b/shared-data/python/opentrons_shared_data/deck/__init__.py @@ -15,9 +15,11 @@ DeckSchemaVersion3, DeckDefinitionV4, DeckSchemaVersion4, + DeckDefinitionV5, + DeckSchemaVersion5, ) -DEFAULT_DECK_DEFINITION_VERSION: Final = 4 +DEFAULT_DECK_DEFINITION_VERSION: Final = 5 class Offset(NamedTuple): @@ -38,6 +40,11 @@ class Offset(NamedTuple): } +@overload +def load(name: str, version: "DeckSchemaVersion5") -> "DeckDefinitionV5": + ... + + @overload def load(name: str, version: "DeckSchemaVersion4") -> "DeckDefinitionV4": ... diff --git a/shared-data/python/opentrons_shared_data/deck/dev_types.py b/shared-data/python/opentrons_shared_data/deck/dev_types.py index 06f372d73bd..4563ff10953 100644 --- a/shared-data/python/opentrons_shared_data/deck/dev_types.py +++ b/shared-data/python/opentrons_shared_data/deck/dev_types.py @@ -10,6 +10,7 @@ from ..module.dev_types import ModuleType +DeckSchemaVersion5 = Literal[5] DeckSchemaVersion4 = Literal[4] DeckSchemaVersion3 = Literal[3] DeckSchemaVersion2 = Literal[2] @@ -111,9 +112,11 @@ class Cutout(TypedDict): class CutoutFixture(TypedDict): id: str + expectOpentronsModuleSerialNumber: bool mayMountTo: List[str] displayName: str providesAddressableAreas: Dict[str, List[str]] + fixtureGroup: Dict[str, List[Dict[str, str]]] height: float @@ -176,4 +179,19 @@ class DeckDefinitionV4(_RequiredDeckDefinitionV4, total=False): gripperOffsets: Dict[str, GripperOffsets] -DeckDefinition = Union[DeckDefinitionV3, DeckDefinitionV4] +class _RequiredDeckDefinitionV5(TypedDict): + otId: str + schemaVersion: Literal[5] + cornerOffsetFromOrigin: List[float] + dimensions: List[float] + metadata: Metadata + robot: Robot + locations: LocationsV4 + cutoutFixtures: List[CutoutFixture] + + +class DeckDefinitionV5(_RequiredDeckDefinitionV5, total=False): + gripperOffsets: Dict[str, GripperOffsets] + + +DeckDefinition = Union[DeckDefinitionV3, DeckDefinitionV4, DeckDefinitionV5] diff --git a/shared-data/python/tests/deck/test_typechecks.py b/shared-data/python/tests/deck/test_typechecks.py index f021004b050..4e2406df0fa 100644 --- a/shared-data/python/tests/deck/test_typechecks.py +++ b/shared-data/python/tests/deck/test_typechecks.py @@ -5,7 +5,10 @@ list_names as list_deck_definition_names, load as load_deck_definition, ) -from opentrons_shared_data.deck.dev_types import DeckDefinitionV3, DeckDefinitionV4 +from opentrons_shared_data.deck.dev_types import ( + DeckDefinitionV3, + DeckDefinitionV5, +) @pytest.mark.parametrize("defname", list_deck_definition_names(version=3)) @@ -14,7 +17,7 @@ def test_v3_defs(defname): typeguard.check_type(defn, DeckDefinitionV3) -@pytest.mark.parametrize("defname", list_deck_definition_names(version=4)) -def test_v4_defs(defname): - defn = load_deck_definition(name=defname, version=4) - typeguard.check_type(defn, DeckDefinitionV4) +@pytest.mark.parametrize("defname", list_deck_definition_names(version=5)) +def test_v5_defs(defname): + defn = load_deck_definition(name=defname, version=5) + typeguard.check_type(defn, DeckDefinitionV5) From 55f798a62049fc468f59f5180cee985856998c57 Mon Sep 17 00:00:00 2001 From: Jeremy Leon Date: Mon, 15 Apr 2024 17:15:23 -0400 Subject: [PATCH 130/194] refactor(api): more clear error messages for type validation when creating runtime parameters (#14903) --- .../protocols/parameters/validation.py | 73 +++++++++++++------ .../protocols/parameters/test_validation.py | 9 ++- 2 files changed, 55 insertions(+), 27 deletions(-) diff --git a/api/src/opentrons/protocols/parameters/validation.py b/api/src/opentrons/protocols/parameters/validation.py index 9410db294ed..2db343c71b6 100644 --- a/api/src/opentrons/protocols/parameters/validation.py +++ b/api/src/opentrons/protocols/parameters/validation.py @@ -29,15 +29,21 @@ def validate_variable_name_unique( def ensure_display_name(display_name: str) -> str: """Validate display name is within the character limit.""" + if not isinstance(display_name, str): + raise ParameterNameError( + f"Display name must be a string and at most {DISPLAY_NAME_MAX_LEN} characters." + ) if len(display_name) > DISPLAY_NAME_MAX_LEN: raise ParameterNameError( - f"Display name {display_name} greater than {DISPLAY_NAME_MAX_LEN} characters." + f'Display name "{display_name}" greater than {DISPLAY_NAME_MAX_LEN} characters.' ) return display_name def ensure_variable_name(variable_name: str) -> str: """Validate variable name is a valid python variable name.""" + if not isinstance(variable_name, str): + raise ParameterNameError("Variable name must be a string.") if not variable_name.isidentifier(): raise ParameterNameError( "Variable name must only contain alphanumeric characters, underscores, and cannot start with a digit." @@ -49,19 +55,29 @@ def ensure_variable_name(variable_name: str) -> str: def ensure_description(description: Optional[str]) -> Optional[str]: """Validate description is within the character limit.""" - if description is not None and len(description) > DESCRIPTION_MAX_LEN: - raise ParameterNameError( - f"Description {description} greater than {DESCRIPTION_MAX_LEN} characters." - ) + if description is not None: + if not isinstance(description, str): + raise ParameterNameError( + f"Description must be a string and at most {DESCRIPTION_MAX_LEN} characters." + ) + if len(description) > DESCRIPTION_MAX_LEN: + raise ParameterNameError( + f'Description "{description}" greater than {DESCRIPTION_MAX_LEN} characters.' + ) return description def ensure_unit_string_length(unit: Optional[str]) -> Optional[str]: """Validate unit is within the character limit.""" - if unit is not None and len(unit) > UNIT_MAX_LEN: - raise ParameterNameError( - f"Description {unit} greater than {UNIT_MAX_LEN} characters." - ) + if unit is not None: + if not isinstance(unit, str): + raise ParameterNameError( + f"Unit must be a string and at most {UNIT_MAX_LEN} characters." + ) + if len(unit) > UNIT_MAX_LEN: + raise ParameterNameError( + f'Unit "{unit}" greater than {UNIT_MAX_LEN} characters.' + ) return unit @@ -135,7 +151,7 @@ def convert_type_string_for_enum( return "str" else: raise ParameterValueError( - f"Cannot resolve parameter type {parameter_type} for an enumerated parameter." + f"Cannot resolve parameter type '{parameter_type.__name__}' for an enumerated parameter." ) @@ -147,7 +163,7 @@ def convert_type_string_for_num_param(parameter_type: type) -> Literal["int", "f return "float" else: raise ParameterValueError( - f"Cannot resolve parameter type {parameter_type} for a number parameter." + f"Cannot resolve parameter type '{parameter_type.__name__}' for a number parameter." ) @@ -173,7 +189,7 @@ def _validate_choices( ensure_display_name(display_name) if not isinstance(value, parameter_type): raise ParameterDefinitionError( - f"All choices provided must match type {type(parameter_type)}" + f"All choices provided must be of type '{parameter_type.__name__}'" ) @@ -192,21 +208,27 @@ def _validate_min_and_max( "If a maximum value is provided a minimum must also be provided." ) elif maximum is not None and minimum is not None: - if isinstance(maximum, (int, float)) and isinstance(minimum, (int, float)): - if maximum <= minimum: + if parameter_type is int or parameter_type is float: + if not isinstance(minimum, parameter_type): raise ParameterDefinitionError( - "Maximum must be greater than the minimum" + f"Minimum is type '{type(minimum).__name__}'," + f" but must be of parameter type '{parameter_type.__name__}'" ) - - if not isinstance(minimum, parameter_type) or not isinstance( - maximum, parameter_type - ): + if not isinstance(maximum, parameter_type): raise ParameterDefinitionError( - f"Minimum and maximum must match type {parameter_type}" + f"Maximum is type '{type(maximum).__name__}'," + f" but must be of parameter type '{parameter_type.__name__}'" + ) + # These asserts are for the type checker and should never actually be asserted false + assert isinstance(minimum, (int, float)) + assert isinstance(maximum, (int, float)) + if maximum <= minimum: + raise ParameterDefinitionError( + "Maximum must be greater than the minimum" ) else: raise ParameterDefinitionError( - "Only parameters of type float or int can have a minimum and maximum" + "Only parameters of type float or int can have a minimum and maximum." ) @@ -214,7 +236,8 @@ def validate_type(value: ParamType, parameter_type: type) -> None: """Validate parameter value is the correct type.""" if not isinstance(value, parameter_type): raise ParameterValueError( - f"Parameter value {value} has type {type(value)}, must match type {parameter_type}." + f"Parameter value {value} has type '{type(value).__name__}'," + f" but must be of type '{parameter_type.__name__}'." ) @@ -226,7 +249,11 @@ def validate_options( parameter_type: type, ) -> None: """Validate default values and all possible constraints for a valid parameter definition.""" - validate_type(default, parameter_type) + if not isinstance(default, parameter_type): + raise ParameterValueError( + f"Parameter default {default} has type '{type(default).__name__}'," + f" but must be of type '{parameter_type.__name__}'." + ) if choices is None and minimum is None and maximum is None: raise ParameterDefinitionError( diff --git a/api/tests/opentrons/protocols/parameters/test_validation.py b/api/tests/opentrons/protocols/parameters/test_validation.py index 4d3b2fc83b5..4206d3d3cd4 100644 --- a/api/tests/opentrons/protocols/parameters/test_validation.py +++ b/api/tests/opentrons/protocols/parameters/test_validation.py @@ -278,14 +278,15 @@ def test_convert_type_string_for_num_param_raises(param_type: type) -> None: None, [{"display_name": "abc", "value": "123"}], int, - "must match type", + "must be of type", ), (123, 1, None, None, int, "maximum must also"), (123, None, 100, None, int, "minimum must also"), (123, 100, 1, None, int, "Maximum must be greater"), - (123, 1.1, 100, None, int, "Minimum and maximum must match type"), - (123, 1, 100.5, None, int, "Minimum and maximum must match type"), - (123, "1", "100", None, int, "Only parameters of type float or int"), + (123, 1.1, 100, None, int, "Minimum is type"), + (123, 1, 100.5, None, int, "Maximum is type"), + (123.0, "1.0", 100.0, None, float, "Minimum is type"), + ("blah", 1, 100, None, str, "Only parameters of type float or int"), ], ) def test_validate_options_raise_definition_error( From 8cc2fc7656ad5e73faa524a001ba708de9e9d776 Mon Sep 17 00:00:00 2001 From: Nick Diehl <47604184+ncdiehl11@users.noreply.github.com> Date: Mon, 15 Apr 2024 17:33:52 -0400 Subject: [PATCH 131/194] fix(app): properly disable proceed button if no available robots (#14907) closes RQA-2517 --- .../__tests__/ChooseRobotSlideout.test.tsx | 17 ++++++++++++++++- app/src/organisms/ChooseRobotSlideout/index.tsx | 9 ++++++--- .../ChooseRobotToRunProtocolSlideout.test.tsx | 13 +++++++++++++ 3 files changed, 35 insertions(+), 4 deletions(-) diff --git a/app/src/organisms/ChooseRobotSlideout/__tests__/ChooseRobotSlideout.test.tsx b/app/src/organisms/ChooseRobotSlideout/__tests__/ChooseRobotSlideout.test.tsx index 6c97f4e62c3..19500166410 100644 --- a/app/src/organisms/ChooseRobotSlideout/__tests__/ChooseRobotSlideout.test.tsx +++ b/app/src/organisms/ChooseRobotSlideout/__tests__/ChooseRobotSlideout.test.tsx @@ -3,6 +3,7 @@ import { vi, it, describe, expect, beforeEach } from 'vitest' import { StaticRouter } from 'react-router-dom' import { fireEvent, screen } from '@testing-library/react' +import { OT2_ROBOT_TYPE } from '@opentrons/shared-data' import { renderWithProviders } from '../../../__testing-utils__' import { i18n } from '../../../i18n' @@ -22,7 +23,7 @@ import { useFeatureFlag } from '../../../redux/config' import { getNetworkInterfaces } from '../../../redux/networking' import { ChooseRobotSlideout } from '..' import { useNotifyService } from '../../../resources/useNotifyService' -import { OT2_ROBOT_TYPE, RunTimeParameter } from '@opentrons/shared-data' +import type { RunTimeParameter } from '@opentrons/shared-data' vi.mock('../../../redux/discovery') vi.mock('../../../redux/robot-update') @@ -295,4 +296,18 @@ describe('ChooseRobotSlideout', () => { ip: 'otherIp', }) }) + + it('sets selected robot to null if no available robots', () => { + vi.mocked(getConnectableRobots).mockReturnValue([]) + render({ + onCloseClick: vi.fn(), + isExpanded: true, + isSelectedRobotOnDifferentSoftwareVersion: false, + selectedRobot: null, + setSelectedRobot: mockSetSelectedRobot, + title: 'choose robot slideout title', + robotType: OT2_ROBOT_TYPE, + }) + expect(mockSetSelectedRobot).toBeCalledWith(null) + }) }) diff --git a/app/src/organisms/ChooseRobotSlideout/index.tsx b/app/src/organisms/ChooseRobotSlideout/index.tsx index c8f5a674257..68b89afddad 100644 --- a/app/src/organisms/ChooseRobotSlideout/index.tsx +++ b/app/src/organisms/ChooseRobotSlideout/index.tsx @@ -184,15 +184,18 @@ export function ChooseRobotSlideout( {} ) + const reducerAvailableRobots = healthyReachableRobots.filter(robot => + showIdleOnly ? !robotBusyStatusByName[robot.name] : robot + ) const reducerBusyCount = healthyReachableRobots.filter( robot => robotBusyStatusByName[robot.name] ).length // this useEffect sets the default selection to the first robot in the list. state is managed by the caller React.useEffect(() => { - if (selectedRobot == null && healthyReachableRobots.length > 0) { - setSelectedRobot(healthyReachableRobots[0]) - } else if (healthyReachableRobots.length === 0) { + if (selectedRobot == null && reducerAvailableRobots.length > 0) { + setSelectedRobot(reducerAvailableRobots[0]) + } else if (reducerAvailableRobots.length === 0) { setSelectedRobot(null) } }, [healthyReachableRobots, selectedRobot, setSelectedRobot]) diff --git a/app/src/organisms/ChooseRobotToRunProtocolSlideout/__tests__/ChooseRobotToRunProtocolSlideout.test.tsx b/app/src/organisms/ChooseRobotToRunProtocolSlideout/__tests__/ChooseRobotToRunProtocolSlideout.test.tsx index b7d2b32cb75..5bd054d887f 100644 --- a/app/src/organisms/ChooseRobotToRunProtocolSlideout/__tests__/ChooseRobotToRunProtocolSlideout.test.tsx +++ b/app/src/organisms/ChooseRobotToRunProtocolSlideout/__tests__/ChooseRobotToRunProtocolSlideout.test.tsx @@ -390,4 +390,17 @@ describe('ChooseRobotToRunProtocolSlideout', () => { {} ) }) + + it('disables proceed button if no available robots', () => { + vi.mocked(getConnectableRobots).mockReturnValue([]) + render({ + storedProtocolData: storedProtocolDataFixture, + onCloseClick: vi.fn(), + showSlideout: true, + }) + const proceedButton = screen.getByRole('button', { + name: 'Continue to parameters', + }) + expect(proceedButton).toBeDisabled() + }) }) From dfb572cd377b2b33f1587dcb3899c9421d747eaa Mon Sep 17 00:00:00 2001 From: Brent Hagen Date: Mon, 15 Apr 2024 20:53:33 -0400 Subject: [PATCH 132/194] feat(app): factory mode desktop toggle (#14911) adds the desktop advanced setting toggle to enable factory mode. closes PLAT-280, PLAT-282 --- app/src/assets/localization/en/anonymous.json | 1 + app/src/assets/localization/en/branded.json | 1 + .../localization/en/device_settings.json | 9 + app/src/atoms/Slideout/MultiSlideout.tsx | 13 +- .../FactoryModeSlideout.tsx | 168 ++++++++++++++++++ .../RobotSettings/AdvancedTab/FactoryMode.tsx | 50 ++++++ .../RobotSettings/AdvancedTab/index.ts | 1 + .../RobotSettings/RobotSettingsAdvanced.tsx | 22 +++ 8 files changed, 255 insertions(+), 10 deletions(-) create mode 100644 app/src/organisms/Devices/RobotSettings/AdvancedTab/AdvancedTabSlideouts/FactoryModeSlideout.tsx create mode 100644 app/src/organisms/Devices/RobotSettings/AdvancedTab/FactoryMode.tsx diff --git a/app/src/assets/localization/en/anonymous.json b/app/src/assets/localization/en/anonymous.json index 2bb4f67a4d7..5dcfd9bf237 100644 --- a/app/src/assets/localization/en/anonymous.json +++ b/app/src/assets/localization/en/anonymous.json @@ -33,6 +33,7 @@ "module_calibration_get_started": "To get started, remove labware from the deck and clean up the working area to make the calibration easier. Also gather the needed equipment shown to the right.The calibration adapter came with your module. The pipette probe came with your pipette.", "module_error_contact_support": "Try powering the module off and on again. If the error persists, contact support.", "network_setup_menu_description": "You’ll use this connection to run software updates and load protocols onto your robot.", + "oem_mode_description": "Enable OEM Mode to remove all instances of Opentrons from the Flex touchscreen.", "opentrons_app_successfully_updated": "The app was successfully updated.", "opentrons_app_update": "app update", "opentrons_app_update_available": "App Update Available", diff --git a/app/src/assets/localization/en/branded.json b/app/src/assets/localization/en/branded.json index 6143400d541..13b53967aff 100644 --- a/app/src/assets/localization/en/branded.json +++ b/app/src/assets/localization/en/branded.json @@ -33,6 +33,7 @@ "module_calibration_get_started": "To get started, remove labware from the deck and clean up the working area to make the calibration easier. Also gather the needed equipment shown to the right.The calibration adapter came with your module. The pipette probe came with your Flex pipette.", "module_error_contact_support": "Try powering the module off and on again. If the error persists, contact Opentrons Support.", "network_setup_menu_description": "You’ll use this connection to run software updates and load protocols onto your Opentrons Flex.", + "oem_mode_description": "Enable OEM Mode to remove all instances of Opentrons from the Flex touchscreen.", "opentrons_app_successfully_updated": "The Opentrons App was successfully updated.", "opentrons_app_update": "Opentrons App update", "opentrons_app_update_available": "Opentrons App Update Available", diff --git a/app/src/assets/localization/en/device_settings.json b/app/src/assets/localization/en/device_settings.json index c6bd00ad70d..3aec18d24a6 100644 --- a/app/src/assets/localization/en/device_settings.json +++ b/app/src/assets/localization/en/device_settings.json @@ -50,6 +50,7 @@ "clear_option_runs_history_subtext": "Clears information about past runs of all protocols.", "clear_option_tip_length_calibrations": "Clear tip length calibrations", "cancel_software_update": "Cancel software update", + "complete_and_restart_robot": "Complete and restart robot", "confirm_device_reset_description": "This will permanently delete all protocol, calibration, and other data. You’ll have to redo initial setup before using the robot again.", "confirm_device_reset_heading": "Are you sure you want to reset your device?", "connect": "Connect", @@ -107,6 +108,7 @@ "enable_status_light": "Enable status light", "enable_status_light_description": "Turn on or off the strip of color LEDs on the front of the robot.", "engaged": "Engaged", + "enter_factory_password": "Enter factory password", "enter_network_name": "Enter network name", "enter_password": "Enter password", "estop": "E-stop", @@ -118,6 +120,7 @@ "ethernet": "Ethernet", "ethernet_connection_description": "Connect an Ethernet cable to the back of the robot and a network switch or hub.", "exit": "exit", + "factory_mode": "Factory Mode", "factory_reset": "Factory Reset", "factory_reset_description": "Resets all settings. You’ll have to redo initial setup before using the robot again.", "factory_reset_modal_description": "This data cannot be retrieved later.", @@ -140,6 +143,7 @@ "install_e_stop": "Install the E-stop", "installing_software": "Installing software...", "installing_update": "Installing update...", + "invalid_password": "Invalid password", "ip_address": "IP Address", "join_other_network": "Join other network", "join_other_network_error_message": "Must be 2–32 characters long", @@ -151,6 +155,7 @@ "launch_jupyter_notebook": "Launch Jupyter Notebook", "legacy_settings": "Legacy Settings", "mac_address": "MAC Address", + "manage_oem_settings": "Manage OEM settings", "minutes": "{{minute}} minutes", "missing_calibration": "Missing calibration", "model_and_serial": "Pipette Model and Serial", @@ -186,7 +191,10 @@ "not_connected_via_wifi": "Not connected via Wi-Fi", "not_connected_via_wired_usb": "Not connected via wired USB", "not_now": "Not now", + "oem_mode": "OEM Mode", + "off": "Off", "one_hour": "1 hour", + "on": "On", "other_networks": "Other Networks", "password": "Password", "password_error_message": "Must be at least 8 characters", @@ -252,6 +260,7 @@ "select_authentication_method": "Select authentication method for your selected network.", "sending_software": "Sending software...", "serial": "Serial", + "setup_mode": "Setup mode", "short_trash_bin": "Short trash bin", "short_trash_bin_description": "For pre-2019 robots with trash bins that are 55mm tall (instead of 77mm default)", "show": "Show", diff --git a/app/src/atoms/Slideout/MultiSlideout.tsx b/app/src/atoms/Slideout/MultiSlideout.tsx index 71ce02f6de6..73054a10a45 100644 --- a/app/src/atoms/Slideout/MultiSlideout.tsx +++ b/app/src/atoms/Slideout/MultiSlideout.tsx @@ -1,16 +1,9 @@ import * as React from 'react' import { Slideout } from './index' -interface MultiSlideoutProps { - title: string | React.ReactElement - children: React.ReactNode - onCloseClick: () => void - currentStep: number - maxSteps: number - // isExpanded is for collapse and expand animation - isExpanded?: boolean - footer?: React.ReactNode -} +import type { MultiSlideoutSpecs, SlideoutProps } from './index' + +type MultiSlideoutProps = SlideoutProps & MultiSlideoutSpecs export const MultiSlideout = (props: MultiSlideoutProps): JSX.Element => { const { diff --git a/app/src/organisms/Devices/RobotSettings/AdvancedTab/AdvancedTabSlideouts/FactoryModeSlideout.tsx b/app/src/organisms/Devices/RobotSettings/AdvancedTab/AdvancedTabSlideouts/FactoryModeSlideout.tsx new file mode 100644 index 00000000000..d034e713373 --- /dev/null +++ b/app/src/organisms/Devices/RobotSettings/AdvancedTab/AdvancedTabSlideouts/FactoryModeSlideout.tsx @@ -0,0 +1,168 @@ +import * as React from 'react' +import { useDispatch } from 'react-redux' +import { useForm, Controller } from 'react-hook-form' +import { useTranslation } from 'react-i18next' + +import { + ALIGN_CENTER, + COLORS, + DIRECTION_COLUMN, + Flex, + PrimaryButton, + SPACING, + StyledText, + TYPOGRAPHY, +} from '@opentrons/components' +import { useRobotSettingsQuery } from '@opentrons/react-api-client' + +import { ToggleButton } from '../../../../../atoms/buttons' +import { InputField } from '../../../../../atoms/InputField' +import { MultiSlideout } from '../../../../../atoms/Slideout/MultiSlideout' +import { restartRobot } from '../../../../../redux/robot-admin' +import { updateSetting } from '../../../../../redux/robot-settings' + +import type { RobotSettingsField } from '@opentrons/api-client' +import type { Dispatch } from '../../../../../redux/types' + +interface FactoryModeSlideoutProps { + isExpanded: boolean + onCloseClick: () => void + robotName: string +} + +interface FormValues { + passwordInput: string +} + +export function FactoryModeSlideout({ + isExpanded, + onCloseClick, + robotName, +}: FactoryModeSlideoutProps): JSX.Element { + const { t } = useTranslation(['device_settings', 'shared', 'branded']) + + const dispatch = useDispatch() + + const { settings } = useRobotSettingsQuery().data ?? {} + const oemModeSetting = (settings ?? []).find( + (setting: RobotSettingsField) => setting?.id === 'enableOEMMode' + ) + const isOEMMode = oemModeSetting?.value ?? null + + const [currentStep, setCurrentStep] = React.useState(1) + const [toggleValue, setToggleValue] = React.useState(false) + + const { + handleSubmit, + control, + formState: { errors }, + trigger, + } = useForm({ + defaultValues: { + passwordInput: '', + }, + }) + const onSubmit = (data: FormValues): void => { + setCurrentStep(2) + } + + const handleSubmitFactoryPassword = (): void => { + // TODO: validation and errors: PLAT-281 + void handleSubmit(onSubmit)() + } + + const handleToggleClick: React.MouseEventHandler = () => { + setToggleValue(toggleValue => !toggleValue) + } + + const handleCompleteClick: React.MouseEventHandler = () => { + dispatch(updateSetting(robotName, 'enableOEMMode', toggleValue)) + dispatch(restartRobot(robotName)) + onCloseClick() + } + + React.useEffect(() => { + // initialize local state to OEM mode value + if (isOEMMode != null) { + setToggleValue(isOEMMode) + } + }, [isOEMMode]) + + return ( + + {currentStep === 1 ? ( + + {t('shared:next')} + + ) : null} + {currentStep === 2 ? ( + + {t('complete_and_restart_robot')} + + ) : null} + + } + > + {currentStep === 1 ? ( + + ( + ) => { + field.onChange(e) + trigger('passwordInput') + }} + value={field.value} + error={fieldState.error?.message && ' '} + onBlur={field.onBlur} + title={t('enter_factory_password')} + /> + )} + /> + {errors.passwordInput != null ? ( + + {errors.passwordInput.message} + + ) : null} + + ) : null} + {currentStep === 2 ? ( + + + {t('oem_mode')} + + + + + {toggleValue ? t('on') : t('off')} + + + {t('branded:oem_mode_description')} + + ) : null} + + ) +} diff --git a/app/src/organisms/Devices/RobotSettings/AdvancedTab/FactoryMode.tsx b/app/src/organisms/Devices/RobotSettings/AdvancedTab/FactoryMode.tsx new file mode 100644 index 00000000000..8d2fda7c386 --- /dev/null +++ b/app/src/organisms/Devices/RobotSettings/AdvancedTab/FactoryMode.tsx @@ -0,0 +1,50 @@ +import * as React from 'react' +import { useTranslation } from 'react-i18next' + +import { + ALIGN_CENTER, + Box, + Flex, + JUSTIFY_SPACE_BETWEEN, + SPACING_AUTO, + SPACING, + StyledText, + TYPOGRAPHY, +} from '@opentrons/components' + +import { TertiaryButton } from '../../../../atoms/buttons' + +interface FactoryModeProps { + isRobotBusy: boolean + setShowFactoryModeSlideout: React.Dispatch> +} + +export function FactoryMode({ + isRobotBusy, + setShowFactoryModeSlideout, +}: FactoryModeProps): JSX.Element { + const { t } = useTranslation('device_settings') + + return ( + + + + {t('factory_mode')} + + + { + setShowFactoryModeSlideout(true) + }} + > + {t('setup_mode')} + + + ) +} diff --git a/app/src/organisms/Devices/RobotSettings/AdvancedTab/index.ts b/app/src/organisms/Devices/RobotSettings/AdvancedTab/index.ts index 86e45ab1f73..b53134df945 100644 --- a/app/src/organisms/Devices/RobotSettings/AdvancedTab/index.ts +++ b/app/src/organisms/Devices/RobotSettings/AdvancedTab/index.ts @@ -1,6 +1,7 @@ export * from './DeviceReset' export * from './DisplayRobotName' export * from './EnableStatusLight' +export * from './FactoryMode' export * from './GantryHoming' export * from './LegacySettings' export * from './OpenJupyterControl' diff --git a/app/src/organisms/Devices/RobotSettings/RobotSettingsAdvanced.tsx b/app/src/organisms/Devices/RobotSettings/RobotSettingsAdvanced.tsx index 8772f9a383a..be9cdcd2be4 100644 --- a/app/src/organisms/Devices/RobotSettings/RobotSettingsAdvanced.tsx +++ b/app/src/organisms/Devices/RobotSettings/RobotSettingsAdvanced.tsx @@ -19,6 +19,7 @@ import { DeviceReset, DisplayRobotName, EnableStatusLight, + FactoryMode, GantryHoming, LegacySettings, OpenJupyterControl, @@ -39,6 +40,7 @@ import { import { RenameRobotSlideout } from './AdvancedTab/AdvancedTabSlideouts/RenameRobotSlideout' import { DeviceResetSlideout } from './AdvancedTab/AdvancedTabSlideouts/DeviceResetSlideout' import { DeviceResetModal } from './AdvancedTab/AdvancedTabSlideouts/DeviceResetModal' +import { FactoryModeSlideout } from './AdvancedTab/AdvancedTabSlideouts/FactoryModeSlideout' import { handleUpdateBuildroot } from './UpdateBuildroot' import { UNREACHABLE } from '../../../redux/discovery' import { getTopPortalEl } from '../../../App/portal' @@ -72,6 +74,10 @@ export function RobotSettingsAdvanced({ showDeviceResetModal, setShowDeviceResetModal, ] = React.useState(false) + const [ + showFactoryModeSlideout, + setShowFactoryModeSlideout, + ] = React.useState(false) const isRobotBusy = useIsRobotBusy({ poll: true }) const isEstopNotDisengaged = useIsEstopNotDisengaged(robotName) @@ -131,6 +137,13 @@ export function RobotSettingsAdvanced({ robotName={robotName} /> )} + {showFactoryModeSlideout && ( + setShowFactoryModeSlideout(false)} + robotName={robotName} + /> + )} {showDeviceResetSlideout && ( handleUpdateBuildroot(robot)} /> + {isFlex ? ( + <> + + + + ) : null} Date: Tue, 16 Apr 2024 05:27:57 -0500 Subject: [PATCH 133/194] fix(app-testing): snapshot failure capture (#14913) This PR is an automated snapshot update request. Please review the changes and merge if they are acceptable or find you bug and fix it. Co-authored-by: y3rsh --- ...M_TC_2_15_ABR4_Illumina_DNA_Prep_24x].json | 822 +++++----- ...nalysisError_ModuleInStagingAreaCol3].json | 481 +----- ...2_15_ABR3_Illumina_DNA_Enrichment_v4].json | 1422 ++++++++--------- ...or_HeaterShakerConflictWithTrashBin2].json | 2 +- ...rror_TrashBinAndThermocyclerConflict].json | 2 +- ...ne_2_16_AnalysisError_TrashBinInCol2].json | 2 +- ...TM_2_15_ABR3_Illumina_DNA_Enrichment].json | 148 +- ...isError_MagneticModuleInFlexProtocol].json | 8 +- ...e_TM_2_16_AnalysisError_ModuleInCol2].json | 2 +- ...sisError_ModuleAndWasteChuteConflict].json | 34 +- ...or_HeaterShakerConflictWithTrashBin1].json | 2 +- 11 files changed, 1257 insertions(+), 1668 deletions(-) diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0400decc88][Flex_P1000MLeft_P50MRight_HS_TM_MM_TC_2_15_ABR4_Illumina_DNA_Prep_24x].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0400decc88][Flex_P1000MLeft_P50MRight_HS_TM_MM_TC_2_15_ABR4_Illumina_DNA_Prep_24x].json index d321c3f1579..69b643fbc46 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0400decc88][Flex_P1000MLeft_P50MRight_HS_TM_MM_TC_2_15_ABR4_Illumina_DNA_Prep_24x].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0400decc88][Flex_P1000MLeft_P50MRight_HS_TM_MM_TC_2_15_ABR4_Illumina_DNA_Prep_24x].json @@ -8513,7 +8513,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -8538,7 +8538,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -11.280000000000001 + "z": -11.279999999999998 }, "origin": "top" }, @@ -8563,7 +8563,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -11.280000000000001 + "z": -11.279999999999998 }, "origin": "top" }, @@ -8588,7 +8588,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -8613,7 +8613,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -8639,7 +8639,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -8664,7 +8664,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -11.280000000000001 + "z": -11.279999999999998 }, "origin": "top" }, @@ -8689,7 +8689,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -11.280000000000001 + "z": -11.279999999999998 }, "origin": "top" }, @@ -8715,7 +8715,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -11.280000000000001 + "z": -11.279999999999998 }, "origin": "top" }, @@ -8740,7 +8740,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -8765,7 +8765,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -8791,7 +8791,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -8816,7 +8816,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -11.280000000000001 + "z": -11.279999999999998 }, "origin": "top" }, @@ -8841,7 +8841,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -11.280000000000001 + "z": -11.279999999999998 }, "origin": "top" }, @@ -8866,7 +8866,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": 2.0 + "z": 2.000000000000007 }, "origin": "top" }, @@ -8890,7 +8890,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": 5.0 + "z": 5.000000000000007 }, "origin": "top" }, @@ -8924,7 +8924,7 @@ "position": { "x": 14.38, "y": 74.24, - "z": 34.650000000000006 + "z": 34.65 } }, "status": "succeeded" @@ -8938,7 +8938,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": 5.0 + "z": 5.000000000000007 }, "origin": "top" }, @@ -9195,7 +9195,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -9220,7 +9220,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -11.280000000000001 + "z": -11.279999999999998 }, "origin": "top" }, @@ -9245,7 +9245,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -11.280000000000001 + "z": -11.279999999999998 }, "origin": "top" }, @@ -9270,7 +9270,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -9295,7 +9295,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -9321,7 +9321,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -9346,7 +9346,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -11.280000000000001 + "z": -11.279999999999998 }, "origin": "top" }, @@ -9371,7 +9371,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -11.280000000000001 + "z": -11.279999999999998 }, "origin": "top" }, @@ -9397,7 +9397,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -11.280000000000001 + "z": -11.279999999999998 }, "origin": "top" }, @@ -9422,7 +9422,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -9447,7 +9447,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -9473,7 +9473,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -9498,7 +9498,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -11.280000000000001 + "z": -11.279999999999998 }, "origin": "top" }, @@ -9523,7 +9523,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -11.280000000000001 + "z": -11.279999999999998 }, "origin": "top" }, @@ -9548,7 +9548,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": 2.0 + "z": 2.000000000000007 }, "origin": "top" }, @@ -9572,7 +9572,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": 5.0 + "z": 5.000000000000007 }, "origin": "top" }, @@ -9606,7 +9606,7 @@ "position": { "x": 23.38, "y": 74.24, - "z": 34.650000000000006 + "z": 34.65 } }, "status": "succeeded" @@ -9620,7 +9620,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": 5.0 + "z": 5.000000000000007 }, "origin": "top" }, @@ -9877,7 +9877,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -9902,7 +9902,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -11.280000000000001 + "z": -11.279999999999998 }, "origin": "top" }, @@ -9927,7 +9927,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -11.280000000000001 + "z": -11.279999999999998 }, "origin": "top" }, @@ -9952,7 +9952,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -9977,7 +9977,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -10003,7 +10003,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -10028,7 +10028,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -11.280000000000001 + "z": -11.279999999999998 }, "origin": "top" }, @@ -10053,7 +10053,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -11.280000000000001 + "z": -11.279999999999998 }, "origin": "top" }, @@ -10079,7 +10079,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -11.280000000000001 + "z": -11.279999999999998 }, "origin": "top" }, @@ -10104,7 +10104,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -10129,7 +10129,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -10155,7 +10155,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -10180,7 +10180,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -11.280000000000001 + "z": -11.279999999999998 }, "origin": "top" }, @@ -10205,7 +10205,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -11.280000000000001 + "z": -11.279999999999998 }, "origin": "top" }, @@ -10230,7 +10230,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": 2.0 + "z": 2.000000000000007 }, "origin": "top" }, @@ -10254,7 +10254,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": 5.0 + "z": 5.000000000000007 }, "origin": "top" }, @@ -10288,7 +10288,7 @@ "position": { "x": 32.38, "y": 74.24, - "z": 34.650000000000006 + "z": 34.65 } }, "status": "succeeded" @@ -10302,7 +10302,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": 5.0 + "z": 5.000000000000007 }, "origin": "top" }, @@ -10534,7 +10534,7 @@ "position": { "x": -5.624999999999998, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 10.0 }, @@ -10559,7 +10559,7 @@ "position": { "x": -5.624999999999998, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 } }, "status": "succeeded" @@ -10584,7 +10584,7 @@ "position": { "x": -5.624999999999998, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -10610,7 +10610,7 @@ "position": { "x": -5.624999999999998, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -10636,7 +10636,7 @@ "position": { "x": -5.624999999999998, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -10662,7 +10662,7 @@ "position": { "x": -5.624999999999998, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -10688,7 +10688,7 @@ "position": { "x": -5.624999999999998, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -10714,7 +10714,7 @@ "position": { "x": -5.624999999999998, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -10740,7 +10740,7 @@ "position": { "x": -5.624999999999998, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -10766,7 +10766,7 @@ "position": { "x": -5.624999999999998, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -10792,7 +10792,7 @@ "position": { "x": -5.624999999999998, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -10818,7 +10818,7 @@ "position": { "x": -5.624999999999998, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -10844,7 +10844,7 @@ "position": { "x": -5.624999999999998, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -10870,7 +10870,7 @@ "position": { "x": -5.624999999999998, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -10896,7 +10896,7 @@ "position": { "x": -5.624999999999998, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -10922,7 +10922,7 @@ "position": { "x": -5.624999999999998, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -10948,7 +10948,7 @@ "position": { "x": -5.624999999999998, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -10974,7 +10974,7 @@ "position": { "x": -5.624999999999998, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -11000,7 +11000,7 @@ "position": { "x": -5.624999999999998, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -11026,7 +11026,7 @@ "position": { "x": -5.624999999999998, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -11052,7 +11052,7 @@ "position": { "x": -5.624999999999998, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -11078,7 +11078,7 @@ "position": { "x": -5.624999999999998, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -11180,7 +11180,7 @@ "position": { "x": 3.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 10.0 }, @@ -11205,7 +11205,7 @@ "position": { "x": 3.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 } }, "status": "succeeded" @@ -11230,7 +11230,7 @@ "position": { "x": 3.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -11256,7 +11256,7 @@ "position": { "x": 3.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -11282,7 +11282,7 @@ "position": { "x": 3.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -11308,7 +11308,7 @@ "position": { "x": 3.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -11334,7 +11334,7 @@ "position": { "x": 3.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -11360,7 +11360,7 @@ "position": { "x": 3.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -11386,7 +11386,7 @@ "position": { "x": 3.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -11412,7 +11412,7 @@ "position": { "x": 3.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -11438,7 +11438,7 @@ "position": { "x": 3.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -11464,7 +11464,7 @@ "position": { "x": 3.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -11490,7 +11490,7 @@ "position": { "x": 3.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -11516,7 +11516,7 @@ "position": { "x": 3.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -11542,7 +11542,7 @@ "position": { "x": 3.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -11568,7 +11568,7 @@ "position": { "x": 3.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -11594,7 +11594,7 @@ "position": { "x": 3.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -11620,7 +11620,7 @@ "position": { "x": 3.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -11646,7 +11646,7 @@ "position": { "x": 3.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -11672,7 +11672,7 @@ "position": { "x": 3.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -11698,7 +11698,7 @@ "position": { "x": 3.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -11724,7 +11724,7 @@ "position": { "x": 3.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -11826,7 +11826,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 10.0 }, @@ -11851,7 +11851,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 } }, "status": "succeeded" @@ -11876,7 +11876,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -11902,7 +11902,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -11928,7 +11928,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -11954,7 +11954,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -11980,7 +11980,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -12006,7 +12006,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -12032,7 +12032,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -12058,7 +12058,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -12084,7 +12084,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -12110,7 +12110,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -12136,7 +12136,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -12162,7 +12162,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -12188,7 +12188,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -12214,7 +12214,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -12240,7 +12240,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -12266,7 +12266,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -12292,7 +12292,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -12318,7 +12318,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -12344,7 +12344,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -12370,7 +12370,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -13428,7 +13428,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -13453,7 +13453,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -13479,7 +13479,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -13505,7 +13505,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -13531,7 +13531,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -13557,7 +13557,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -13592,7 +13592,7 @@ "position": { "x": 14.38, "y": 74.24, - "z": 35.650000000000006 + "z": 35.65 } }, "status": "succeeded" @@ -13625,7 +13625,7 @@ "position": { "x": 14.38, "y": 74.24, - "z": 35.650000000000006 + "z": 35.65 } }, "status": "succeeded" @@ -13650,7 +13650,7 @@ "position": { "x": 14.38, "y": 74.24, - "z": 35.650000000000006 + "z": 35.65 }, "volume": 20.0 }, @@ -13741,7 +13741,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -13766,7 +13766,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -13792,7 +13792,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -13818,7 +13818,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -13844,7 +13844,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -13870,7 +13870,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -13905,7 +13905,7 @@ "position": { "x": 23.38, "y": 74.24, - "z": 35.650000000000006 + "z": 35.65 } }, "status": "succeeded" @@ -13938,7 +13938,7 @@ "position": { "x": 23.38, "y": 74.24, - "z": 35.650000000000006 + "z": 35.65 } }, "status": "succeeded" @@ -13963,7 +13963,7 @@ "position": { "x": 23.38, "y": 74.24, - "z": 35.650000000000006 + "z": 35.65 }, "volume": 20.0 }, @@ -14054,7 +14054,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -14079,7 +14079,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -14105,7 +14105,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -14131,7 +14131,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -14157,7 +14157,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -14183,7 +14183,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -14218,7 +14218,7 @@ "position": { "x": 32.38, "y": 74.24, - "z": 35.650000000000006 + "z": 35.65 } }, "status": "succeeded" @@ -14251,7 +14251,7 @@ "position": { "x": 32.38, "y": 74.24, - "z": 35.650000000000006 + "z": 35.65 } }, "status": "succeeded" @@ -14276,7 +14276,7 @@ "position": { "x": 32.38, "y": 74.24, - "z": 35.650000000000006 + "z": 35.65 }, "volume": 20.0 }, @@ -15315,7 +15315,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -15340,7 +15340,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -15366,7 +15366,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -15392,7 +15392,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -15418,7 +15418,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -15444,7 +15444,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -15479,7 +15479,7 @@ "position": { "x": 14.38, "y": 74.24, - "z": 35.650000000000006 + "z": 35.65 } }, "status": "succeeded" @@ -15512,7 +15512,7 @@ "position": { "x": 14.38, "y": 74.24, - "z": 35.650000000000006 + "z": 35.65 } }, "status": "succeeded" @@ -15537,7 +15537,7 @@ "position": { "x": 14.38, "y": 74.24, - "z": 35.650000000000006 + "z": 35.65 }, "volume": 20.0 }, @@ -15628,7 +15628,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -15653,7 +15653,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -15679,7 +15679,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -15705,7 +15705,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -15731,7 +15731,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -15757,7 +15757,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -15792,7 +15792,7 @@ "position": { "x": 23.38, "y": 74.24, - "z": 35.650000000000006 + "z": 35.65 } }, "status": "succeeded" @@ -15825,7 +15825,7 @@ "position": { "x": 23.38, "y": 74.24, - "z": 35.650000000000006 + "z": 35.65 } }, "status": "succeeded" @@ -15850,7 +15850,7 @@ "position": { "x": 23.38, "y": 74.24, - "z": 35.650000000000006 + "z": 35.65 }, "volume": 20.0 }, @@ -15941,7 +15941,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -15966,7 +15966,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -15992,7 +15992,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -16018,7 +16018,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -16044,7 +16044,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -16070,7 +16070,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -16105,7 +16105,7 @@ "position": { "x": 32.38, "y": 74.24, - "z": 35.650000000000006 + "z": 35.65 } }, "status": "succeeded" @@ -16138,7 +16138,7 @@ "position": { "x": 32.38, "y": 74.24, - "z": 35.650000000000006 + "z": 35.65 } }, "status": "succeeded" @@ -16163,7 +16163,7 @@ "position": { "x": 32.38, "y": 74.24, - "z": 35.650000000000006 + "z": 35.65 }, "volume": 20.0 }, @@ -17202,7 +17202,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -17227,7 +17227,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -17253,7 +17253,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -17279,7 +17279,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -17305,7 +17305,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -17331,7 +17331,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -17366,7 +17366,7 @@ "position": { "x": 14.38, "y": 74.24, - "z": 35.650000000000006 + "z": 35.65 } }, "status": "succeeded" @@ -17399,7 +17399,7 @@ "position": { "x": 14.38, "y": 74.24, - "z": 35.650000000000006 + "z": 35.65 } }, "status": "succeeded" @@ -17424,7 +17424,7 @@ "position": { "x": 14.38, "y": 74.24, - "z": 35.650000000000006 + "z": 35.65 }, "volume": 20.0 }, @@ -17515,7 +17515,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -17540,7 +17540,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -17566,7 +17566,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -17592,7 +17592,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -17618,7 +17618,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -17644,7 +17644,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -17679,7 +17679,7 @@ "position": { "x": 23.38, "y": 74.24, - "z": 35.650000000000006 + "z": 35.65 } }, "status": "succeeded" @@ -17712,7 +17712,7 @@ "position": { "x": 23.38, "y": 74.24, - "z": 35.650000000000006 + "z": 35.65 } }, "status": "succeeded" @@ -17737,7 +17737,7 @@ "position": { "x": 23.38, "y": 74.24, - "z": 35.650000000000006 + "z": 35.65 }, "volume": 20.0 }, @@ -17828,7 +17828,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -17853,7 +17853,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -17879,7 +17879,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -17905,7 +17905,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -17931,7 +17931,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -17957,7 +17957,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -17992,7 +17992,7 @@ "position": { "x": 32.38, "y": 74.24, - "z": 35.650000000000006 + "z": 35.65 } }, "status": "succeeded" @@ -18025,7 +18025,7 @@ "position": { "x": 32.38, "y": 74.24, - "z": 35.650000000000006 + "z": 35.65 } }, "status": "succeeded" @@ -18050,7 +18050,7 @@ "position": { "x": 32.38, "y": 74.24, - "z": 35.650000000000006 + "z": 35.65 }, "volume": 20.0 }, @@ -19417,7 +19417,7 @@ "offset": { "x": 1.040000000000001, "y": 0.0, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -19442,7 +19442,7 @@ "offset": { "x": 1.040000000000001, "y": 0.0, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -19467,7 +19467,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -19492,7 +19492,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -19517,7 +19517,7 @@ "offset": { "x": 0.0, "y": 1.0400000000000063, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -19542,7 +19542,7 @@ "offset": { "x": 0.0, "y": 1.0400000000000063, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -19567,7 +19567,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -19592,7 +19592,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -19617,7 +19617,7 @@ "offset": { "x": -1.040000000000001, "y": 0.0, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -19642,7 +19642,7 @@ "offset": { "x": -1.040000000000001, "y": 0.0, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -19667,7 +19667,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -19692,7 +19692,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -19717,7 +19717,7 @@ "offset": { "x": 0.0, "y": -1.0400000000000063, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -19742,7 +19742,7 @@ "offset": { "x": 0.0, "y": -1.0400000000000063, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -19767,7 +19767,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -19792,7 +19792,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -19818,7 +19818,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -19843,7 +19843,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -7.390000000000001 + "z": -7.389999999999997 }, "origin": "top" }, @@ -19867,7 +19867,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": 5.0 + "z": 5.000000000000007 }, "origin": "top" }, @@ -19901,7 +19901,7 @@ "position": { "x": 14.38, "y": 74.24, - "z": 34.650000000000006 + "z": 34.65 } }, "status": "succeeded" @@ -19915,7 +19915,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": 5.0 + "z": 5.000000000000007 }, "origin": "top" }, @@ -20015,7 +20015,7 @@ "offset": { "x": 1.0399999999999991, "y": 0.0, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -20040,7 +20040,7 @@ "offset": { "x": 1.0399999999999991, "y": 0.0, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -20065,7 +20065,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -20090,7 +20090,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -20115,7 +20115,7 @@ "offset": { "x": 0.0, "y": 1.0400000000000063, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -20140,7 +20140,7 @@ "offset": { "x": 0.0, "y": 1.0400000000000063, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -20165,7 +20165,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -20190,7 +20190,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -20215,7 +20215,7 @@ "offset": { "x": -1.0399999999999991, "y": 0.0, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -20240,7 +20240,7 @@ "offset": { "x": -1.0399999999999991, "y": 0.0, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -20265,7 +20265,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -20290,7 +20290,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -20315,7 +20315,7 @@ "offset": { "x": 0.0, "y": -1.0400000000000063, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -20340,7 +20340,7 @@ "offset": { "x": 0.0, "y": -1.0400000000000063, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -20365,7 +20365,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -20390,7 +20390,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -20416,7 +20416,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -20441,7 +20441,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -7.390000000000001 + "z": -7.389999999999997 }, "origin": "top" }, @@ -20465,7 +20465,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": 5.0 + "z": 5.000000000000007 }, "origin": "top" }, @@ -20499,7 +20499,7 @@ "position": { "x": 23.38, "y": 74.24, - "z": 34.650000000000006 + "z": 34.65 } }, "status": "succeeded" @@ -20513,7 +20513,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": 5.0 + "z": 5.000000000000007 }, "origin": "top" }, @@ -20613,7 +20613,7 @@ "offset": { "x": 1.0399999999999991, "y": 0.0, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -20638,7 +20638,7 @@ "offset": { "x": 1.0399999999999991, "y": 0.0, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -20663,7 +20663,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -20688,7 +20688,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -20713,7 +20713,7 @@ "offset": { "x": 0.0, "y": 1.0400000000000063, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -20738,7 +20738,7 @@ "offset": { "x": 0.0, "y": 1.0400000000000063, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -20763,7 +20763,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -20788,7 +20788,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -20813,7 +20813,7 @@ "offset": { "x": -1.0399999999999991, "y": 0.0, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -20838,7 +20838,7 @@ "offset": { "x": -1.0399999999999991, "y": 0.0, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -20863,7 +20863,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -20888,7 +20888,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -20913,7 +20913,7 @@ "offset": { "x": 0.0, "y": -1.0400000000000063, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -20938,7 +20938,7 @@ "offset": { "x": 0.0, "y": -1.0400000000000063, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -20963,7 +20963,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -20988,7 +20988,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -21014,7 +21014,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -21039,7 +21039,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -7.390000000000001 + "z": -7.389999999999997 }, "origin": "top" }, @@ -21063,7 +21063,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": 5.0 + "z": 5.000000000000007 }, "origin": "top" }, @@ -21097,7 +21097,7 @@ "position": { "x": 32.38, "y": 74.24, - "z": 34.650000000000006 + "z": 34.65 } }, "status": "succeeded" @@ -21111,7 +21111,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": 5.0 + "z": 5.000000000000007 }, "origin": "top" }, @@ -21311,7 +21311,7 @@ "position": { "x": -5.624999999999998, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 10.0 }, @@ -21337,7 +21337,7 @@ "position": { "x": -5.624999999999998, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 10.0 }, @@ -21363,7 +21363,7 @@ "position": { "x": -5.624999999999998, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 10.0 }, @@ -21389,7 +21389,7 @@ "position": { "x": -5.624999999999998, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 10.0 }, @@ -21415,7 +21415,7 @@ "position": { "x": -5.624999999999998, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 10.0 }, @@ -21441,7 +21441,7 @@ "position": { "x": -5.624999999999998, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 10.0 }, @@ -21467,7 +21467,7 @@ "position": { "x": -5.624999999999998, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 10.0 }, @@ -21569,7 +21569,7 @@ "position": { "x": 3.375, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 10.0 }, @@ -21595,7 +21595,7 @@ "position": { "x": 3.375, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 10.0 }, @@ -21621,7 +21621,7 @@ "position": { "x": 3.375, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 10.0 }, @@ -21647,7 +21647,7 @@ "position": { "x": 3.375, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 10.0 }, @@ -21673,7 +21673,7 @@ "position": { "x": 3.375, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 10.0 }, @@ -21699,7 +21699,7 @@ "position": { "x": 3.375, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 10.0 }, @@ -21725,7 +21725,7 @@ "position": { "x": 3.375, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 10.0 }, @@ -21827,7 +21827,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 10.0 }, @@ -21853,7 +21853,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 10.0 }, @@ -21879,7 +21879,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 10.0 }, @@ -21905,7 +21905,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 10.0 }, @@ -21931,7 +21931,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 10.0 }, @@ -21957,7 +21957,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 10.0 }, @@ -21983,7 +21983,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 10.0 }, @@ -22307,7 +22307,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -22333,7 +22333,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -14.780000000000001 + "z": -14.779999999999998 }, "origin": "top" }, @@ -22359,7 +22359,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -22461,7 +22461,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -22487,7 +22487,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -14.780000000000001 + "z": -14.779999999999998 }, "origin": "top" }, @@ -22513,7 +22513,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -22615,7 +22615,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -22641,7 +22641,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -14.780000000000001 + "z": -14.779999999999998 }, "origin": "top" }, @@ -22667,7 +22667,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -22935,7 +22935,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -22960,7 +22960,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -9.780000000000001 + "z": -9.779999999999998 }, "origin": "top" }, @@ -22985,7 +22985,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -9.780000000000001 + "z": -9.779999999999998 }, "origin": "top" }, @@ -23010,7 +23010,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -23035,7 +23035,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -23061,7 +23061,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -23086,7 +23086,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -9.780000000000001 + "z": -9.779999999999998 }, "origin": "top" }, @@ -23111,7 +23111,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -9.780000000000001 + "z": -9.779999999999998 }, "origin": "top" }, @@ -23137,7 +23137,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -9.780000000000001 + "z": -9.779999999999998 }, "origin": "top" }, @@ -23162,7 +23162,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -23187,7 +23187,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -23213,7 +23213,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -23238,7 +23238,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -9.780000000000001 + "z": -9.779999999999998 }, "origin": "top" }, @@ -23263,7 +23263,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -9.780000000000001 + "z": -9.779999999999998 }, "origin": "top" }, @@ -23288,7 +23288,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": 2.0 + "z": 2.000000000000007 }, "origin": "top" }, @@ -23312,7 +23312,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": 5.0 + "z": 5.000000000000007 }, "origin": "top" }, @@ -23346,7 +23346,7 @@ "position": { "x": 50.38, "y": 74.24, - "z": 34.650000000000006 + "z": 34.65 } }, "status": "succeeded" @@ -23360,7 +23360,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": 5.0 + "z": 5.000000000000007 }, "origin": "top" }, @@ -23617,7 +23617,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -23642,7 +23642,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -9.780000000000001 + "z": -9.779999999999998 }, "origin": "top" }, @@ -23667,7 +23667,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -9.780000000000001 + "z": -9.779999999999998 }, "origin": "top" }, @@ -23692,7 +23692,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -23717,7 +23717,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -23743,7 +23743,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -23768,7 +23768,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -9.780000000000001 + "z": -9.779999999999998 }, "origin": "top" }, @@ -23793,7 +23793,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -9.780000000000001 + "z": -9.779999999999998 }, "origin": "top" }, @@ -23819,7 +23819,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -9.780000000000001 + "z": -9.779999999999998 }, "origin": "top" }, @@ -23844,7 +23844,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -23869,7 +23869,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -23895,7 +23895,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -23920,7 +23920,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -9.780000000000001 + "z": -9.779999999999998 }, "origin": "top" }, @@ -23945,7 +23945,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -9.780000000000001 + "z": -9.779999999999998 }, "origin": "top" }, @@ -23970,7 +23970,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": 2.0 + "z": 2.000000000000007 }, "origin": "top" }, @@ -23994,7 +23994,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": 5.0 + "z": 5.000000000000007 }, "origin": "top" }, @@ -24028,7 +24028,7 @@ "position": { "x": 59.38, "y": 74.24, - "z": 34.650000000000006 + "z": 34.65 } }, "status": "succeeded" @@ -24042,7 +24042,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": 5.0 + "z": 5.000000000000007 }, "origin": "top" }, @@ -24299,7 +24299,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -24324,7 +24324,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -9.780000000000001 + "z": -9.779999999999998 }, "origin": "top" }, @@ -24349,7 +24349,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -9.780000000000001 + "z": -9.779999999999998 }, "origin": "top" }, @@ -24374,7 +24374,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -24399,7 +24399,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -24425,7 +24425,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -24450,7 +24450,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -9.780000000000001 + "z": -9.779999999999998 }, "origin": "top" }, @@ -24475,7 +24475,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -9.780000000000001 + "z": -9.779999999999998 }, "origin": "top" }, @@ -24501,7 +24501,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -9.780000000000001 + "z": -9.779999999999998 }, "origin": "top" }, @@ -24526,7 +24526,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -24551,7 +24551,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -24577,7 +24577,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -24602,7 +24602,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -9.780000000000001 + "z": -9.779999999999998 }, "origin": "top" }, @@ -24627,7 +24627,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -9.780000000000001 + "z": -9.779999999999998 }, "origin": "top" }, @@ -24652,7 +24652,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": 2.0 + "z": 2.000000000000007 }, "origin": "top" }, @@ -24676,7 +24676,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": 5.0 + "z": 5.000000000000007 }, "origin": "top" }, @@ -24710,7 +24710,7 @@ "position": { "x": 68.38, "y": 74.24, - "z": 34.650000000000006 + "z": 34.65 } }, "status": "succeeded" @@ -24724,7 +24724,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": 5.0 + "z": 5.000000000000007 }, "origin": "top" }, @@ -29887,7 +29887,7 @@ "offset": { "x": 1.0399999999999991, "y": 0.0, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -29912,7 +29912,7 @@ "offset": { "x": 1.0399999999999991, "y": 0.0, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -29937,7 +29937,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -29962,7 +29962,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -29987,7 +29987,7 @@ "offset": { "x": 0.0, "y": 1.0400000000000063, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -30012,7 +30012,7 @@ "offset": { "x": 0.0, "y": 1.0400000000000063, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -30037,7 +30037,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -30062,7 +30062,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -30087,7 +30087,7 @@ "offset": { "x": -1.0399999999999991, "y": 0.0, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -30112,7 +30112,7 @@ "offset": { "x": -1.0399999999999991, "y": 0.0, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -30137,7 +30137,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -30162,7 +30162,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -30187,7 +30187,7 @@ "offset": { "x": 0.0, "y": -1.0400000000000063, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -30212,7 +30212,7 @@ "offset": { "x": 0.0, "y": -1.0400000000000063, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -30237,7 +30237,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -30262,7 +30262,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -30288,7 +30288,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -30313,7 +30313,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -7.390000000000001 + "z": -7.389999999999997 }, "origin": "top" }, @@ -30337,7 +30337,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": 5.0 + "z": 5.000000000000007 }, "origin": "top" }, @@ -30371,7 +30371,7 @@ "position": { "x": 50.38, "y": 74.24, - "z": 34.650000000000006 + "z": 34.65 } }, "status": "succeeded" @@ -30385,7 +30385,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": 5.0 + "z": 5.000000000000007 }, "origin": "top" }, @@ -30485,7 +30485,7 @@ "offset": { "x": 1.0399999999999991, "y": 0.0, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -30510,7 +30510,7 @@ "offset": { "x": 1.0399999999999991, "y": 0.0, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -30535,7 +30535,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -30560,7 +30560,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -30585,7 +30585,7 @@ "offset": { "x": 0.0, "y": 1.0400000000000063, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -30610,7 +30610,7 @@ "offset": { "x": 0.0, "y": 1.0400000000000063, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -30635,7 +30635,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -30660,7 +30660,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -30685,7 +30685,7 @@ "offset": { "x": -1.0399999999999991, "y": 0.0, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -30710,7 +30710,7 @@ "offset": { "x": -1.0399999999999991, "y": 0.0, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -30735,7 +30735,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -30760,7 +30760,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -30785,7 +30785,7 @@ "offset": { "x": 0.0, "y": -1.0400000000000063, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -30810,7 +30810,7 @@ "offset": { "x": 0.0, "y": -1.0400000000000063, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -30835,7 +30835,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -30860,7 +30860,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -30886,7 +30886,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -30911,7 +30911,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -7.390000000000001 + "z": -7.389999999999997 }, "origin": "top" }, @@ -30935,7 +30935,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": 5.0 + "z": 5.000000000000007 }, "origin": "top" }, @@ -30969,7 +30969,7 @@ "position": { "x": 59.38, "y": 74.24, - "z": 34.650000000000006 + "z": 34.65 } }, "status": "succeeded" @@ -30983,7 +30983,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": 5.0 + "z": 5.000000000000007 }, "origin": "top" }, @@ -31083,7 +31083,7 @@ "offset": { "x": 1.0400000000000063, "y": 0.0, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -31108,7 +31108,7 @@ "offset": { "x": 1.0400000000000063, "y": 0.0, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -31133,7 +31133,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -31158,7 +31158,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -31183,7 +31183,7 @@ "offset": { "x": 0.0, "y": 1.0400000000000063, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -31208,7 +31208,7 @@ "offset": { "x": 0.0, "y": 1.0400000000000063, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -31233,7 +31233,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -31258,7 +31258,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -31283,7 +31283,7 @@ "offset": { "x": -1.0400000000000063, "y": 0.0, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -31308,7 +31308,7 @@ "offset": { "x": -1.0400000000000063, "y": 0.0, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -31333,7 +31333,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -31358,7 +31358,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -31383,7 +31383,7 @@ "offset": { "x": 0.0, "y": -1.0400000000000063, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -31408,7 +31408,7 @@ "offset": { "x": 0.0, "y": -1.0400000000000063, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -31433,7 +31433,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -31458,7 +31458,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -31484,7 +31484,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -31509,7 +31509,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -7.390000000000001 + "z": -7.389999999999997 }, "origin": "top" }, @@ -31533,7 +31533,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": 5.0 + "z": 5.000000000000007 }, "origin": "top" }, @@ -31567,7 +31567,7 @@ "position": { "x": 68.38, "y": 74.24, - "z": 34.650000000000006 + "z": 34.65 } }, "status": "succeeded" @@ -31581,7 +31581,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": 5.0 + "z": 5.000000000000007 }, "origin": "top" }, 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 74cf05cce32..87642f0e06f 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 @@ -96,6 +96,13 @@ }, { "commandType": "loadModule", + "error": { + "detail": "Cannot use Temperature Module in C3, not compatible with one or more of the following fixtures: Slot C4", + "errorCode": "4000", + "errorInfo": {}, + "errorType": "IncompatibleAddressableAreaError", + "wrappedErrors": [] + }, "notes": [], "params": { "location": { @@ -103,448 +110,7 @@ }, "model": "temperatureModuleV2" }, - "result": { - "definition": { - "calibrationPoint": { - "x": 11.7, - "y": 8.75, - "z": 80.09 - }, - "compatibleWith": [ - "temperatureModuleV1" - ], - "dimensions": { - "bareOverallHeight": 84.0, - "overLabwareHeight": 0.0 - }, - "displayName": "Temperature Module GEN2", - "gripperOffsets": { - "default": { - "dropOffset": { - "x": 0.0, - "y": 0.0, - "z": 1.0 - }, - "pickUpOffset": { - "x": 0.0, - "y": 0.0, - "z": 0.0 - } - } - }, - "labwareOffset": { - "x": -1.45, - "y": -0.15, - "z": 80.09 - }, - "model": "temperatureModuleV2", - "moduleType": "temperatureModuleType", - "otSharedSchema": "module/schemas/2", - "quirks": [], - "slotTransforms": { - "ot2_short_trash": { - "3": { - "labwareOffset": [ - [ - -1, - -0.15, - 0, - 0 - ], - [ - 0, - 0, - 0, - 1 - ], - [ - 0, - 0, - 0, - 1 - ], - [ - 0, - 0, - 0, - 1 - ] - ] - }, - "6": { - "labwareOffset": [ - [ - -1, - -0.15, - 0, - 0 - ], - [ - 0, - 0, - 0, - 1 - ], - [ - 0, - 0, - 0, - 1 - ], - [ - 0, - 0, - 0, - 1 - ] - ] - }, - "9": { - "labwareOffset": [ - [ - -1, - -0.15, - 0, - 0 - ], - [ - 0, - 0, - 0, - 1 - ], - [ - 0, - 0, - 0, - 1 - ], - [ - 0, - 0, - 0, - 1 - ] - ] - } - }, - "ot2_standard": { - "3": { - "labwareOffset": [ - [ - -1, - -0.3, - 0, - 0 - ], - [ - 0, - 0, - 0, - 1 - ], - [ - 0, - 0, - 0, - 1 - ], - [ - 0, - 0, - 0, - 1 - ] - ] - }, - "6": { - "labwareOffset": [ - [ - -1, - -0.3, - 0, - 0 - ], - [ - 0, - 0, - 0, - 1 - ], - [ - 0, - 0, - 0, - 1 - ], - [ - 0, - 0, - 0, - 1 - ] - ] - }, - "9": { - "labwareOffset": [ - [ - -1, - -0.3, - 0, - 0 - ], - [ - 0, - 0, - 0, - 1 - ], - [ - 0, - 0, - 0, - 1 - ], - [ - 0, - 0, - 0, - 1 - ] - ] - } - }, - "ot3_standard": { - "A1": { - "labwareOffset": [ - [ - -71.09, - 0, - 0, - 1 - ], - [ - 0, - 0, - 0, - 1 - ], - [ - 0, - 0, - 0.15, - 1 - ], - [ - 0, - 0, - 1, - 1.45 - ] - ] - }, - "A3": { - "labwareOffset": [ - [ - -71.09, - 0, - 0, - 1 - ], - [ - 0, - 0, - 0, - 1 - ], - [ - 0, - 0, - 0.15, - 1 - ], - [ - 0, - 0, - 1, - 1.45 - ] - ] - }, - "B1": { - "labwareOffset": [ - [ - -71.09, - 0, - 0, - 1 - ], - [ - 0, - 0, - 0, - 1 - ], - [ - 0, - 0, - 0.15, - 1 - ], - [ - 0, - 0, - 1, - 1.45 - ] - ] - }, - "B3": { - "labwareOffset": [ - [ - -71.09, - 0, - 0, - 1 - ], - [ - 0, - 0, - 0, - 1 - ], - [ - 0, - 0, - 0.15, - 1 - ], - [ - 0, - 0, - 1, - 1.45 - ] - ] - }, - "C1": { - "labwareOffset": [ - [ - -71.09, - 0, - 0, - 1 - ], - [ - 0, - 0, - 0, - 1 - ], - [ - 0, - 0, - 0.15, - 1 - ], - [ - 0, - 0, - 1, - 1.45 - ] - ] - }, - "C3": { - "labwareOffset": [ - [ - -71.09, - 0, - 0, - 1 - ], - [ - 0, - 0, - 0, - 1 - ], - [ - 0, - 0, - 0.15, - 1 - ], - [ - 0, - 0, - 1, - 1.45 - ] - ] - }, - "D1": { - "labwareOffset": [ - [ - -71.09, - 0, - 0, - 1 - ], - [ - 0, - 0, - 0, - 1 - ], - [ - 0, - 0, - 0.15, - 1 - ], - [ - 0, - 0, - 1, - 1.45 - ] - ] - }, - "D3": { - "labwareOffset": [ - [ - -71.09, - 0, - 0, - 1 - ], - [ - 0, - 0, - 0, - 1 - ], - [ - 0, - 0, - 0.15, - 1 - ], - [ - 0, - 0, - 1, - 1.45 - ] - ] - } - } - } - }, - "model": "temperatureModuleV2" - }, - "status": "succeeded" + "status": "failed" } ], "config": { @@ -556,21 +122,25 @@ }, "errors": [ { - "detail": "DeckConflictError [line 17]: nest_1_reservoir_290ml in slot C4 prevents temperatureModuleV2 from using slot C3.", + "detail": "ProtocolCommandFailedError [line 17]: Error 4000 GENERAL_ERROR (ProtocolCommandFailedError): IncompatibleAddressableAreaError: Cannot use Temperature Module in C3, not compatible with one or more of the following fixtures: Slot C4", "errorCode": "4000", "errorInfo": {}, "errorType": "ExceptionInProtocolError", "wrappedErrors": [ { - "detail": "opentrons.motion_planning.deck_conflict.DeckConflictError: nest_1_reservoir_290ml in slot C4 prevents temperatureModuleV2 from using slot C3.", + "detail": "IncompatibleAddressableAreaError: Cannot use Temperature Module in C3, not compatible with one or more of the following fixtures: Slot C4", "errorCode": "4000", - "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 124, 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 814, 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 424, 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 203, in check\n wrapped_deck_conflict.check(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/motion_planning/deck_conflict.py\", line 223, in check\n raise DeckConflictError(\n" - }, - "errorType": "PythonException", - "wrappedErrors": [] + "errorInfo": {}, + "errorType": "ProtocolCommandFailedError", + "wrappedErrors": [ + { + "detail": "Cannot use Temperature Module in C3, not compatible with one or more of the following fixtures: Slot C4", + "errorCode": "4000", + "errorInfo": {}, + "errorType": "IncompatibleAddressableAreaError", + "wrappedErrors": [] + } + ] } ] } @@ -615,14 +185,7 @@ "author": "Derek Maggio ", "protocolName": "QA Protocol - Analysis Error - Module in Staging Area Column 3" }, - "modules": [ - { - "location": { - "slotName": "C3" - }, - "model": "temperatureModuleV2" - } - ], + "modules": [], "pipettes": [], "robotType": "OT-3 Standard", "runTimeParameters": [] diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[37c9086bf4][Flex_P1000MLeft_P50MRight_HS_MM_TC_TM_2_15_ABR3_Illumina_DNA_Enrichment_v4].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[37c9086bf4][Flex_P1000MLeft_P50MRight_HS_MM_TC_TM_2_15_ABR3_Illumina_DNA_Enrichment_v4].json index 58867a05b3f..0693b30cc54 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[37c9086bf4][Flex_P1000MLeft_P50MRight_HS_MM_TC_TM_2_15_ABR3_Illumina_DNA_Enrichment_v4].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[37c9086bf4][Flex_P1000MLeft_P50MRight_HS_MM_TC_TM_2_15_ABR3_Illumina_DNA_Enrichment_v4].json @@ -11809,7 +11809,7 @@ "position": { "x": -5.624999999999998, "y": 356.2, - "z": 1.329999999999993 + "z": 1.33 } }, "status": "succeeded" @@ -11834,7 +11834,7 @@ "position": { "x": -5.624999999999998, "y": 356.2, - "z": 1.329999999999993 + "z": 1.33 }, "volume": 101.0 }, @@ -11860,7 +11860,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 101.0 }, @@ -11935,7 +11935,7 @@ "position": { "x": 3.375, "y": 356.2, - "z": 1.329999999999993 + "z": 1.33 } }, "status": "succeeded" @@ -11960,7 +11960,7 @@ "position": { "x": 3.375, "y": 356.2, - "z": 1.329999999999993 + "z": 1.33 }, "volume": 101.0 }, @@ -11986,7 +11986,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 101.0 }, @@ -12061,7 +12061,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 1.329999999999993 + "z": 1.33 } }, "status": "succeeded" @@ -12086,7 +12086,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 1.329999999999993 + "z": 1.33 }, "volume": 101.0 }, @@ -12112,7 +12112,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 101.0 }, @@ -12335,7 +12335,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 125.0 }, @@ -12360,7 +12360,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -12385,7 +12385,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 100.0 }, @@ -12410,7 +12410,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -12435,7 +12435,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 80.0 }, @@ -12461,7 +12461,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 80.0 }, @@ -12486,7 +12486,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -12511,7 +12511,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 100.0 }, @@ -12537,7 +12537,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 100.0 }, @@ -12562,7 +12562,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -12587,7 +12587,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 80.0 }, @@ -12613,7 +12613,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 80.0 }, @@ -12638,7 +12638,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -12663,7 +12663,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 100.0 }, @@ -12965,7 +12965,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 125.0 }, @@ -12990,7 +12990,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -13015,7 +13015,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 100.0 }, @@ -13040,7 +13040,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -13065,7 +13065,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 80.0 }, @@ -13091,7 +13091,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 80.0 }, @@ -13116,7 +13116,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -13141,7 +13141,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 100.0 }, @@ -13167,7 +13167,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 100.0 }, @@ -13192,7 +13192,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -13217,7 +13217,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 80.0 }, @@ -13243,7 +13243,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 80.0 }, @@ -13268,7 +13268,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -13293,7 +13293,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 100.0 }, @@ -13595,7 +13595,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 125.0 }, @@ -13620,7 +13620,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -13645,7 +13645,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 100.0 }, @@ -13670,7 +13670,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -13695,7 +13695,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 80.0 }, @@ -13721,7 +13721,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 80.0 }, @@ -13746,7 +13746,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -13771,7 +13771,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 100.0 }, @@ -13797,7 +13797,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 100.0 }, @@ -13822,7 +13822,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -13847,7 +13847,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 80.0 }, @@ -13873,7 +13873,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 80.0 }, @@ -13898,7 +13898,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -13923,7 +13923,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 100.0 }, @@ -15118,7 +15118,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -15144,7 +15144,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -15220,7 +15220,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -15246,7 +15246,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -15322,7 +15322,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -15348,7 +15348,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -16356,7 +16356,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -16382,7 +16382,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -16458,7 +16458,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -16484,7 +16484,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -16560,7 +16560,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -16586,7 +16586,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -17594,7 +17594,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -17620,7 +17620,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -17696,7 +17696,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -17722,7 +17722,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -17798,7 +17798,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -17824,7 +17824,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -18832,7 +18832,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -18858,7 +18858,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -18934,7 +18934,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -18960,7 +18960,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -19036,7 +19036,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -19062,7 +19062,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -19165,7 +19165,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 22.200000000000003 + "z": 22.2 } }, "status": "succeeded" @@ -19190,7 +19190,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 22.200000000000003 + "z": 22.2 }, "volume": 200.0 }, @@ -19216,7 +19216,7 @@ "position": { "x": 41.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -19291,7 +19291,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 22.200000000000003 + "z": 22.2 } }, "status": "succeeded" @@ -19316,7 +19316,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 22.200000000000003 + "z": 22.2 }, "volume": 200.0 }, @@ -19342,7 +19342,7 @@ "position": { "x": 50.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -19417,7 +19417,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 22.200000000000003 + "z": 22.2 } }, "status": "succeeded" @@ -19442,7 +19442,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 22.200000000000003 + "z": 22.2 }, "volume": 200.0 }, @@ -19468,7 +19468,7 @@ "position": { "x": 59.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -21541,7 +21541,7 @@ "position": { "x": 21.375000000000004, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 22.0 }, @@ -21667,7 +21667,7 @@ "position": { "x": 30.375000000000004, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 22.0 }, @@ -21793,7 +21793,7 @@ "position": { "x": 39.375, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 22.0 }, @@ -21905,7 +21905,7 @@ "position": { "x": 21.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 4.0 }, @@ -21930,7 +21930,7 @@ "position": { "x": 21.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 } }, "status": "succeeded" @@ -21955,7 +21955,7 @@ "position": { "x": 21.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -21981,7 +21981,7 @@ "position": { "x": 21.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -22083,7 +22083,7 @@ "position": { "x": 30.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 4.0 }, @@ -22108,7 +22108,7 @@ "position": { "x": 30.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 } }, "status": "succeeded" @@ -22133,7 +22133,7 @@ "position": { "x": 30.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -22159,7 +22159,7 @@ "position": { "x": 30.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -22261,7 +22261,7 @@ "position": { "x": 39.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 4.0 }, @@ -22286,7 +22286,7 @@ "position": { "x": 39.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 } }, "status": "succeeded" @@ -22311,7 +22311,7 @@ "position": { "x": 39.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -22337,7 +22337,7 @@ "position": { "x": 39.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -22479,7 +22479,7 @@ "position": { "x": 21.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 5.0 }, @@ -22581,7 +22581,7 @@ "position": { "x": 30.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 5.0 }, @@ -22683,7 +22683,7 @@ "position": { "x": 39.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 5.0 }, @@ -22795,7 +22795,7 @@ "position": { "x": 21.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -22820,7 +22820,7 @@ "position": { "x": 21.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 } }, "status": "succeeded" @@ -22845,7 +22845,7 @@ "position": { "x": 21.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 45.0 }, @@ -22871,7 +22871,7 @@ "position": { "x": 21.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 45.0 }, @@ -22973,7 +22973,7 @@ "position": { "x": 30.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -22998,7 +22998,7 @@ "position": { "x": 30.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 } }, "status": "succeeded" @@ -23023,7 +23023,7 @@ "position": { "x": 30.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 45.0 }, @@ -23049,7 +23049,7 @@ "position": { "x": 30.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 45.0 }, @@ -23151,7 +23151,7 @@ "position": { "x": 39.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -23176,7 +23176,7 @@ "position": { "x": 39.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 } }, "status": "succeeded" @@ -23201,7 +23201,7 @@ "position": { "x": 39.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 45.0 }, @@ -23227,7 +23227,7 @@ "position": { "x": 39.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 45.0 }, @@ -23378,7 +23378,7 @@ "position": { "x": 21.375000000000004, "y": 356.2, - "z": 1.329999999999993 + "z": 1.33 } }, "status": "succeeded" @@ -23403,7 +23403,7 @@ "position": { "x": 21.375000000000004, "y": 356.2, - "z": 1.329999999999993 + "z": 1.33 }, "volume": 46.0 }, @@ -23429,7 +23429,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 46.0 }, @@ -23504,7 +23504,7 @@ "position": { "x": 30.375000000000004, "y": 356.2, - "z": 1.329999999999993 + "z": 1.33 } }, "status": "succeeded" @@ -23529,7 +23529,7 @@ "position": { "x": 30.375000000000004, "y": 356.2, - "z": 1.329999999999993 + "z": 1.33 }, "volume": 46.0 }, @@ -23555,7 +23555,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 46.0 }, @@ -23630,7 +23630,7 @@ "position": { "x": 39.375, "y": 356.2, - "z": 1.329999999999993 + "z": 1.33 } }, "status": "succeeded" @@ -23655,7 +23655,7 @@ "position": { "x": 39.375, "y": 356.2, - "z": 1.329999999999993 + "z": 1.33 }, "volume": 46.0 }, @@ -23681,7 +23681,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 46.0 }, @@ -23845,7 +23845,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 40.5 }, @@ -23870,7 +23870,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -23895,7 +23895,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 60.0 }, @@ -23920,7 +23920,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -23945,7 +23945,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 60.0 }, @@ -23971,7 +23971,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 60.0 }, @@ -23996,7 +23996,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -24021,7 +24021,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 30.0 }, @@ -24047,7 +24047,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 60.0 }, @@ -24072,7 +24072,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -24097,7 +24097,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 60.0 }, @@ -24123,7 +24123,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 60.0 }, @@ -24148,7 +24148,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -24173,7 +24173,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 30.0 }, @@ -24423,7 +24423,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 40.5 }, @@ -24448,7 +24448,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -24473,7 +24473,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 60.0 }, @@ -24498,7 +24498,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -24523,7 +24523,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 60.0 }, @@ -24549,7 +24549,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 60.0 }, @@ -24574,7 +24574,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -24599,7 +24599,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 30.0 }, @@ -24625,7 +24625,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 60.0 }, @@ -24650,7 +24650,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -24675,7 +24675,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 60.0 }, @@ -24701,7 +24701,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 60.0 }, @@ -24726,7 +24726,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -24751,7 +24751,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 30.0 }, @@ -25001,7 +25001,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 40.5 }, @@ -25026,7 +25026,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -25051,7 +25051,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 60.0 }, @@ -25076,7 +25076,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -25101,7 +25101,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 60.0 }, @@ -25127,7 +25127,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 60.0 }, @@ -25152,7 +25152,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -25177,7 +25177,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 30.0 }, @@ -25203,7 +25203,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 60.0 }, @@ -25228,7 +25228,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -25253,7 +25253,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 60.0 }, @@ -25279,7 +25279,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 60.0 }, @@ -25304,7 +25304,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -25329,7 +25329,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 30.0 }, @@ -30796,7 +30796,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -30821,7 +30821,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -30896,7 +30896,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -30921,7 +30921,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -30996,7 +30996,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -31021,7 +31021,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -31096,7 +31096,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -31121,7 +31121,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -31147,7 +31147,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -31394,7 +31394,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -31419,7 +31419,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -31494,7 +31494,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -31519,7 +31519,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -31594,7 +31594,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -31619,7 +31619,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -31694,7 +31694,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -31719,7 +31719,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -31745,7 +31745,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -31992,7 +31992,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -32017,7 +32017,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -32092,7 +32092,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -32117,7 +32117,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -32192,7 +32192,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -32217,7 +32217,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -32292,7 +32292,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -32317,7 +32317,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -32343,7 +32343,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -32613,7 +32613,7 @@ "position": { "x": 48.375, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 31.0 }, @@ -32739,7 +32739,7 @@ "position": { "x": 57.375, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 31.0 }, @@ -32865,7 +32865,7 @@ "position": { "x": 66.375, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 31.0 }, @@ -33501,7 +33501,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -33553,7 +33553,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -33605,7 +33605,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -33657,7 +33657,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -33709,7 +33709,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -33761,7 +33761,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -33813,7 +33813,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -33865,7 +33865,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -33917,7 +33917,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -33969,7 +33969,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -34021,7 +34021,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -34073,7 +34073,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -34125,7 +34125,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -34177,7 +34177,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -34229,7 +34229,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -34281,7 +34281,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -34333,7 +34333,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -34385,7 +34385,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -34437,7 +34437,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -34489,7 +34489,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -34541,7 +34541,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -34593,7 +34593,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -34645,7 +34645,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -34697,7 +34697,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -34749,7 +34749,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -34801,7 +34801,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -34853,7 +34853,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -34905,7 +34905,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -34957,7 +34957,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -35009,7 +35009,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -35061,7 +35061,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -35113,7 +35113,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -35165,7 +35165,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -35217,7 +35217,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -35269,7 +35269,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -35321,7 +35321,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -36073,7 +36073,7 @@ "position": { "x": 48.375, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 32.0 }, @@ -36125,7 +36125,7 @@ "position": { "x": 57.375, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 32.0 }, @@ -36177,7 +36177,7 @@ "position": { "x": 66.375, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 32.0 }, @@ -36255,7 +36255,7 @@ "position": { "x": -5.624999999999998, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 30.0 }, @@ -36307,7 +36307,7 @@ "position": { "x": 3.375, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 30.0 }, @@ -36359,7 +36359,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 30.0 }, @@ -36506,7 +36506,7 @@ "position": { "x": -5.624999999999998, "y": 356.2, - "z": 1.329999999999993 + "z": 1.33 } }, "status": "succeeded" @@ -36531,7 +36531,7 @@ "position": { "x": -5.624999999999998, "y": 356.2, - "z": 1.329999999999993 + "z": 1.33 }, "volume": 101.0 }, @@ -36557,7 +36557,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 101.0 }, @@ -36632,7 +36632,7 @@ "position": { "x": 3.375, "y": 356.2, - "z": 1.329999999999993 + "z": 1.33 } }, "status": "succeeded" @@ -36657,7 +36657,7 @@ "position": { "x": 3.375, "y": 356.2, - "z": 1.329999999999993 + "z": 1.33 }, "volume": 101.0 }, @@ -36683,7 +36683,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 101.0 }, @@ -36758,7 +36758,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 1.329999999999993 + "z": 1.33 } }, "status": "succeeded" @@ -36783,7 +36783,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 1.329999999999993 + "z": 1.33 }, "volume": 101.0 }, @@ -36809,7 +36809,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 101.0 }, @@ -37032,7 +37032,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 125.0 }, @@ -37057,7 +37057,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -37082,7 +37082,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 100.0 }, @@ -37107,7 +37107,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -37132,7 +37132,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 80.0 }, @@ -37158,7 +37158,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 80.0 }, @@ -37183,7 +37183,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -37208,7 +37208,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 100.0 }, @@ -37234,7 +37234,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 100.0 }, @@ -37259,7 +37259,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -37284,7 +37284,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 80.0 }, @@ -37310,7 +37310,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 80.0 }, @@ -37335,7 +37335,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -37360,7 +37360,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 100.0 }, @@ -37662,7 +37662,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 125.0 }, @@ -37687,7 +37687,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -37712,7 +37712,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 100.0 }, @@ -37737,7 +37737,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -37762,7 +37762,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 80.0 }, @@ -37788,7 +37788,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 80.0 }, @@ -37813,7 +37813,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -37838,7 +37838,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 100.0 }, @@ -37864,7 +37864,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 100.0 }, @@ -37889,7 +37889,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -37914,7 +37914,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 80.0 }, @@ -37940,7 +37940,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 80.0 }, @@ -37965,7 +37965,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -37990,7 +37990,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 100.0 }, @@ -38292,7 +38292,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 125.0 }, @@ -38317,7 +38317,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -38342,7 +38342,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 100.0 }, @@ -38367,7 +38367,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -38392,7 +38392,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 80.0 }, @@ -38418,7 +38418,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 80.0 }, @@ -38443,7 +38443,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -38468,7 +38468,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 100.0 }, @@ -38494,7 +38494,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 100.0 }, @@ -38519,7 +38519,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -38544,7 +38544,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 80.0 }, @@ -38570,7 +38570,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 80.0 }, @@ -38595,7 +38595,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -38620,7 +38620,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 100.0 }, @@ -39815,7 +39815,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -39841,7 +39841,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -39917,7 +39917,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -39943,7 +39943,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -40019,7 +40019,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -40045,7 +40045,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -41053,7 +41053,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -41079,7 +41079,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -41155,7 +41155,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -41181,7 +41181,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -41257,7 +41257,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -41283,7 +41283,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -42291,7 +42291,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -42317,7 +42317,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -42393,7 +42393,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -42419,7 +42419,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -42495,7 +42495,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -42521,7 +42521,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -43529,7 +43529,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -43555,7 +43555,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -43631,7 +43631,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -43657,7 +43657,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -43733,7 +43733,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -43759,7 +43759,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -43862,7 +43862,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 22.200000000000003 + "z": 22.2 } }, "status": "succeeded" @@ -43887,7 +43887,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 22.200000000000003 + "z": 22.2 }, "volume": 200.0 }, @@ -43913,7 +43913,7 @@ "position": { "x": 41.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -43988,7 +43988,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 22.200000000000003 + "z": 22.2 } }, "status": "succeeded" @@ -44013,7 +44013,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 22.200000000000003 + "z": 22.2 }, "volume": 200.0 }, @@ -44039,7 +44039,7 @@ "position": { "x": 50.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -44114,7 +44114,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 22.200000000000003 + "z": 22.2 } }, "status": "succeeded" @@ -44139,7 +44139,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 22.200000000000003 + "z": 22.2 }, "volume": 200.0 }, @@ -44165,7 +44165,7 @@ "position": { "x": 59.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -46238,7 +46238,7 @@ "position": { "x": 21.375000000000004, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 22.0 }, @@ -46364,7 +46364,7 @@ "position": { "x": 30.375000000000004, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 22.0 }, @@ -46490,7 +46490,7 @@ "position": { "x": 39.375, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 22.0 }, @@ -46602,7 +46602,7 @@ "position": { "x": 21.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 4.0 }, @@ -46627,7 +46627,7 @@ "position": { "x": 21.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 } }, "status": "succeeded" @@ -46652,7 +46652,7 @@ "position": { "x": 21.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -46678,7 +46678,7 @@ "position": { "x": 21.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -46780,7 +46780,7 @@ "position": { "x": 30.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 4.0 }, @@ -46805,7 +46805,7 @@ "position": { "x": 30.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 } }, "status": "succeeded" @@ -46830,7 +46830,7 @@ "position": { "x": 30.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -46856,7 +46856,7 @@ "position": { "x": 30.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -46958,7 +46958,7 @@ "position": { "x": 39.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 4.0 }, @@ -46983,7 +46983,7 @@ "position": { "x": 39.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 } }, "status": "succeeded" @@ -47008,7 +47008,7 @@ "position": { "x": 39.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -47034,7 +47034,7 @@ "position": { "x": 39.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -47176,7 +47176,7 @@ "position": { "x": 21.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 5.0 }, @@ -47278,7 +47278,7 @@ "position": { "x": 30.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 5.0 }, @@ -47380,7 +47380,7 @@ "position": { "x": 39.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 5.0 }, @@ -47492,7 +47492,7 @@ "position": { "x": 21.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -47517,7 +47517,7 @@ "position": { "x": 21.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 } }, "status": "succeeded" @@ -47542,7 +47542,7 @@ "position": { "x": 21.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 45.0 }, @@ -47568,7 +47568,7 @@ "position": { "x": 21.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 45.0 }, @@ -47670,7 +47670,7 @@ "position": { "x": 30.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -47695,7 +47695,7 @@ "position": { "x": 30.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 } }, "status": "succeeded" @@ -47720,7 +47720,7 @@ "position": { "x": 30.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 45.0 }, @@ -47746,7 +47746,7 @@ "position": { "x": 30.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 45.0 }, @@ -47848,7 +47848,7 @@ "position": { "x": 39.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -47873,7 +47873,7 @@ "position": { "x": 39.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 } }, "status": "succeeded" @@ -47898,7 +47898,7 @@ "position": { "x": 39.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 45.0 }, @@ -47924,7 +47924,7 @@ "position": { "x": 39.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 45.0 }, @@ -48075,7 +48075,7 @@ "position": { "x": 21.375000000000004, "y": 356.2, - "z": 1.329999999999993 + "z": 1.33 } }, "status": "succeeded" @@ -48100,7 +48100,7 @@ "position": { "x": 21.375000000000004, "y": 356.2, - "z": 1.329999999999993 + "z": 1.33 }, "volume": 46.0 }, @@ -48126,7 +48126,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 46.0 }, @@ -48201,7 +48201,7 @@ "position": { "x": 30.375000000000004, "y": 356.2, - "z": 1.329999999999993 + "z": 1.33 } }, "status": "succeeded" @@ -48226,7 +48226,7 @@ "position": { "x": 30.375000000000004, "y": 356.2, - "z": 1.329999999999993 + "z": 1.33 }, "volume": 46.0 }, @@ -48252,7 +48252,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 46.0 }, @@ -48327,7 +48327,7 @@ "position": { "x": 39.375, "y": 356.2, - "z": 1.329999999999993 + "z": 1.33 } }, "status": "succeeded" @@ -48352,7 +48352,7 @@ "position": { "x": 39.375, "y": 356.2, - "z": 1.329999999999993 + "z": 1.33 }, "volume": 46.0 }, @@ -48378,7 +48378,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 46.0 }, @@ -48542,7 +48542,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 40.5 }, @@ -48567,7 +48567,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -48592,7 +48592,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 60.0 }, @@ -48617,7 +48617,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -48642,7 +48642,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 60.0 }, @@ -48668,7 +48668,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 60.0 }, @@ -48693,7 +48693,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -48718,7 +48718,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 30.0 }, @@ -48744,7 +48744,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 60.0 }, @@ -48769,7 +48769,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -48794,7 +48794,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 60.0 }, @@ -48820,7 +48820,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 60.0 }, @@ -48845,7 +48845,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -48870,7 +48870,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 30.0 }, @@ -49120,7 +49120,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 40.5 }, @@ -49145,7 +49145,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -49170,7 +49170,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 60.0 }, @@ -49195,7 +49195,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -49220,7 +49220,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 60.0 }, @@ -49246,7 +49246,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 60.0 }, @@ -49271,7 +49271,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -49296,7 +49296,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 30.0 }, @@ -49322,7 +49322,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 60.0 }, @@ -49347,7 +49347,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -49372,7 +49372,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 60.0 }, @@ -49398,7 +49398,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 60.0 }, @@ -49423,7 +49423,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -49448,7 +49448,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 30.0 }, @@ -49698,7 +49698,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 40.5 }, @@ -49723,7 +49723,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -49748,7 +49748,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 60.0 }, @@ -49773,7 +49773,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -49798,7 +49798,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 60.0 }, @@ -49824,7 +49824,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 60.0 }, @@ -49849,7 +49849,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -49874,7 +49874,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 30.0 }, @@ -49900,7 +49900,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 60.0 }, @@ -49925,7 +49925,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -49950,7 +49950,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 60.0 }, @@ -49976,7 +49976,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 60.0 }, @@ -50001,7 +50001,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -50026,7 +50026,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 30.0 }, @@ -55493,7 +55493,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -55518,7 +55518,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -55593,7 +55593,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -55618,7 +55618,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -55693,7 +55693,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -55718,7 +55718,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -55793,7 +55793,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -55818,7 +55818,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -55844,7 +55844,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -56091,7 +56091,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -56116,7 +56116,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -56191,7 +56191,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -56216,7 +56216,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -56291,7 +56291,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -56316,7 +56316,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -56391,7 +56391,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -56416,7 +56416,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -56442,7 +56442,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -56689,7 +56689,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -56714,7 +56714,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -56789,7 +56789,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -56814,7 +56814,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -56889,7 +56889,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -56914,7 +56914,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -56989,7 +56989,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -57014,7 +57014,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -57040,7 +57040,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -57310,7 +57310,7 @@ "position": { "x": 48.375, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 31.0 }, @@ -57436,7 +57436,7 @@ "position": { "x": 57.375, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 31.0 }, @@ -57562,7 +57562,7 @@ "position": { "x": 66.375, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 31.0 }, @@ -58198,7 +58198,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -58250,7 +58250,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -58302,7 +58302,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -58354,7 +58354,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -58406,7 +58406,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -58458,7 +58458,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -58510,7 +58510,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -58562,7 +58562,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -58614,7 +58614,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -58666,7 +58666,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -58718,7 +58718,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -58770,7 +58770,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -58822,7 +58822,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -58874,7 +58874,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -58926,7 +58926,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -58978,7 +58978,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -59030,7 +59030,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -59082,7 +59082,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -59134,7 +59134,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -59186,7 +59186,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -59238,7 +59238,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -59290,7 +59290,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -59342,7 +59342,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -59394,7 +59394,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -59446,7 +59446,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -59498,7 +59498,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -59550,7 +59550,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -59602,7 +59602,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -59654,7 +59654,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -59706,7 +59706,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -59758,7 +59758,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -59810,7 +59810,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -59862,7 +59862,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -59914,7 +59914,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -59966,7 +59966,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -60018,7 +60018,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -60770,7 +60770,7 @@ "position": { "x": 48.375, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 32.0 }, @@ -60822,7 +60822,7 @@ "position": { "x": 57.375, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 32.0 }, @@ -60874,7 +60874,7 @@ "position": { "x": 66.375, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 32.0 }, @@ -60952,7 +60952,7 @@ "position": { "x": -5.624999999999998, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 30.0 }, @@ -61004,7 +61004,7 @@ "position": { "x": 3.375, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 30.0 }, @@ -61056,7 +61056,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 30.0 }, @@ -61203,7 +61203,7 @@ "position": { "x": -5.624999999999998, "y": 356.2, - "z": 1.329999999999993 + "z": 1.33 } }, "status": "succeeded" @@ -61228,7 +61228,7 @@ "position": { "x": -5.624999999999998, "y": 356.2, - "z": 1.329999999999993 + "z": 1.33 }, "volume": 101.0 }, @@ -61254,7 +61254,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 101.0 }, @@ -61329,7 +61329,7 @@ "position": { "x": 3.375, "y": 356.2, - "z": 1.329999999999993 + "z": 1.33 } }, "status": "succeeded" @@ -61354,7 +61354,7 @@ "position": { "x": 3.375, "y": 356.2, - "z": 1.329999999999993 + "z": 1.33 }, "volume": 101.0 }, @@ -61380,7 +61380,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 101.0 }, @@ -61455,7 +61455,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 1.329999999999993 + "z": 1.33 } }, "status": "succeeded" @@ -61480,7 +61480,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 1.329999999999993 + "z": 1.33 }, "volume": 101.0 }, @@ -61506,7 +61506,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 101.0 }, @@ -61729,7 +61729,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 125.0 }, @@ -61754,7 +61754,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -61779,7 +61779,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 100.0 }, @@ -61804,7 +61804,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -61829,7 +61829,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 80.0 }, @@ -61855,7 +61855,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 80.0 }, @@ -61880,7 +61880,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -61905,7 +61905,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 100.0 }, @@ -61931,7 +61931,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 100.0 }, @@ -61956,7 +61956,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -61981,7 +61981,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 80.0 }, @@ -62007,7 +62007,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 80.0 }, @@ -62032,7 +62032,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -62057,7 +62057,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 100.0 }, @@ -62359,7 +62359,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 125.0 }, @@ -62384,7 +62384,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -62409,7 +62409,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 100.0 }, @@ -62434,7 +62434,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -62459,7 +62459,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 80.0 }, @@ -62485,7 +62485,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 80.0 }, @@ -62510,7 +62510,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -62535,7 +62535,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 100.0 }, @@ -62561,7 +62561,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 100.0 }, @@ -62586,7 +62586,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -62611,7 +62611,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 80.0 }, @@ -62637,7 +62637,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 80.0 }, @@ -62662,7 +62662,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -62687,7 +62687,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 100.0 }, @@ -62989,7 +62989,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 125.0 }, @@ -63014,7 +63014,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -63039,7 +63039,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 100.0 }, @@ -63064,7 +63064,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -63089,7 +63089,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 80.0 }, @@ -63115,7 +63115,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 80.0 }, @@ -63140,7 +63140,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -63165,7 +63165,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 100.0 }, @@ -63191,7 +63191,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 100.0 }, @@ -63216,7 +63216,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -63241,7 +63241,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 80.0 }, @@ -63267,7 +63267,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 80.0 }, @@ -63292,7 +63292,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -63317,7 +63317,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 100.0 }, @@ -64512,7 +64512,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -64538,7 +64538,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -64614,7 +64614,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -64640,7 +64640,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -64716,7 +64716,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -64742,7 +64742,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -65750,7 +65750,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -65776,7 +65776,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -65852,7 +65852,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -65878,7 +65878,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -65954,7 +65954,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -65980,7 +65980,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -66988,7 +66988,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -67014,7 +67014,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -67090,7 +67090,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -67116,7 +67116,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -67192,7 +67192,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -67218,7 +67218,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -68226,7 +68226,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -68252,7 +68252,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -68328,7 +68328,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -68354,7 +68354,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -68430,7 +68430,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -68456,7 +68456,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -68559,7 +68559,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 22.200000000000003 + "z": 22.2 } }, "status": "succeeded" @@ -68584,7 +68584,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 22.200000000000003 + "z": 22.2 }, "volume": 200.0 }, @@ -68610,7 +68610,7 @@ "position": { "x": 41.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -68685,7 +68685,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 22.200000000000003 + "z": 22.2 } }, "status": "succeeded" @@ -68710,7 +68710,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 22.200000000000003 + "z": 22.2 }, "volume": 200.0 }, @@ -68736,7 +68736,7 @@ "position": { "x": 50.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -68811,7 +68811,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 22.200000000000003 + "z": 22.2 } }, "status": "succeeded" @@ -68836,7 +68836,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 22.200000000000003 + "z": 22.2 }, "volume": 200.0 }, @@ -68862,7 +68862,7 @@ "position": { "x": 59.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -70935,7 +70935,7 @@ "position": { "x": 21.375000000000004, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 22.0 }, @@ -71061,7 +71061,7 @@ "position": { "x": 30.375000000000004, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 22.0 }, @@ -71187,7 +71187,7 @@ "position": { "x": 39.375, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 22.0 }, @@ -71299,7 +71299,7 @@ "position": { "x": 21.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 4.0 }, @@ -71324,7 +71324,7 @@ "position": { "x": 21.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 } }, "status": "succeeded" @@ -71349,7 +71349,7 @@ "position": { "x": 21.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -71375,7 +71375,7 @@ "position": { "x": 21.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -71477,7 +71477,7 @@ "position": { "x": 30.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 4.0 }, @@ -71502,7 +71502,7 @@ "position": { "x": 30.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 } }, "status": "succeeded" @@ -71527,7 +71527,7 @@ "position": { "x": 30.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -71553,7 +71553,7 @@ "position": { "x": 30.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -71655,7 +71655,7 @@ "position": { "x": 39.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 4.0 }, @@ -71680,7 +71680,7 @@ "position": { "x": 39.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 } }, "status": "succeeded" @@ -71705,7 +71705,7 @@ "position": { "x": 39.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -71731,7 +71731,7 @@ "position": { "x": 39.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -71873,7 +71873,7 @@ "position": { "x": 21.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 5.0 }, @@ -71975,7 +71975,7 @@ "position": { "x": 30.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 5.0 }, @@ -72077,7 +72077,7 @@ "position": { "x": 39.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 5.0 }, @@ -72189,7 +72189,7 @@ "position": { "x": 21.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -72214,7 +72214,7 @@ "position": { "x": 21.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 } }, "status": "succeeded" @@ -72239,7 +72239,7 @@ "position": { "x": 21.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 45.0 }, @@ -72265,7 +72265,7 @@ "position": { "x": 21.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 45.0 }, @@ -72367,7 +72367,7 @@ "position": { "x": 30.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -72392,7 +72392,7 @@ "position": { "x": 30.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 } }, "status": "succeeded" @@ -72417,7 +72417,7 @@ "position": { "x": 30.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 45.0 }, @@ -72443,7 +72443,7 @@ "position": { "x": 30.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 45.0 }, @@ -72545,7 +72545,7 @@ "position": { "x": 39.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -72570,7 +72570,7 @@ "position": { "x": 39.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 } }, "status": "succeeded" @@ -72595,7 +72595,7 @@ "position": { "x": 39.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 45.0 }, @@ -72621,7 +72621,7 @@ "position": { "x": 39.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 45.0 }, @@ -72772,7 +72772,7 @@ "position": { "x": 21.375000000000004, "y": 356.2, - "z": 1.329999999999993 + "z": 1.33 } }, "status": "succeeded" @@ -72797,7 +72797,7 @@ "position": { "x": 21.375000000000004, "y": 356.2, - "z": 1.329999999999993 + "z": 1.33 }, "volume": 46.0 }, @@ -72823,7 +72823,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 46.0 }, @@ -72898,7 +72898,7 @@ "position": { "x": 30.375000000000004, "y": 356.2, - "z": 1.329999999999993 + "z": 1.33 } }, "status": "succeeded" @@ -72923,7 +72923,7 @@ "position": { "x": 30.375000000000004, "y": 356.2, - "z": 1.329999999999993 + "z": 1.33 }, "volume": 46.0 }, @@ -72949,7 +72949,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 46.0 }, @@ -73024,7 +73024,7 @@ "position": { "x": 39.375, "y": 356.2, - "z": 1.329999999999993 + "z": 1.33 } }, "status": "succeeded" @@ -73049,7 +73049,7 @@ "position": { "x": 39.375, "y": 356.2, - "z": 1.329999999999993 + "z": 1.33 }, "volume": 46.0 }, @@ -73075,7 +73075,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 46.0 }, @@ -73239,7 +73239,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 40.5 }, @@ -73264,7 +73264,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -73289,7 +73289,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 60.0 }, @@ -73314,7 +73314,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -73339,7 +73339,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 60.0 }, @@ -73365,7 +73365,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 60.0 }, @@ -73390,7 +73390,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -73415,7 +73415,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 30.0 }, @@ -73441,7 +73441,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 60.0 }, @@ -73466,7 +73466,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -73491,7 +73491,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 60.0 }, @@ -73517,7 +73517,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 60.0 }, @@ -73542,7 +73542,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -73567,7 +73567,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 30.0 }, @@ -73817,7 +73817,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 40.5 }, @@ -73842,7 +73842,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -73867,7 +73867,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 60.0 }, @@ -73892,7 +73892,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -73917,7 +73917,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 60.0 }, @@ -73943,7 +73943,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 60.0 }, @@ -73968,7 +73968,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -73993,7 +73993,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 30.0 }, @@ -74019,7 +74019,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 60.0 }, @@ -74044,7 +74044,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -74069,7 +74069,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 60.0 }, @@ -74095,7 +74095,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 60.0 }, @@ -74120,7 +74120,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -74145,7 +74145,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 30.0 }, @@ -74395,7 +74395,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 40.5 }, @@ -74420,7 +74420,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -74445,7 +74445,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 60.0 }, @@ -74470,7 +74470,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -74495,7 +74495,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 60.0 }, @@ -74521,7 +74521,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 60.0 }, @@ -74546,7 +74546,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -74571,7 +74571,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 30.0 }, @@ -74597,7 +74597,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 60.0 }, @@ -74622,7 +74622,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -74647,7 +74647,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 60.0 }, @@ -74673,7 +74673,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 60.0 }, @@ -74698,7 +74698,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -74723,7 +74723,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 30.0 }, @@ -80190,7 +80190,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -80215,7 +80215,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -80290,7 +80290,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -80315,7 +80315,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -80390,7 +80390,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -80415,7 +80415,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -80490,7 +80490,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -80515,7 +80515,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -80541,7 +80541,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -80788,7 +80788,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -80813,7 +80813,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -80888,7 +80888,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -80913,7 +80913,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -80988,7 +80988,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -81013,7 +81013,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -81088,7 +81088,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -81113,7 +81113,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -81139,7 +81139,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -81386,7 +81386,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -81411,7 +81411,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -81486,7 +81486,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -81511,7 +81511,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -81586,7 +81586,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -81611,7 +81611,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -81686,7 +81686,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -81711,7 +81711,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -81737,7 +81737,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -82007,7 +82007,7 @@ "position": { "x": 48.375, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 31.0 }, @@ -82133,7 +82133,7 @@ "position": { "x": 57.375, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 31.0 }, @@ -82259,7 +82259,7 @@ "position": { "x": 66.375, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 31.0 }, @@ -82895,7 +82895,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -82947,7 +82947,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -82999,7 +82999,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -83051,7 +83051,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -83103,7 +83103,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -83155,7 +83155,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -83207,7 +83207,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -83259,7 +83259,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -83311,7 +83311,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -83363,7 +83363,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -83415,7 +83415,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -83467,7 +83467,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -83519,7 +83519,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -83571,7 +83571,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -83623,7 +83623,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -83675,7 +83675,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -83727,7 +83727,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -83779,7 +83779,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -83831,7 +83831,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -83883,7 +83883,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -83935,7 +83935,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -83987,7 +83987,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -84039,7 +84039,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -84091,7 +84091,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -84143,7 +84143,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -84195,7 +84195,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -84247,7 +84247,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -84299,7 +84299,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -84351,7 +84351,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -84403,7 +84403,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -84455,7 +84455,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -84507,7 +84507,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -84559,7 +84559,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -84611,7 +84611,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -84663,7 +84663,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -84715,7 +84715,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -85467,7 +85467,7 @@ "position": { "x": 48.375, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 32.0 }, @@ -85519,7 +85519,7 @@ "position": { "x": 57.375, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 32.0 }, @@ -85571,7 +85571,7 @@ "position": { "x": 66.375, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 32.0 }, @@ -85649,7 +85649,7 @@ "position": { "x": -5.624999999999998, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 30.0 }, @@ -85701,7 +85701,7 @@ "position": { "x": 3.375, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 30.0 }, @@ -85753,7 +85753,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 30.0 }, diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[3b1bfd0d2d][OT2_None_None_HS_2_16_AnalysisError_HeaterShakerConflictWithTrashBin2].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[3b1bfd0d2d][OT2_None_None_HS_2_16_AnalysisError_HeaterShakerConflictWithTrashBin2].json index a8091a65bdd..9ccf2c716e0 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[3b1bfd0d2d][OT2_None_None_HS_2_16_AnalysisError_HeaterShakerConflictWithTrashBin2].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[3b1bfd0d2d][OT2_None_None_HS_2_16_AnalysisError_HeaterShakerConflictWithTrashBin2].json @@ -478,7 +478,7 @@ "errorInfo": { "args": "('trash bin in slot 12 prevents heaterShakerModuleV1 from using slot 9.',)", "class": "DeckConflictError", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 124, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"OT2_None_None_HS_2_16_AnalysisError_HeaterShakerConflictWithTrashBin2.py\", line 11, 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 814, 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 424, 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 203, in check\n wrapped_deck_conflict.check(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/motion_planning/deck_conflict.py\", line 210, in check\n raise DeckConflictError(\n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 124, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"OT2_None_None_HS_2_16_AnalysisError_HeaterShakerConflictWithTrashBin2.py\", line 11, 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 814, 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 425, 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 203, in check\n wrapped_deck_conflict.check(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/motion_planning/deck_conflict.py\", line 210, in check\n raise DeckConflictError(\n" }, "errorType": "PythonException", "wrappedErrors": [] diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[5931902632][Flex_None_None_TC_2_16_AnalysisError_TrashBinAndThermocyclerConflict].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[5931902632][Flex_None_None_TC_2_16_AnalysisError_TrashBinAndThermocyclerConflict].json index 65d49f5fb6b..c43a9f80c61 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[5931902632][Flex_None_None_TC_2_16_AnalysisError_TrashBinAndThermocyclerConflict].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[5931902632][Flex_None_None_TC_2_16_AnalysisError_TrashBinAndThermocyclerConflict].json @@ -137,7 +137,7 @@ "errorInfo": { "args": "('thermocyclerModuleV2 in slot B1 prevents trash bin from using slot A1.',)", "class": "DeckConflictError", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 124, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"Flex_None_None_TC_2_16_AnalysisError_TrashBinAndThermocyclerConflict.py\", line 13, 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 518, in load_trash_bin\n trash_bin = self._core.load_trash_bin(slot_name, addressable_area_name)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/protocol.py\", line 529, in load_trash_bin\n self._add_disposal_location_to_engine(trash_bin)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/protocol.py\", line 148, in _add_disposal_location_to_engine\n deck_conflict.check(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/deck_conflict.py\", line 203, in check\n wrapped_deck_conflict.check(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/motion_planning/deck_conflict.py\", line 210, in check\n raise DeckConflictError(\n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 124, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"Flex_None_None_TC_2_16_AnalysisError_TrashBinAndThermocyclerConflict.py\", line 13, 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 518, in load_trash_bin\n trash_bin = self._core.load_trash_bin(slot_name, addressable_area_name)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/protocol.py\", line 530, in load_trash_bin\n self._add_disposal_location_to_engine(trash_bin)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/protocol.py\", line 149, in _add_disposal_location_to_engine\n deck_conflict.check(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/deck_conflict.py\", line 203, in check\n wrapped_deck_conflict.check(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/motion_planning/deck_conflict.py\", line 210, in check\n raise DeckConflictError(\n" }, "errorType": "PythonException", "wrappedErrors": [] diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7be98bf838][Flex_None_None_2_16_AnalysisError_TrashBinInCol2].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7be98bf838][Flex_None_None_2_16_AnalysisError_TrashBinInCol2].json index bd95551628d..89c43a035a9 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7be98bf838][Flex_None_None_2_16_AnalysisError_TrashBinInCol2].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7be98bf838][Flex_None_None_2_16_AnalysisError_TrashBinInCol2].json @@ -28,7 +28,7 @@ "errorInfo": { "args": "('Invalid location for trash bin: C2.\\nValid slots: Any slot in column 1 or 3.',)", "class": "InvalidTrashBinLocationError", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 124, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"Flex_None_None_2_16_AnalysisError_TrashBinInCol2.py\", line 15, 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 513, in load_trash_bin\n addressable_area_name = validation.ensure_and_convert_trash_bin_location(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/validation.py\", line 327, in ensure_and_convert_trash_bin_location\n raise InvalidTrashBinLocationError(\n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 124, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"Flex_None_None_2_16_AnalysisError_TrashBinInCol2.py\", line 15, 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 513, in load_trash_bin\n addressable_area_name = validation.ensure_and_convert_trash_bin_location(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/validation.py\", line 331, in ensure_and_convert_trash_bin_location\n raise InvalidTrashBinLocationError(\n" }, "errorType": "PythonException", "wrappedErrors": [] diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7ea2fdcec4][Flex_P1000MLeft_P50MRight_HS_MM_TC_TM_2_15_ABR3_Illumina_DNA_Enrichment].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7ea2fdcec4][Flex_P1000MLeft_P50MRight_HS_MM_TC_TM_2_15_ABR3_Illumina_DNA_Enrichment].json index bcdd4fb2a95..54003322788 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7ea2fdcec4][Flex_P1000MLeft_P50MRight_HS_MM_TC_TM_2_15_ABR3_Illumina_DNA_Enrichment].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7ea2fdcec4][Flex_P1000MLeft_P50MRight_HS_MM_TC_TM_2_15_ABR3_Illumina_DNA_Enrichment].json @@ -9597,7 +9597,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 50.0 }, @@ -9709,7 +9709,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 10.0 }, @@ -9821,7 +9821,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 10.0 }, @@ -9846,7 +9846,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 } }, "status": "succeeded" @@ -9871,7 +9871,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 90.0 }, @@ -9897,7 +9897,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 90.0 }, @@ -10033,7 +10033,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 121.0 }, @@ -10085,7 +10085,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 121.0 }, @@ -10137,7 +10137,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 121.0 }, @@ -10189,7 +10189,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 121.0 }, @@ -10274,7 +10274,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 1.329999999999993 + "z": 1.33 } }, "status": "succeeded" @@ -10299,7 +10299,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 1.329999999999993 + "z": 1.33 }, "volume": 101.0 }, @@ -10325,7 +10325,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 101.0 }, @@ -10808,7 +10808,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 125.0 }, @@ -10833,7 +10833,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -10858,7 +10858,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 100.0 }, @@ -10883,7 +10883,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -10908,7 +10908,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 80.0 }, @@ -10934,7 +10934,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 80.0 }, @@ -10959,7 +10959,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -10984,7 +10984,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 100.0 }, @@ -11010,7 +11010,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 100.0 }, @@ -11035,7 +11035,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -11060,7 +11060,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 80.0 }, @@ -11086,7 +11086,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 80.0 }, @@ -11111,7 +11111,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -11136,7 +11136,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 100.0 }, @@ -11717,7 +11717,7 @@ "position": { "x": 57.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 200.0 }, @@ -11743,7 +11743,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -12222,7 +12222,7 @@ "position": { "x": 66.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 200.0 }, @@ -12248,7 +12248,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -12727,7 +12727,7 @@ "position": { "x": 75.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 200.0 }, @@ -12753,7 +12753,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -13232,7 +13232,7 @@ "position": { "x": 84.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 200.0 }, @@ -13258,7 +13258,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -13361,7 +13361,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 22.200000000000003 + "z": 22.2 } }, "status": "succeeded" @@ -13386,7 +13386,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 22.200000000000003 + "z": 22.2 }, "volume": 200.0 }, @@ -13412,7 +13412,7 @@ "position": { "x": 41.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -14336,7 +14336,7 @@ "position": { "x": 30.375000000000004, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 22.0 }, @@ -14448,7 +14448,7 @@ "position": { "x": 30.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 4.0 }, @@ -14473,7 +14473,7 @@ "position": { "x": 84.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 } }, "status": "succeeded" @@ -14498,7 +14498,7 @@ "position": { "x": 84.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -14524,7 +14524,7 @@ "position": { "x": 84.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -14666,7 +14666,7 @@ "position": { "x": 30.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 5.0 }, @@ -14778,7 +14778,7 @@ "position": { "x": 30.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -14803,7 +14803,7 @@ "position": { "x": 30.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 } }, "status": "succeeded" @@ -14828,7 +14828,7 @@ "position": { "x": 30.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 45.0 }, @@ -14854,7 +14854,7 @@ "position": { "x": 30.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 45.0 }, @@ -15012,7 +15012,7 @@ "position": { "x": 30.375000000000004, "y": 356.2, - "z": 1.329999999999993 + "z": 1.33 } }, "status": "succeeded" @@ -15037,7 +15037,7 @@ "position": { "x": 30.375000000000004, "y": 356.2, - "z": 1.329999999999993 + "z": 1.33 }, "volume": 46.0 }, @@ -15063,7 +15063,7 @@ "position": { "x": 50.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 46.0 }, @@ -15227,7 +15227,7 @@ "position": { "x": 50.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 40.5 }, @@ -15252,7 +15252,7 @@ "position": { "x": 50.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -15277,7 +15277,7 @@ "position": { "x": 50.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 60.0 }, @@ -15302,7 +15302,7 @@ "position": { "x": 50.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -15327,7 +15327,7 @@ "position": { "x": 50.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 60.0 }, @@ -15353,7 +15353,7 @@ "position": { "x": 50.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 60.0 }, @@ -15378,7 +15378,7 @@ "position": { "x": 50.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -15403,7 +15403,7 @@ "position": { "x": 50.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 30.0 }, @@ -15429,7 +15429,7 @@ "position": { "x": 50.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 60.0 }, @@ -15454,7 +15454,7 @@ "position": { "x": 50.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -15479,7 +15479,7 @@ "position": { "x": 50.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 60.0 }, @@ -15505,7 +15505,7 @@ "position": { "x": 50.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 60.0 }, @@ -15530,7 +15530,7 @@ "position": { "x": 50.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -15555,7 +15555,7 @@ "position": { "x": 50.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 30.0 }, @@ -17654,7 +17654,7 @@ "position": { "x": 50.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -17679,7 +17679,7 @@ "position": { "x": 50.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -17754,7 +17754,7 @@ "position": { "x": 50.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -17779,7 +17779,7 @@ "position": { "x": 50.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -17854,7 +17854,7 @@ "position": { "x": 50.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -17879,7 +17879,7 @@ "position": { "x": 50.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -17954,7 +17954,7 @@ "position": { "x": 50.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -17979,7 +17979,7 @@ "position": { "x": 50.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -18005,7 +18005,7 @@ "position": { "x": 50.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[cda954ef1e][Flex_None_None_MM_2_16_AnalysisError_MagneticModuleInFlexProtocol].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[cda954ef1e][Flex_None_None_MM_2_16_AnalysisError_MagneticModuleInFlexProtocol].json index d88dc1e3bc9..baba4ad26fa 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[cda954ef1e][Flex_None_None_MM_2_16_AnalysisError_MagneticModuleInFlexProtocol].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[cda954ef1e][Flex_None_None_MM_2_16_AnalysisError_MagneticModuleInFlexProtocol].json @@ -17,18 +17,18 @@ }, "errors": [ { - "detail": "ValueError [line 15]: A magneticModuleType cannot be loaded into slot C1", + "detail": "ValueError [line 15]: Module Type magneticModuleType does not have a related fixture ID.", "errorCode": "4000", "errorInfo": {}, "errorType": "ExceptionInProtocolError", "wrappedErrors": [ { - "detail": "ValueError: A magneticModuleType cannot be loaded into slot C1", + "detail": "ValueError: Module Type magneticModuleType does not have a related fixture ID.", "errorCode": "4000", "errorInfo": { - "args": "('A magneticModuleType cannot be loaded into slot C1',)", + "args": "('Module Type magneticModuleType does not have a related fixture ID.',)", "class": "ValueError", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 124, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"Flex_None_None_MM_2_16_AnalysisError_MagneticModuleInFlexProtocol.py\", line 15, 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 814, 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 412, in load_module\n self._ensure_module_location(normalized_deck_slot, module_type)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/protocol.py\", line 631, in _ensure_module_location\n raise ValueError(f\"A {module_type.value} cannot be loaded into slot {slot}\")\n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 124, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"Flex_None_None_MM_2_16_AnalysisError_MagneticModuleInFlexProtocol.py\", line 15, 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 814, 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 413, in load_module\n self._ensure_module_location(normalized_deck_slot, module_type)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/protocol.py\", line 637, in _ensure_module_location\n cutout_fixture_id = ModuleType.to_module_fixture_id(module_type)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/hardware_control/modules/types.py\", line 79, in to_module_fixture_id\n raise ValueError(\n" }, "errorType": "PythonException", "wrappedErrors": [] diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ce0f35b3c6][Flex_None_None_TM_2_16_AnalysisError_ModuleInCol2].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ce0f35b3c6][Flex_None_None_TM_2_16_AnalysisError_ModuleInCol2].json index 9f85e6ecdcb..515359e1672 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ce0f35b3c6][Flex_None_None_TM_2_16_AnalysisError_ModuleInCol2].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ce0f35b3c6][Flex_None_None_TM_2_16_AnalysisError_ModuleInCol2].json @@ -28,7 +28,7 @@ "errorInfo": { "args": "('A temperatureModuleType cannot be loaded into slot C2',)", "class": "ValueError", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 124, 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_ModuleInCol2.py\", line 15, 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 814, 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 412, in load_module\n self._ensure_module_location(normalized_deck_slot, module_type)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/protocol.py\", line 631, in _ensure_module_location\n raise ValueError(f\"A {module_type.value} cannot be loaded into slot {slot}\")\n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 124, 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_ModuleInCol2.py\", line 15, 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 814, 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 413, in load_module\n self._ensure_module_location(normalized_deck_slot, module_type)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/protocol.py\", line 646, in _ensure_module_location\n raise ValueError(\n" }, "errorType": "PythonException", "wrappedErrors": [] diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d8ec3534d4][Flex_P1000_96_TM_2_16_AnalysisError_ModuleAndWasteChuteConflict].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d8ec3534d4][Flex_P1000_96_TM_2_16_AnalysisError_ModuleAndWasteChuteConflict].json index 85e9ca095c0..64864e5d715 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d8ec3534d4][Flex_P1000_96_TM_2_16_AnalysisError_ModuleAndWasteChuteConflict].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d8ec3534d4][Flex_P1000_96_TM_2_16_AnalysisError_ModuleAndWasteChuteConflict].json @@ -1219,6 +1219,24 @@ } }, "status": "succeeded" + }, + { + "commandType": "loadModule", + "error": { + "detail": "Cannot use Temperature Module in D3, not compatible with one or more of the following fixtures: Waste Chute", + "errorCode": "4000", + "errorInfo": {}, + "errorType": "IncompatibleAddressableAreaError", + "wrappedErrors": [] + }, + "notes": [], + "params": { + "location": { + "slotName": "D3" + }, + "model": "temperatureModuleV2" + }, + "status": "failed" } ], "config": { @@ -1230,17 +1248,25 @@ }, "errors": [ { - "detail": "IncompatibleAddressableAreaError [line 19]: Error 4000 GENERAL_ERROR (IncompatibleAddressableAreaError): Cannot use Slot D3, not compatible with one or more of the following fixtures: Waste Chute", + "detail": "ProtocolCommandFailedError [line 19]: Error 4000 GENERAL_ERROR (ProtocolCommandFailedError): IncompatibleAddressableAreaError: Cannot use Temperature Module in D3, not compatible with one or more of the following fixtures: Waste Chute", "errorCode": "4000", "errorInfo": {}, "errorType": "ExceptionInProtocolError", "wrappedErrors": [ { - "detail": "Cannot use Slot D3, not compatible with one or more of the following fixtures: Waste Chute", + "detail": "IncompatibleAddressableAreaError: Cannot use Temperature Module in D3, not compatible with one or more of the following fixtures: Waste Chute", "errorCode": "4000", "errorInfo": {}, - "errorType": "IncompatibleAddressableAreaError", - "wrappedErrors": [] + "errorType": "ProtocolCommandFailedError", + "wrappedErrors": [ + { + "detail": "Cannot use Temperature Module in D3, not compatible with one or more of the following fixtures: Waste Chute", + "errorCode": "4000", + "errorInfo": {}, + "errorType": "IncompatibleAddressableAreaError", + "wrappedErrors": [] + } + ] } ] } diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e49dae5293][OT2_None_None_HS_2_16_AnalysisError_HeaterShakerConflictWithTrashBin1].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e49dae5293][OT2_None_None_HS_2_16_AnalysisError_HeaterShakerConflictWithTrashBin1].json index 7c7138566d7..a54c74b3347 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e49dae5293][OT2_None_None_HS_2_16_AnalysisError_HeaterShakerConflictWithTrashBin1].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e49dae5293][OT2_None_None_HS_2_16_AnalysisError_HeaterShakerConflictWithTrashBin1].json @@ -478,7 +478,7 @@ "errorInfo": { "args": "('trash bin in slot 12 prevents heaterShakerModuleV1 from using slot 11.',)", "class": "DeckConflictError", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 124, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"OT2_None_None_HS_2_16_AnalysisError_HeaterShakerConflictWithTrashBin1.py\", line 11, 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 814, 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 424, 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 203, in check\n wrapped_deck_conflict.check(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/motion_planning/deck_conflict.py\", line 223, in check\n raise DeckConflictError(\n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 124, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"OT2_None_None_HS_2_16_AnalysisError_HeaterShakerConflictWithTrashBin1.py\", line 11, 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 814, 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 425, 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 203, in check\n wrapped_deck_conflict.check(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/motion_planning/deck_conflict.py\", line 223, in check\n raise DeckConflictError(\n" }, "errorType": "PythonException", "wrappedErrors": [] From f7dd2fa996a814fcb5e5506944e018ee9deda662 Mon Sep 17 00:00:00 2001 From: Josh McVey Date: Tue, 16 Apr 2024 07:28:00 -0500 Subject: [PATCH 134/194] chore(internal release): notes (#14914) # Internal release notes Get in the habit of updating these notes, even if only to create the comparison URL. Most often I expect these notes are not worth the effort but for stable internal releases will be a nice way to list what is tested. --- api/release-notes-internal.md | 14 ++++++++++++++ app-shell/build/release-notes-internal.md | 14 ++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/api/release-notes-internal.md b/api/release-notes-internal.md index f05cd2e2f1e..21ec74be010 100644 --- a/api/release-notes-internal.md +++ b/api/release-notes-internal.md @@ -2,6 +2,20 @@ For more details about this release, please see the full [technical change log][ [technical change log]: https://github.com/Opentrons/opentrons/releases +## Internal Release 1.4.0-alpha.0 + +This internal release is from the `edge` branch to contain rapid dev on new features for 7.3.0. This release is for internal testing purposes and if used may require a factory reset of the robot to return to a stable version. + + + +--- + +## Internal Release 1.3.0-alpha.0 + +This internal release is from the `edge` branch to contain rapid dev on new features for 7.3.0. This release is for internal testing purposes and if used may require a factory reset of the robot to return to a stable version. + + + --- # Internal Release 1.1.0 diff --git a/app-shell/build/release-notes-internal.md b/app-shell/build/release-notes-internal.md index a15d877c0ab..92e1af7c0d8 100644 --- a/app-shell/build/release-notes-internal.md +++ b/app-shell/build/release-notes-internal.md @@ -1,6 +1,20 @@ For more details about this release, please see the full [technical changelog][]. [technical change log]: https://github.com/Opentrons/opentrons/releases +## Internal Release 1.4.0-alpha.0 + +This internal release is from the `edge` branch to contain rapid dev on new features for 7.3.0. This release is for internal testing purposes and if used may require a factory reset of the robot to return to a stable version. + + + +--- + +## Internal Release 1.3.0-alpha.0 + +This internal release is from the `edge` branch to contain rapid dev on new features for 7.3.0. This release is for internal testing purposes and if used may require a factory reset of the robot to return to a stable version. + + + --- # Internal Release 1.1.0 From 829aa7986ce0c2f7a53511124219cfd84ed3c5dc Mon Sep 17 00:00:00 2001 From: Jethary Rader <66035149+jerader@users.noreply.github.com> Date: Tue, 16 Apr 2024 10:18:22 -0400 Subject: [PATCH 135/194] refactor(protocol-designer): increased test coverage for ConnectedStepItem (#14900) closes AUTH-300 --- .../src/components/lists/TitledStepList.tsx | 7 +- .../__tests__/ConnectedStepItem.test.tsx | 136 +++++++++++++++++- 2 files changed, 140 insertions(+), 3 deletions(-) diff --git a/protocol-designer/src/components/lists/TitledStepList.tsx b/protocol-designer/src/components/lists/TitledStepList.tsx index 0e3da16c542..1b88b291b1e 100644 --- a/protocol-designer/src/components/lists/TitledStepList.tsx +++ b/protocol-designer/src/components/lists/TitledStepList.tsx @@ -110,7 +110,12 @@ export function TitledStepList(props: Props): JSX.Element {

    )} {iconName && ( - + )}

    {props.title}

    {collapsible && ( diff --git a/protocol-designer/src/containers/__tests__/ConnectedStepItem.test.tsx b/protocol-designer/src/containers/__tests__/ConnectedStepItem.test.tsx index cce62e03887..4156202796b 100644 --- a/protocol-designer/src/containers/__tests__/ConnectedStepItem.test.tsx +++ b/protocol-designer/src/containers/__tests__/ConnectedStepItem.test.tsx @@ -1,7 +1,8 @@ import * as React from 'react' import { describe, it, beforeEach, vi } from 'vitest' import { screen } from '@testing-library/react' -import { fixture96Plate } from '@opentrons/shared-data' +import '@testing-library/jest-dom/vitest' +import { fixture96Plate, fixtureTiprack1000ul } from '@opentrons/shared-data' import { renderWithProviders } from '../../__testing-utils__' import { i18n } from '../../localization' import { @@ -49,8 +50,9 @@ const heaterShakerStepId = 'hsStepId' const thermocyclerStepId = 'tcStepId' const temperatureStepId = 'tempStepId' const moveLabwareStepId = 'moveLabwareId' +const mixStepId = 'mixStepId' +const moveLiquidStepId = 'moveLiquidStepId' -// TODO(jr, 4/8/24): add test coverage for mix and moveLiquid!!! describe('ConnectedStepItem', () => { let props: React.ComponentProps beforeEach(() => { @@ -89,6 +91,14 @@ describe('ConnectedStepItem', () => { stepType: 'moveLabware', id: moveLabwareStepId, }, + [mixStepId]: { + stepType: 'mix', + id: mixStepId, + }, + [moveLiquidStepId]: { + stepType: 'moveLiquid', + id: moveLiquidStepId, + }, }) vi.mocked(getArgsAndErrorsByStepId).mockReturnValue({ [pauseStepId]: { @@ -115,6 +125,14 @@ describe('ConnectedStepItem', () => { errors: false, stepArgs: null, }, + [mixStepId]: { + errors: false, + stepArgs: null, + }, + [moveLiquidStepId]: { + errors: false, + stepArgs: null, + }, }) vi.mocked(getErrorStepId).mockReturnValue(null) vi.mocked(getHasTimelineWarningsPerStep).mockReturnValue({ @@ -124,6 +142,8 @@ describe('ConnectedStepItem', () => { [thermocyclerStepId]: false, [temperatureStepId]: false, [moveLabwareStepId]: false, + [mixStepId]: false, + [moveLiquidStepId]: false, }) vi.mocked(getHasFormLevelWarningsPerStep).mockReturnValue({ [pauseStepId]: false, @@ -132,6 +152,8 @@ describe('ConnectedStepItem', () => { [thermocyclerStepId]: false, [temperatureStepId]: false, [moveLabwareStepId]: false, + [mixStepId]: false, + [moveLiquidStepId]: false, }) vi.mocked(getInitialDeckSetup).mockReturnValue({ pipettes: {}, @@ -179,6 +201,12 @@ describe('ConnectedStepItem', () => { slot: 'A2', def: fixture96Plate as LabwareDefinition2, }, + tipId: { + id: 'tipId', + labwareDefURI: `opentrons/${fixtureTiprack1000ul.parameters.loadName}/1`, + slot: 'D2', + def: fixtureTiprack1000ul as LabwareDefinition2, + }, }, }) vi.mocked(getCollapsedSteps).mockReturnValue({ @@ -188,6 +216,8 @@ describe('ConnectedStepItem', () => { [thermocyclerStepId]: true, [temperatureStepId]: true, [moveLabwareStepId]: true, + [mixStepId]: true, + [moveLiquidStepId]: true, }) vi.mocked(getHoveredSubstep).mockReturnValue(null) vi.mocked(getSelectedStepId).mockReturnValue(pauseStepId) @@ -198,6 +228,8 @@ describe('ConnectedStepItem', () => { thermocyclerStepId, moveLabwareStepId, temperatureStepId, + mixStepId, + moveLiquidStepId, ]) vi.mocked(getMultiSelectItemIds).mockReturnValue(null) vi.mocked(getMultiSelectLastSelected).mockReturnValue(null) @@ -260,6 +292,32 @@ describe('ConnectedStepItem', () => { newLocation: { slotName: 'B2' }, }, }, + [mixStepId]: { + substepType: 'sourceDest', + multichannel: false, + commandCreatorFnName: 'mix', + parentStepId: mixStepId, + rows: [ + { + activeTips: null, + }, + ], + }, + [moveLiquidStepId]: { + substepType: 'sourceDest', + multichannel: false, + commandCreatorFnName: 'transfer', + parentStepId: moveLiquidStepId, + rows: [ + { + activeTips: { labwareId: 'tipId', wellName: 'A1' }, + substepIndex: 2, + source: { well: 'A1', preIngreds: {}, postIngreds: {} }, + dest: { well: 'A1', preIngreds: {}, postIngreds: {} }, + volume: 50, + }, + ], + }, }) vi.mocked(labwareIngredSelectors.getLiquidNamesById).mockReturnValue({}) vi.mocked(getLabwareNicknamesById).mockReturnValue({}) @@ -286,6 +344,8 @@ describe('ConnectedStepItem', () => { [thermocyclerStepId]: true, [temperatureStepId]: true, [moveLabwareStepId]: true, + [mixStepId]: true, + [moveLiquidStepId]: true, }) vi.mocked(getSelectedStepId).mockReturnValue(magnetStepId) props.stepId = magnetStepId @@ -303,6 +363,8 @@ describe('ConnectedStepItem', () => { [thermocyclerStepId]: true, [temperatureStepId]: true, [moveLabwareStepId]: true, + [mixStepId]: true, + [moveLiquidStepId]: true, }) vi.mocked(getSelectedStepId).mockReturnValue(heaterShakerStepId) props.stepId = heaterShakerStepId @@ -326,6 +388,8 @@ describe('ConnectedStepItem', () => { [thermocyclerStepId]: false, [temperatureStepId]: true, [moveLabwareStepId]: true, + [mixStepId]: true, + [moveLiquidStepId]: true, }) vi.mocked(getSelectedStepId).mockReturnValue(thermocyclerStepId) props.stepId = thermocyclerStepId @@ -348,6 +412,8 @@ describe('ConnectedStepItem', () => { [thermocyclerStepId]: true, [temperatureStepId]: false, [moveLabwareStepId]: true, + [mixStepId]: true, + [moveLiquidStepId]: true, }) vi.mocked(getSelectedStepId).mockReturnValue(temperatureStepId) props.stepId = temperatureStepId @@ -367,6 +433,8 @@ describe('ConnectedStepItem', () => { [thermocyclerStepId]: true, [temperatureStepId]: true, [moveLabwareStepId]: false, + [mixStepId]: true, + [moveLiquidStepId]: true, }) vi.mocked(getSelectedStepId).mockReturnValue(moveLabwareStepId) props.stepId = moveLabwareStepId @@ -376,4 +444,68 @@ describe('ConnectedStepItem', () => { screen.getByText('labware') screen.getByText('new location') }) + it('renders an expanded step for mix', () => { + vi.mocked(getCollapsedSteps).mockReturnValue({ + [pauseStepId]: true, + [magnetStepId]: true, + [heaterShakerStepId]: true, + [thermocyclerStepId]: true, + [temperatureStepId]: true, + [moveLabwareStepId]: true, + [mixStepId]: false, + [moveLiquidStepId]: true, + }) + vi.mocked(getSelectedStepId).mockReturnValue(mixStepId) + props.stepId = mixStepId + render(props) + screen.getByText('2. mix') + screen.getByText('uL') + screen.getByText('μL') + }) + it('renders an expanded step for move liquid (transfer)', () => { + vi.mocked(getCollapsedSteps).mockReturnValue({ + [pauseStepId]: true, + [magnetStepId]: true, + [heaterShakerStepId]: true, + [thermocyclerStepId]: true, + [temperatureStepId]: true, + [moveLabwareStepId]: true, + [mixStepId]: true, + [moveLiquidStepId]: false, + }) + vi.mocked(getSelectedStepId).mockReturnValue(moveLiquidStepId) + props.stepId = moveLiquidStepId + render(props) + screen.getByText('2. transfer') + screen.getByText('ASPIRATE') + screen.getByText('DISPENSE') + screen.getAllByText('A1') + screen.getByText('50 μL') + }) + it('renders a timeline warning icon for move liquid', () => { + vi.mocked(getHasTimelineWarningsPerStep).mockReturnValue({ + [pauseStepId]: false, + [magnetStepId]: false, + [heaterShakerStepId]: false, + [thermocyclerStepId]: false, + [temperatureStepId]: false, + [moveLabwareStepId]: false, + [mixStepId]: false, + [moveLiquidStepId]: true, + }) + vi.mocked(getCollapsedSteps).mockReturnValue({ + [pauseStepId]: true, + [magnetStepId]: true, + [heaterShakerStepId]: true, + [thermocyclerStepId]: true, + [temperatureStepId]: true, + [moveLabwareStepId]: true, + [mixStepId]: true, + [moveLiquidStepId]: false, + }) + vi.mocked(getSelectedStepId).mockReturnValue(moveLiquidStepId) + props.stepId = moveLiquidStepId + render(props) + screen.getByTestId('TitledStepList_icon_alert-circle') + }) }) From df1d203c825588439e714b6a612fecb2268e2ff5 Mon Sep 17 00:00:00 2001 From: Nick Diehl <47604184+ncdiehl11@users.noreply.github.com> Date: Tue, 16 Apr 2024 12:29:52 -0400 Subject: [PATCH 136/194] fix(app): disable 'Run a protocol' robot overflow menu item if robot is busy (#14916) closes RQA-2570 --- .../organisms/Devices/RobotOverflowMenu.tsx | 30 ++++++++++--------- .../__tests__/RobotOverflowMenu.test.tsx | 13 +++----- 2 files changed, 20 insertions(+), 23 deletions(-) diff --git a/app/src/organisms/Devices/RobotOverflowMenu.tsx b/app/src/organisms/Devices/RobotOverflowMenu.tsx index 751729b25a8..dd624e2dcd5 100644 --- a/app/src/organisms/Devices/RobotOverflowMenu.tsx +++ b/app/src/organisms/Devices/RobotOverflowMenu.tsx @@ -84,25 +84,27 @@ export function RobotOverflowMenu(props: RobotOverflowMenuProps): JSX.Element { if (robot.status === CONNECTABLE && runId == null) { menuItems = ( <> - {!isRobotBusy ? ( - - {t('run_a_protocol')} - - ) : null} + + {t('run_a_protocol')} + {isRobotOnWrongVersionOfSoftware && ( {t('shared:a_software_update_is_available')} )} + {!isRobotOnWrongVersionOfSoftware && isRobotBusy && ( + + {t('shared:robot_is_busy')} + + )} ) => { return renderWithProviders( @@ -85,19 +86,13 @@ describe('RobotOverflowMenu', () => { expect(run).toBeDisabled() }) - it('should only render robot settings when e-stop is pressed or disconnected', () => { + it('disables the run a protocol menu item if robot is busy', () => { vi.mocked(useCurrentRunId).mockReturnValue(null) - vi.mocked(getRobotUpdateDisplayInfo).mockReturnValue({ - autoUpdateAction: 'upgrade', - autoUpdateDisabledReason: null, - updateFromFileDisabledReason: null, - }) - vi.mocked(useIsRobotBusy).mockReturnValue(true) render(props) const btn = screen.getByLabelText('RobotOverflowMenu_button') fireEvent.click(btn) - expect(screen.queryByText('Run a protocol')).not.toBeInTheDocument() - screen.getByText('Robot settings') + const run = screen.getByText('Run a protocol') + expect(run).toBeDisabled() }) }) From 59d4cc618e56181cf26597b8173e7593f791cb72 Mon Sep 17 00:00:00 2001 From: Jethary Rader <66035149+jerader@users.noreply.github.com> Date: Tue, 16 Apr 2024 13:03:53 -0400 Subject: [PATCH 137/194] =?UTF-8?q?fix(protocol-designer):=20moveLabware?= =?UTF-8?q?=20newLocation=20error=20accounts=20for=20cu=E2=80=A6=20(#14917?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …stom labware closes RESC-243 and RQA-2573 --- .../src/steplist/formLevel/moveLabwareFormErrors.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/protocol-designer/src/steplist/formLevel/moveLabwareFormErrors.ts b/protocol-designer/src/steplist/formLevel/moveLabwareFormErrors.ts index 28828c7524d..b9ee871772d 100644 --- a/protocol-designer/src/steplist/formLevel/moveLabwareFormErrors.ts +++ b/protocol-designer/src/steplist/formLevel/moveLabwareFormErrors.ts @@ -1,8 +1,9 @@ -import { LabwareLocation } from '@opentrons/shared-data' +import { getLabwareDefIsStandard } from '@opentrons/shared-data' import { COMPATIBLE_LABWARE_ALLOWLIST_BY_MODULE_TYPE, COMPATIBLE_LABWARE_ALLOWLIST_FOR_ADAPTER, } from '../../utils/labwareModuleCompatibility' +import type { LabwareLocation } from '@opentrons/shared-data' import type { InvariantContext, LabwareEntity, @@ -11,15 +12,18 @@ import type { ProfileFormError } from './profileErrors' type HydratedFormData = any -// TODO(Jr, 1/16/24): look into the use case of this util since the i18n strings -// previously listed in this util were not found in any json. const getMoveLabwareError = ( labware: LabwareEntity, newLocation: LabwareLocation, invariantContext: InvariantContext ): string | null => { let errorString: string | null = null - if (labware == null || newLocation == null || newLocation === 'offDeck') + if ( + labware == null || + newLocation == null || + newLocation === 'offDeck' || + !getLabwareDefIsStandard(labware?.def) + ) return null const selectedLabwareDefUri = labware?.labwareDefURI if ('moduleId' in newLocation) { From 9152f22b42c9615e6b938c1155a8b7475d05d017 Mon Sep 17 00:00:00 2001 From: Jamey Huffnagle Date: Tue, 16 Apr 2024 13:28:58 -0400 Subject: [PATCH 138/194] fix(app): Handle Unsafe Move to Plunger during Drop-Tip (#14910) Closes EXEC-186 If the gantry is not homed and a powercycle occurs, drop-tip wizard cannot proceed with flows. An error is raised during the flow, and ultimately a home command is dispatched that has the side effect of potentially aspirating liquid into the pipette, damaging it. We special case home errors to prevent this. The primary functional difference is now that any time an error occurs, exiting the wizard via the header should not home the gantry. Homing as a result of an error should only occur when the "Confirm removal and home" button is presented and clicked. --- .../localization/en/drop_tip_wizard.json | 3 + .../DropTipWizard/BeforeBeginning.tsx | 24 +- .../DropTipWizard/ChooseLocation.tsx | 16 +- .../DropTipWizard/ExitConfirmation.tsx | 8 +- .../organisms/DropTipWizard/JogToPosition.tsx | 11 +- .../DropTipWizard/__tests__/utils.test.tsx | 131 +++++ app/src/organisms/DropTipWizard/constants.ts | 4 + app/src/organisms/DropTipWizard/index.tsx | 467 ++++++++++-------- app/src/organisms/DropTipWizard/utils.tsx | 185 +++++++ 9 files changed, 601 insertions(+), 248 deletions(-) create mode 100644 app/src/organisms/DropTipWizard/__tests__/utils.test.tsx create mode 100644 app/src/organisms/DropTipWizard/utils.tsx diff --git a/app/src/assets/localization/en/drop_tip_wizard.json b/app/src/assets/localization/en/drop_tip_wizard.json index 66924d00210..fc3bf25dfdf 100644 --- a/app/src/assets/localization/en/drop_tip_wizard.json +++ b/app/src/assets/localization/en/drop_tip_wizard.json @@ -3,10 +3,12 @@ "begin_removal": "Begin removal", "blowout_complete": "blowout complete", "blowout_liquid": "Blow out liquid", + "cant_safely_drop_tips": "Can't safely drop tips", "choose_blowout_location": "choose blowout location", "choose_drop_tip_location": "choose tip-drop location", "confirm_blowout_location": "Is the pipette positioned where the liquids should be blown out?", "confirm_drop_tip_location": "Is the pipette positioned where the tips should be dropped?", + "confirm_removal_and_home": "Confirm removal and home", "drop_tip_complete": "tip drop complete", "drop_tip_failed": "The drop tip could not be completed. Contact customer support for assistance.", "drop_tips": "drop tips", @@ -21,6 +23,7 @@ "position_the_pipette": "position the pipette", "remove_the_tips": "You may want to remove the tips from the {{mount}} Pipette before using it again in a protocol.", "remove_the_tips_from_pipette": "You may want to remove the tips from the pipette before using it again in a protocol.", + "remove_the_tips_manually": "Remove the tips manually. Then home the gantry. Homing with tips attached could pull liquid into the pipette and damage it.", "remove_tips": "Remove tips", "select_blowout_slot": "You can blow out liquid into a labware or dispose of it.Select the slot where you want to blow out the liquid on the deck map to the right. Once confirmed, the gantry will move to the chosen slot.", "select_blowout_slot_odd": "You can blow out liquid into a labware or dispose of it.
    After the gantry moves to the chosen slot, use the jog controls to move the pipette to the exact position for blowing out.", diff --git a/app/src/organisms/DropTipWizard/BeforeBeginning.tsx b/app/src/organisms/DropTipWizard/BeforeBeginning.tsx index 69a8d7de694..cd21cc3e1a4 100644 --- a/app/src/organisms/DropTipWizard/BeforeBeginning.tsx +++ b/app/src/organisms/DropTipWizard/BeforeBeginning.tsx @@ -24,30 +24,20 @@ import { import { SmallButton, MediumButton } from '../../atoms/buttons' import { InProgressModal } from '../../molecules/InProgressModal/InProgressModal' -// import { NeedHelpLink } from '../CalibrationPanels' import blowoutVideo from '../../assets/videos/droptip-wizard/Blowout-Liquid.webm' import droptipVideo from '../../assets/videos/droptip-wizard/Drop-tip.webm' -// TODO: get help link article URL -// const NEED_HELP_URL = '' - interface BeforeBeginningProps { setShouldDispenseLiquid: (shouldDispenseLiquid: boolean) => void createdMaintenanceRunId: string | null isOnDevice: boolean - isRobotMoving: boolean } export const BeforeBeginning = ( props: BeforeBeginningProps ): JSX.Element | null => { - const { - setShouldDispenseLiquid, - createdMaintenanceRunId, - isOnDevice, - isRobotMoving, - } = props + const { setShouldDispenseLiquid, createdMaintenanceRunId, isOnDevice } = props const { i18n, t } = useTranslation(['drop_tip_wizard', 'shared']) const [flowType, setFlowType] = React.useState< 'liquid_and_tips' | 'only_tips' | null @@ -57,16 +47,8 @@ export const BeforeBeginning = ( setShouldDispenseLiquid(flowType === 'liquid_and_tips') } - if (isRobotMoving || createdMaintenanceRunId == null) { - return ( - - ) + if (createdMaintenanceRunId == null) { + return } if (isOnDevice) { diff --git a/app/src/organisms/DropTipWizard/ChooseLocation.tsx b/app/src/organisms/DropTipWizard/ChooseLocation.tsx index 8050c776698..7a86da67223 100644 --- a/app/src/organisms/DropTipWizard/ChooseLocation.tsx +++ b/app/src/organisms/DropTipWizard/ChooseLocation.tsx @@ -22,15 +22,13 @@ import { import { getDeckDefFromRobotType } from '@opentrons/shared-data' import { SmallButton } from '../../atoms/buttons' -import { InProgressModal } from '../../molecules/InProgressModal/InProgressModal' -// import { NeedHelpLink } from '../CalibrationPanels' import { TwoUpTileLayout } from '../LabwarePositionCheck/TwoUpTileLayout' import type { CommandData } from '@opentrons/api-client' import type { AddressableAreaName, RobotType } from '@opentrons/shared-data' +import type { ErrorDetails } from './utils' // TODO: get help link article URL -// const NEED_HELP_URL = '' interface ChooseLocationProps { handleProceed: () => void @@ -41,9 +39,8 @@ interface ChooseLocationProps { moveToAddressableArea: ( addressableArea: AddressableAreaName ) => Promise - isRobotMoving: boolean isOnDevice: boolean - setErrorMessage: (arg0: string) => void + setErrorDetails: (errorDetails: ErrorDetails) => void } export const ChooseLocation = ( @@ -56,9 +53,8 @@ export const ChooseLocation = ( body, robotType, moveToAddressableArea, - isRobotMoving, isOnDevice, - setErrorMessage, + setErrorDetails, } = props const { i18n, t } = useTranslation(['drop_tip_wizard', 'shared']) const deckDef = getDeckDefFromRobotType(robotType) @@ -74,14 +70,10 @@ export const ChooseLocation = ( if (deckSlot != null) { moveToAddressableArea(deckSlot) .then(() => handleProceed()) - .catch(e => setErrorMessage(`${e.message}`)) + .catch(e => setErrorDetails({ message: `${e.message}` })) } } - if (isRobotMoving) { - return - } - if (isOnDevice) { return ( void handleGoBack: () => void - isRobotMoving: boolean } export function ExitConfirmation(props: ExitConfirmationProps): JSX.Element { - const { handleGoBack, handleExit, isRobotMoving } = props + const { handleGoBack, handleExit } = props const { i18n, t } = useTranslation(['drop_tip_wizard', 'shared']) const flowTitle = t('drop_tips') const isOnDevice = useSelector(getIsOnDevice) - if (isRobotMoving) { - return - } - return ( void body: string - isRobotMoving: boolean currentStep: string isOnDevice: boolean } @@ -161,7 +160,6 @@ export const JogToPosition = ( handleJog, handleProceed, body, - isRobotMoving, currentStep, isOnDevice, } = props @@ -171,10 +169,10 @@ export const JogToPosition = ( setShowPositionConfirmation, ] = React.useState(false) // Includes special case homing only present in this step. - const [isRobotInMotion, setIsRobotInMotion] = React.useState(isRobotMoving) + const [isRobotInMotion, setIsRobotInMotion] = React.useState(false) const onGoBack = (): void => { - setIsRobotInMotion(() => true) + setIsRobotInMotion(true) handleGoBack() } @@ -201,11 +199,6 @@ export const JogToPosition = ( ) } - // Moving due to "Exit" or "Go back" click. - if (isRobotInMotion) { - return - } - if (isOnDevice) { return ( { + let props: UseDropTipErrorComponentsProps + let mockOnClose: Mock + let mockTranslation: Mock + let mockChainRunCommands: Mock + + beforeEach(() => { + mockOnClose = vi.fn() + mockTranslation = vi.fn() + mockChainRunCommands = vi.fn() + + props = { + maintenanceRunId: MOCK_MAINTENANCE_RUN_ID, + onClose: mockOnClose, + errorDetails: { + type: MOCK_ERROR_TYPE, + message: MOCK_ERROR_MESSAGE, + header: MOCK_ERROR_HEADER, + }, + isOnDevice: true, + t: mockTranslation, + chainRunCommands: mockChainRunCommands, + } + }) + + it('should return the generic text and error message if there is are no special-cased error details', () => { + const result = useDropTipErrorComponents(props) + expect(result.button).toBeNull() + render(result.subHeader) + expect(mockTranslation).toHaveBeenCalledWith('drop_tip_failed') + screen.getByText(MOCK_ERROR_MESSAGE) + }) + + it('should return a generic message only if there are no error details', () => { + props.errorDetails = null + const result = useDropTipErrorComponents(props) + expect(result.button).toBeNull() + render(result.subHeader) + expect(mockTranslation).toHaveBeenCalledWith('drop_tip_failed') + expect(screen.queryByText(MOCK_ERROR_MESSAGE)).not.toBeInTheDocument() + }) + + it(`should return correct special components if error type is ${DROP_TIP_SPECIAL_ERROR_TYPES.MUST_HOME_ERROR}`, () => { + // @ts-expect-error errorDetails is in fact not null in the test. + props.errorDetails.type = DROP_TIP_SPECIAL_ERROR_TYPES.MUST_HOME_ERROR + const result = useDropTipErrorComponents(props) + expect(mockTranslation).toHaveBeenCalledWith('confirm_removal_and_home') + + render(result.button) + const btn = screen.getByRole('button') + fireEvent.click(btn) + expect(mockOnClose).toHaveBeenCalled() + expect(mockChainRunCommands).toHaveBeenCalledWith( + MOCK_MAINTENANCE_RUN_ID, + [ + { + commandType: 'home' as const, + params: {}, + }, + ], + true + ) + + render(result.subHeader) + screen.getByText(MOCK_ERROR_MESSAGE) + }) +}) + +describe('useWizardExitHeader', () => { + let props: UseWizardExitHeaderProps + let mockHandleCleanUpAndClose: Mock + let mockConfirmExit: Mock + + beforeEach(() => { + mockHandleCleanUpAndClose = vi.fn() + mockConfirmExit = vi.fn() + + props = { + isFinalStep: true, + hasInitiatedExit: false, + errorDetails: null, + handleCleanUpAndClose: mockHandleCleanUpAndClose, + confirmExit: mockConfirmExit, + } + }) + + it('should appropriately return handleCleanUpAndClose', () => { + const handleExit = useWizardExitHeader(props) + expect(handleExit).toEqual(props.handleCleanUpAndClose) + }) + + it('should appropriately return confirmExit', () => { + props = { ...props, isFinalStep: false } + const handleExit = useWizardExitHeader(props) + expect(handleExit).toEqual(props.confirmExit) + }) + + it('should appropriately return handleCleanUpAndClose with homeOnError = false', () => { + const errorDetails = { message: 'Some error occurred' } + const modifiedProps = { ...props, errorDetails } + const handleExit = useWizardExitHeader(modifiedProps) + expect(mockHandleCleanUpAndClose.mock.calls.length).toBe(0) + handleExit() + expect(mockHandleCleanUpAndClose).toHaveBeenCalledWith(false) + }) + + it('should appropriately return a function that does nothing ', () => { + const modifiedProps = { ...props, hasInitiatedExit: true } + const handleExit = useWizardExitHeader(modifiedProps) + handleExit() + expect(mockHandleCleanUpAndClose.mock.calls.length).toBe(0) + expect(mockConfirmExit.mock.calls.length).toBe(0) + }) +}) diff --git a/app/src/organisms/DropTipWizard/constants.ts b/app/src/organisms/DropTipWizard/constants.ts index 0390fd1870f..6d322f779ec 100644 --- a/app/src/organisms/DropTipWizard/constants.ts +++ b/app/src/organisms/DropTipWizard/constants.ts @@ -16,3 +16,7 @@ export const DROP_TIP_STEPS = [ POSITION_AND_DROP_TIP, DROP_TIP_SUCCESS, ] + +export const DROP_TIP_SPECIAL_ERROR_TYPES = { + MUST_HOME_ERROR: 'MustHomeError', +} as const diff --git a/app/src/organisms/DropTipWizard/index.tsx b/app/src/organisms/DropTipWizard/index.tsx index 3d7896663d4..ca668cd7013 100644 --- a/app/src/organisms/DropTipWizard/index.tsx +++ b/app/src/organisms/DropTipWizard/index.tsx @@ -11,6 +11,7 @@ import { COLORS, BORDERS, StyledText, + JUSTIFY_FLEX_END, } from '@opentrons/components' import { useCreateMaintenanceCommandMutation, @@ -43,6 +44,12 @@ import { BeforeBeginning } from './BeforeBeginning' import { ChooseLocation } from './ChooseLocation' import { JogToPosition } from './JogToPosition' import { Success } from './Success' +import { InProgressModal } from '../../molecules/InProgressModal/InProgressModal' +import { + useHandleDropTipCommandErrors, + useDropTipErrorComponents, + useWizardExitHeader, +} from './utils' import type { PipetteData } from '@opentrons/api-client' import type { CreateMaintenanceRunType } from '@opentrons/react-api-client' @@ -54,6 +61,8 @@ import type { } from '@opentrons/shared-data' import type { Axis, Sign, StepSize } from '../../molecules/JogControls/types' import type { Jog } from '../../molecules/JogControls' +import type { ErrorDetails } from './utils' +import type { DropTipWizardStep } from './types' const RUN_REFETCH_INTERVAL_MS = 5000 const JOG_COMMAND_TIMEOUT_MS = 10000 @@ -110,7 +119,7 @@ export function DropTipWizard(props: MaintenanceRunManagerProps): JSX.Element { }) .catch(e => e) }, - onError: error => setErrorMessage(error.message), + onError: error => setErrorDetails({ message: error.message }), }) const { data: maintenanceRunData } = useNotifyCurrentMaintenanceRun({ @@ -141,14 +150,16 @@ export function DropTipWizard(props: MaintenanceRunManagerProps): JSX.Element { ]) const [isExiting, setIsExiting] = React.useState(false) - const [errorMessage, setErrorMessage] = React.useState(null) + const [errorDetails, setErrorDetails] = React.useState( + null + ) const { deleteMaintenanceRun } = useDeleteMaintenanceRunMutation({ onSuccess: () => closeFlow(), onError: () => closeFlow(), }) - const handleCleanUpAndClose = (): void => { + const handleCleanUpAndClose = (homeOnExit: boolean = true): void => { if (hasCleanedUpAndClosed.current) return hasCleanedUpAndClosed.current = true @@ -156,23 +167,23 @@ export function DropTipWizard(props: MaintenanceRunManagerProps): JSX.Element { if (maintenanceRunData?.data.id == null) { closeFlow() } else { - chainRunCommands( - maintenanceRunData?.data.id, - [ - { - commandType: 'home' as const, - params: { axes: ['leftZ', 'rightZ', 'x', 'y'] }, - }, - ], - true + ;(homeOnExit + ? chainRunCommands( + maintenanceRunData?.data.id, + [ + { + commandType: 'home' as const, + params: { axes: ['leftZ', 'rightZ', 'x', 'y'] }, + }, + ], + true + ) + : new Promise((resolve, reject) => resolve()) ) - .then(() => { - deleteMaintenanceRun(maintenanceRunData?.data.id) - }) .catch(error => { console.error(error.message) - deleteMaintenanceRun(maintenanceRunData?.data.id) }) + .finally(() => deleteMaintenanceRun(maintenanceRunData?.data.id)) } } @@ -188,8 +199,8 @@ export function DropTipWizard(props: MaintenanceRunManagerProps): JSX.Element { handleCleanUpAndClose={handleCleanUpAndClose} chainRunCommands={chainRunCommands} createRunCommand={createMaintenanceCommand} - errorMessage={errorMessage} - setErrorMessage={setErrorMessage} + errorDetails={errorDetails} + setErrorDetails={setErrorDetails} isExiting={isExiting} deckConfig={deckConfig} /> @@ -203,9 +214,9 @@ interface DropTipWizardProps { createMaintenanceRun: CreateMaintenanceRunType isRobotMoving: boolean isExiting: boolean - setErrorMessage: (message: string | null) => void - errorMessage: string | null - handleCleanUpAndClose: () => void + setErrorDetails: (errorDetails: ErrorDetails) => void + errorDetails: ErrorDetails | null + handleCleanUpAndClose: (homeOnError?: boolean) => void chainRunCommands: ReturnType< typeof useChainMaintenanceCommands >['chainRunCommands'] @@ -227,20 +238,23 @@ export const DropTipWizardComponent = ( chainRunCommands, isRobotMoving, createRunCommand, - setErrorMessage, - errorMessage, + setErrorDetails, + errorDetails, isExiting, createdMaintenanceRunId, instrumentModelSpecs, deckConfig, } = props - const isOnDevice = useSelector(getIsOnDevice) const { t, i18n } = useTranslation('drop_tip_wizard') - const [currentStepIndex, setCurrentStepIndex] = React.useState(0) const [shouldDispenseLiquid, setShouldDispenseLiquid] = React.useState< boolean | null >(null) + const hasInitiatedExit = React.useRef(false) + + const isOnDevice = useSelector(getIsOnDevice) + const setSpecificErrorDetails = useHandleDropTipCommandErrors(setErrorDetails) + const DropTipWizardSteps = getDropTipWizardSteps(shouldDispenseLiquid) const currentStep = shouldDispenseLiquid != null @@ -248,11 +262,31 @@ export const DropTipWizardComponent = ( : null const isFinalStep = currentStepIndex === DropTipWizardSteps.length - 1 + const { + confirm: confirmExit, + showConfirmation: showConfirmExit, + cancel: cancelExit, + } = useConditionalConfirm(handleCleanUpAndClose, true) + + const { + button: errorExitBtn, + subHeader: errorSubHeader, + } = useDropTipErrorComponents({ + t, + errorDetails, + isOnDevice, + chainRunCommands, + maintenanceRunId: createdMaintenanceRunId, + onClose: handleCleanUpAndClose, + }) + React.useEffect(() => { if (createdMaintenanceRunId == null) { - createMaintenanceRun({}).catch((e: Error) => - setErrorMessage(`Error creating maintenance run: ${e.message}`) - ) + createMaintenanceRun({}).catch((e: Error) => { + setSpecificErrorDetails({ + message: `Error creating maintenance run: ${e.message}`, + }) + }) } }, []) @@ -280,18 +314,14 @@ export const DropTipWizardComponent = ( }, waitUntilComplete: true, timeout: JOG_COMMAND_TIMEOUT_MS, - }).catch((e: Error) => - setErrorMessage(`Error issuing jog command: ${e.message}`) - ) + }).catch((e: Error) => { + setSpecificErrorDetails({ + message: `Error issuing jog command: ${e.message}`, + }) + }) } } - const { - confirm: confirmExit, - showConfirmation: showConfirmExit, - cancel: cancelExit, - } = useConditionalConfirm(handleCleanUpAndClose, true) - const moveToAddressableArea = ( addressableArea: AddressableAreaName ): Promise => { @@ -326,189 +356,228 @@ export const DropTipWizardComponent = ( ).then(commandData => { const error = commandData[0].data.error if (error != null) { - setErrorMessage(`error moving to position: ${error.detail}`) + setSpecificErrorDetails({ + runCommandError: error, + message: `Error moving to position: ${error.detail}`, + }) } return null }) } else { - setErrorMessage(`error moving to position: invalid addressable area.`) + setSpecificErrorDetails({ + message: `Error moving to position: invalid addressable area.`, + }) return Promise.resolve(null) } } - let modalContent: JSX.Element =
    UNASSIGNED STEP
    - if (showConfirmExit) { - modalContent = ( - { - hasInitiatedExit.current = true - confirmExit() - }} - isRobotMoving={isRobotMoving} - /> - ) - } else if (errorMessage != null) { - modalContent = ( - - {t('drop_tip_failed')} - {errorMessage} - - } - /> - ) - } else if (shouldDispenseLiquid == null) { - modalContent = ( - - ) - } else if ( - currentStep === CHOOSE_BLOWOUT_LOCATION || - currentStep === CHOOSE_DROP_TIP_LOCATION - ) { - let bodyTextKey - if (currentStep === CHOOSE_BLOWOUT_LOCATION) { - bodyTextKey = isOnDevice - ? 'select_blowout_slot_odd' - : 'select_blowout_slot' + const modalContent = buildModalContent() + + function buildModalContent(): JSX.Element { + if (isRobotMoving) { + return buildRobotInMotion() + } else if (showConfirmExit) { + return buildShowExitConfirmation() + } else if (errorDetails != null) { + return buildErrorScreen() + } else if (shouldDispenseLiquid == null) { + return buildBeforeBeginning() + } else if ( + currentStep === CHOOSE_BLOWOUT_LOCATION || + currentStep === CHOOSE_DROP_TIP_LOCATION + ) { + return buildChooseLocation() + } else if ( + currentStep === POSITION_AND_BLOWOUT || + currentStep === POSITION_AND_DROP_TIP + ) { + return buildJogToPosition() + } else if ( + currentStep === BLOWOUT_SUCCESS || + currentStep === DROP_TIP_SUCCESS + ) { + return buildSuccess() } else { - bodyTextKey = isOnDevice - ? 'select_drop_tip_slot_odd' - : 'select_drop_tip_slot' + return
    UNASSIGNED STEP
    } - modalContent = ( - { - setCurrentStepIndex(0) - setShouldDispenseLiquid(null) - }} - title={ - currentStep === CHOOSE_BLOWOUT_LOCATION - ? i18n.format(t('choose_blowout_location'), 'capitalize') - : i18n.format(t('choose_drop_tip_location'), 'capitalize') - } - body={ - }} - /> - } - moveToAddressableArea={moveToAddressableArea} - isRobotMoving={isRobotMoving} - isOnDevice={isOnDevice} - setErrorMessage={setErrorMessage} - /> - ) - } else if ( - currentStep === POSITION_AND_BLOWOUT || - currentStep === POSITION_AND_DROP_TIP - ) { - modalContent = ( - { - if (createdMaintenanceRunId != null) { - chainRunCommands( - createdMaintenanceRunId, - [ - currentStep === POSITION_AND_BLOWOUT - ? { - commandType: 'blowOutInPlace', - params: { - pipetteId: MANAGED_PIPETTE_ID, - flowRate: - instrumentModelSpecs.defaultBlowOutFlowRate.value, + + function buildRobotInMotion(): JSX.Element { + return + } + + function buildShowExitConfirmation(): JSX.Element { + return ( + { + hasInitiatedExit.current = true + confirmExit() + }} + /> + ) + } + + function buildErrorScreen(): JSX.Element { + return ( + + {errorExitBtn} + + ) + } + + function buildBeforeBeginning(): JSX.Element { + return ( + + ) + } + + function buildChooseLocation(): JSX.Element { + let bodyTextKey: string + if (currentStep === CHOOSE_BLOWOUT_LOCATION) { + bodyTextKey = isOnDevice + ? 'select_blowout_slot_odd' + : 'select_blowout_slot' + } else { + bodyTextKey = isOnDevice + ? 'select_drop_tip_slot_odd' + : 'select_drop_tip_slot' + } + return ( + { + setCurrentStepIndex(0) + setShouldDispenseLiquid(null) + }} + title={ + currentStep === CHOOSE_BLOWOUT_LOCATION + ? i18n.format(t('choose_blowout_location'), 'capitalize') + : i18n.format(t('choose_drop_tip_location'), 'capitalize') + } + body={ + }} + /> + } + moveToAddressableArea={moveToAddressableArea} + isOnDevice={isOnDevice} + setErrorDetails={setSpecificErrorDetails} + /> + ) + } + + function buildJogToPosition(): JSX.Element { + return ( + { + if (createdMaintenanceRunId != null) { + chainRunCommands( + createdMaintenanceRunId, + [ + currentStep === POSITION_AND_BLOWOUT + ? { + commandType: 'blowOutInPlace', + params: { + pipetteId: MANAGED_PIPETTE_ID, + flowRate: + instrumentModelSpecs.defaultBlowOutFlowRate.value, + }, + } + : { + commandType: 'dropTipInPlace', + params: { pipetteId: MANAGED_PIPETTE_ID }, }, - } - : { - commandType: 'dropTipInPlace', - params: { pipetteId: MANAGED_PIPETTE_ID }, - }, - ], - true - ) - .then(commandData => { - const error = commandData[0].data.error - if (error != null) { - setErrorMessage(`error moving to position: ${error.detail}`) - } else proceed() - }) - .catch(e => - setErrorMessage( - `Error issuing ${ - currentStep === POSITION_AND_BLOWOUT - ? 'blowout' - : 'drop tip' - } command: ${e.message}` - ) + ], + true ) + .then(commandData => { + const error = commandData[0].data.error + if (error != null) { + setSpecificErrorDetails({ + runCommandError: error, + message: `Error moving to position: ${error.detail}`, + }) + } else { + proceed() + } + }) + .catch(e => + setSpecificErrorDetails({ + message: `Error issuing ${ + currentStep === POSITION_AND_BLOWOUT + ? 'blowout' + : 'drop tip' + } command: ${e.message}`, + }) + ) + } + }} + handleGoBack={goBack} + body={ + currentStep === POSITION_AND_BLOWOUT + ? t('position_and_blowout') + : t('position_and_drop_tip') } - }} - isRobotMoving={isRobotMoving} - handleGoBack={goBack} - body={ - currentStep === POSITION_AND_BLOWOUT - ? t('position_and_blowout') - : t('position_and_drop_tip') - } - currentStep={currentStep} - isOnDevice={isOnDevice} - /> - ) - } else if ( - currentStep === BLOWOUT_SUCCESS || - currentStep === DROP_TIP_SUCCESS - ) { - modalContent = ( - - ) + currentStep={currentStep as DropTipWizardStep} + isOnDevice={isOnDevice} + /> + ) + } + + function buildSuccess(): JSX.Element { + return ( + + ) + } } - const hasInitiatedExit = React.useRef(false) - let handleExit: () => void = () => null - if (!hasInitiatedExit.current) handleExit = confirmExit - else if (errorMessage != null) handleExit = handleCleanUpAndClose + const wizardHeaderOnExit = useWizardExitHeader({ + isFinalStep, + hasInitiatedExit: hasInitiatedExit.current, + errorDetails, + confirmExit, + handleCleanUpAndClose, + }) const wizardHeader = ( ) diff --git a/app/src/organisms/DropTipWizard/utils.tsx b/app/src/organisms/DropTipWizard/utils.tsx new file mode 100644 index 00000000000..d0a38fc768b --- /dev/null +++ b/app/src/organisms/DropTipWizard/utils.tsx @@ -0,0 +1,185 @@ +import * as React from 'react' +import { useTranslation } from 'react-i18next' + +import { AlertPrimaryButton, SPACING } from '@opentrons/components' + +import { DROP_TIP_SPECIAL_ERROR_TYPES } from './constants' +import { SmallButton } from '../../atoms/buttons' + +import type { RunCommandError } from '@opentrons/api-client' +import type { useChainMaintenanceCommands } from '../../resources/runs' + +export interface ErrorDetails { + message: string + header?: string + type?: string +} + +interface HandleDropTipCommandErrorsCbProps { + runCommandError?: RunCommandError + message?: string + header?: string + type?: RunCommandError['errorType'] +} + +/** + * @description Wraps the error state setter, updating the setter if the error should be special-cased. + */ +export function useHandleDropTipCommandErrors( + setErrorDetails: (errorDetails: ErrorDetails) => void +): (cbProps: HandleDropTipCommandErrorsCbProps) => void { + const { t } = useTranslation('drop_tip_wizard') + + return ({ + runCommandError, + message, + header, + type, + }: HandleDropTipCommandErrorsCbProps) => { + if ( + runCommandError?.errorType === + DROP_TIP_SPECIAL_ERROR_TYPES.MUST_HOME_ERROR + ) { + const headerText = t('cant_safely_drop_tips') + const messageText = t('remove_the_tips_manually') + + setErrorDetails({ + header: headerText, + message: messageText, + type: DROP_TIP_SPECIAL_ERROR_TYPES.MUST_HOME_ERROR, + }) + } else { + const messageText = message ?? '' + setErrorDetails({ header, message: messageText, type }) + } + } +} + +interface DropTipErrorComponents { + button: JSX.Element | null + subHeader: JSX.Element +} + +export interface UseDropTipErrorComponentsProps { + isOnDevice: boolean + t: (translationString: string) => string + maintenanceRunId: string | null + onClose: () => void + errorDetails: ErrorDetails | null + chainRunCommands: ReturnType< + typeof useChainMaintenanceCommands + >['chainRunCommands'] +} + +/** + * @description Returns special-cased components given error details. + */ +export function useDropTipErrorComponents({ + t, + maintenanceRunId, + onClose, + errorDetails, + isOnDevice, + chainRunCommands, +}: UseDropTipErrorComponentsProps): DropTipErrorComponents { + return errorDetails?.type === DROP_TIP_SPECIAL_ERROR_TYPES.MUST_HOME_ERROR + ? buildHandleMustHome() + : buildGenericError() + + function buildGenericError(): DropTipErrorComponents { + return { + button: null, + subHeader: ( + <> + {t('drop_tip_failed')} +
    + {errorDetails?.message} + + ), + } + } + + function buildHandleMustHome(): DropTipErrorComponents { + const handleOnClick = (): void => { + if (maintenanceRunId !== null) { + void chainRunCommands( + maintenanceRunId, + [ + { + commandType: 'home' as const, + params: {}, + }, + ], + true + ) + onClose() + } + } + + return { + button: isOnDevice ? ( + + ) : ( + + {t('confirm_removal_and_home')} + + ), + subHeader: <>{errorDetails?.message}, + } + } +} + +export interface UseWizardExitHeaderProps { + isFinalStep: boolean + hasInitiatedExit: boolean + errorDetails: ErrorDetails | null + handleCleanUpAndClose: (homeOnError?: boolean) => void + confirmExit: (homeOnError?: boolean) => void +} + +/** + * @description Determines the appropriate onClick for the wizard exit button, ensuring the exit logic can occur at + * most one time. + */ +export function useWizardExitHeader({ + isFinalStep, + hasInitiatedExit, + errorDetails, + handleCleanUpAndClose, + confirmExit, +}: UseWizardExitHeaderProps): () => void { + return buildHandleExit() + + function buildHandleExit(): () => void { + if (!hasInitiatedExit) { + if (errorDetails != null) { + // When an error occurs, do not home when exiting the flow via the wizard header. + return buildNoHomeCleanUpAndClose() + } else if (isFinalStep) { + return buildHandleCleanUpAndClose() + } else { + return buildConfirmExit() + } + } else { + return buildGenericCase() + } + } + + function buildGenericCase(): () => void { + return () => null + } + function buildNoHomeCleanUpAndClose(): () => void { + return () => handleCleanUpAndClose(false) + } + function buildHandleCleanUpAndClose(): () => void { + return handleCleanUpAndClose + } + function buildConfirmExit(): () => void { + return confirmExit + } +} From 1fb2e85834bda9a0b57d5f5525f72fb46cc48130 Mon Sep 17 00:00:00 2001 From: Nick Diehl <47604184+ncdiehl11@users.noreply.github.com> Date: Tue, 16 Apr 2024 14:11:08 -0400 Subject: [PATCH 139/194] fix(app): set max height for desktop modal shell (#14915) closes RQA-2279 --- app/src/assets/localization/en/pipette_wizard_flows.json | 2 +- app/src/organisms/PipetteWizardFlows/ChoosePipette.tsx | 8 ++++++-- .../PipetteWizardFlows/__tests__/ChoosePipette.test.tsx | 4 ++-- app/src/organisms/PipetteWizardFlows/index.tsx | 2 +- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/app/src/assets/localization/en/pipette_wizard_flows.json b/app/src/assets/localization/en/pipette_wizard_flows.json index c1694816f00..53ae23d07e2 100644 --- a/app/src/assets/localization/en/pipette_wizard_flows.json +++ b/app/src/assets/localization/en/pipette_wizard_flows.json @@ -32,7 +32,7 @@ "detach_mount_attach_96": "Detach {{mount}} Pipette and Attach 96-Channel Pipette", "detach_mounting_plate_instructions": "Hold onto the plate so it does not fall. Then remove the pins on the plate from the slots on the gantry carriage.", "detach_next_pipette": "Detach next pipette", - "detach_pipette_to_attach_96": "Detach {{pipetteName}} and attach 96-Channel pipette", + "detach_pipette_to_attach_96": "Detach {{pipetteName}} and Attach 96-Channel pipette", "detach_pipette": "detach {{mount}} pipette", "detach_pipettes_attach_96": "Detach Pipettes and Attach 96-Channel Pipette", "detach_z_axis_screw_again": "detach the z-axis screw before attaching the 96-Channel Pipette.", diff --git a/app/src/organisms/PipetteWizardFlows/ChoosePipette.tsx b/app/src/organisms/PipetteWizardFlows/ChoosePipette.tsx index 8d9330315c7..f3926a711a3 100644 --- a/app/src/organisms/PipetteWizardFlows/ChoosePipette.tsx +++ b/app/src/organisms/PipetteWizardFlows/ChoosePipette.tsx @@ -207,7 +207,11 @@ export const ChoosePipette = (props: ChoosePipetteProps): JSX.Element => {
    ) : ( - + {showExitConfirmation ? ( setShowExitConfirmation(false)} @@ -218,7 +222,7 @@ export const ChoosePipette = (props: ChoosePipetteProps): JSX.Element => { ) : ( diff --git a/app/src/organisms/PipetteWizardFlows/__tests__/ChoosePipette.test.tsx b/app/src/organisms/PipetteWizardFlows/__tests__/ChoosePipette.test.tsx index 37570d8c5ff..bca0e623619 100644 --- a/app/src/organisms/PipetteWizardFlows/__tests__/ChoosePipette.test.tsx +++ b/app/src/organisms/PipetteWizardFlows/__tests__/ChoosePipette.test.tsx @@ -150,7 +150,7 @@ describe('ChoosePipette', () => { props = { ...props, selectedPipette: NINETY_SIX_CHANNEL } render(props) screen.getByText( - 'Detach Flex 1-Channel 1000 μL and attach 96-Channel pipette' + 'Detach Flex 1-Channel 1000 μL and Attach 96-Channel pipette' ) }) @@ -163,7 +163,7 @@ describe('ChoosePipette', () => { props = { ...props, selectedPipette: NINETY_SIX_CHANNEL } render(props) screen.getByText( - 'Detach Flex 1-Channel 1000 μL and attach 96-Channel pipette' + 'Detach Flex 1-Channel 1000 μL and Attach 96-Channel pipette' ) }) }) diff --git a/app/src/organisms/PipetteWizardFlows/index.tsx b/app/src/organisms/PipetteWizardFlows/index.tsx index 1a671fb31fb..337a51028ed 100644 --- a/app/src/organisms/PipetteWizardFlows/index.tsx +++ b/app/src/organisms/PipetteWizardFlows/index.tsx @@ -421,7 +421,7 @@ export const PipetteWizardFlows = ( currentStep.section === SECTIONS.BEFORE_BEGINNING && selectedPipette === NINETY_SIX_CHANNEL && flowType === FLOWS.ATTACH - ? '70%' + ? '30rem' : 'auto' } header={wizardHeader} From 55d25bb2f938006aa246a32080d37460fa3c3e55 Mon Sep 17 00:00:00 2001 From: Jethary Rader <66035149+jerader@users.noreply.github.com> Date: Tue, 16 Apr 2024 14:13:53 -0400 Subject: [PATCH 140/194] =?UTF-8?q?fix(protocol-designer):=20filter=20out?= =?UTF-8?q?=20module=20addressable=20areas=20from=20newL=E2=80=A6=20(#1491?= =?UTF-8?q?8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …ocation dropdown closes AUTH-348 --- .../src/top-selectors/labware-locations/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/protocol-designer/src/top-selectors/labware-locations/index.ts b/protocol-designer/src/top-selectors/labware-locations/index.ts index 9396bd121b8..6c66367fb4f 100644 --- a/protocol-designer/src/top-selectors/labware-locations/index.ts +++ b/protocol-designer/src/top-selectors/labware-locations/index.ts @@ -11,6 +11,7 @@ import { STAGING_AREA_RIGHT_SLOT_FIXTURE, isAddressableAreaStandardSlot, MOVABLE_TRASH_ADDRESSABLE_AREAS, + FLEX_MODULE_ADDRESSABLE_AREAS, } from '@opentrons/shared-data' import { COLUMN_4_SLOTS } from '@opentrons/step-generation' import { @@ -232,7 +233,8 @@ export const getUnoccupiedLabwareLocationOptions: Selector< .includes(slotId) && !isTrashSlot && !WASTE_CHUTE_ADDRESSABLE_AREAS.includes(slotId) && - !notSelectedStagingAreaAddressableAreas.includes(slotId) + !notSelectedStagingAreaAddressableAreas.includes(slotId) && + !FLEX_MODULE_ADDRESSABLE_AREAS.includes(slotId) ) }) .map(slotId => ({ name: slotId, value: slotId })) From 071cc97a51b70996cbf41365fe923c73b31092c2 Mon Sep 17 00:00:00 2001 From: Brent Hagen Date: Tue, 16 Apr 2024 14:21:39 -0400 Subject: [PATCH 141/194] refactor(api,app): remove internal_only flag from enableOEMMode setting (#14920) enableOEMMode isn't really an internal_only setting, and we need it included in the robot settings api response. change needed for the ODD text to anonymize and the factory mode toggle to work. originally part of oem-mode-integration branch. --- api/src/opentrons/config/advanced_settings.py | 5 ----- .../Devices/RobotSettings/RobotSettingsFeatureFlags.tsx | 1 + 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/api/src/opentrons/config/advanced_settings.py b/api/src/opentrons/config/advanced_settings.py index 4d83d8ed1af..6a6076a8432 100644 --- a/api/src/opentrons/config/advanced_settings.py +++ b/api/src/opentrons/config/advanced_settings.py @@ -238,11 +238,6 @@ class Setting(NamedTuple): title="Enable OEM Mode", description="This setting anonymizes Opentrons branding in the ODD app.", robot_type=[RobotTypeEnum.FLEX], - show_if=( - "enableOEMMode", - True, - ), - internal_only=True, ), SettingDefinition( _id="enablePerformanceMetrics", diff --git a/app/src/organisms/Devices/RobotSettings/RobotSettingsFeatureFlags.tsx b/app/src/organisms/Devices/RobotSettings/RobotSettingsFeatureFlags.tsx index 80ed8a04f5b..9837c314ac3 100644 --- a/app/src/organisms/Devices/RobotSettings/RobotSettingsFeatureFlags.tsx +++ b/app/src/organisms/Devices/RobotSettings/RobotSettingsFeatureFlags.tsx @@ -29,6 +29,7 @@ interface RobotSettingsFeatureFlagsProps { const NON_FEATURE_FLAG_SETTINGS = [ 'enableDoorSafetySwitch', + 'enableOEMMode', 'disableHomeOnBoot', 'deckCalibrationDots', 'shortFixedTrash', From d77bbb72bf8bc25e029cb2a7d3f32c62ae8e5b52 Mon Sep 17 00:00:00 2001 From: Derek Maggio Date: Tue, 16 Apr 2024 11:26:44 -0700 Subject: [PATCH 142/194] fix: fix timing function cross-platform bug (#14919) # Overview Closes https://opentrons.atlassian.net/browse/EXEC-406 Use a timing function universal to Windows, Mac, and Linux when running on Windows and Mac This will allow performance metrics tests to run locally for devs The production implementation will always use Linux # Test Plan - Create test that patches _get_timing_function to return universal timing function and make sure that works - Realized I missed timing synchronous functions so I added that too # Changelog - In robot_context_tracker.py create a _get_timing_function which imports the correct package based on os # Review requests None # Risk assessment low --- .../robot_context_tracker.py | 36 +++++++++--- .../test_robot_context_tracker.py | 57 ++++++++++++++++++- 2 files changed, 84 insertions(+), 9 deletions(-) diff --git a/performance-metrics/src/performance_metrics/robot_context_tracker.py b/performance-metrics/src/performance_metrics/robot_context_tracker.py index 188129046ff..606be71e649 100644 --- a/performance-metrics/src/performance_metrics/robot_context_tracker.py +++ b/performance-metrics/src/performance_metrics/robot_context_tracker.py @@ -2,22 +2,42 @@ import csv from pathlib import Path +import platform + +from functools import wraps, partial +from time import perf_counter_ns import os +from typing import Callable, TypeVar, cast + -from functools import wraps -from time import perf_counter_ns, clock_gettime_ns, CLOCK_REALTIME -from typing import Callable, TypeVar from typing_extensions import ParamSpec from collections import deque -from performance_metrics.datashapes import ( - RawContextData, - RobotContextState, -) +from performance_metrics.datashapes import RawContextData, RobotContextState P = ParamSpec("P") R = TypeVar("R") +def _get_timing_function() -> Callable[[], int]: + """Returns a timing function for the current platform.""" + time_function: Callable[[], int] + if platform.system() == "Linux": + from time import clock_gettime_ns, CLOCK_REALTIME + + time_function = cast( + Callable[[], int], partial(clock_gettime_ns, CLOCK_REALTIME) + ) + else: + from time import time_ns + + time_function = time_ns + + return time_function + + +timing_function = _get_timing_function() + + class RobotContextTracker: """Tracks and stores robot context and execution duration for different operations.""" @@ -43,7 +63,7 @@ def inner_decorator(func: Callable[P, R]) -> Callable[P, R]: @wraps(func) def wrapper(*args: P.args, **kwargs: P.kwargs) -> R: - function_start_time = clock_gettime_ns(CLOCK_REALTIME) + function_start_time = timing_function() duration_start_time = perf_counter_ns() try: result = func(*args, **kwargs) diff --git a/performance-metrics/tests/performance_metrics/test_robot_context_tracker.py b/performance-metrics/tests/performance_metrics/test_robot_context_tracker.py index d78d5054fe6..5345004eb44 100644 --- a/performance-metrics/tests/performance_metrics/test_robot_context_tracker.py +++ b/performance-metrics/tests/performance_metrics/test_robot_context_tracker.py @@ -5,7 +5,8 @@ import pytest from performance_metrics.robot_context_tracker import RobotContextTracker from performance_metrics.datashapes import RobotContextState -from time import sleep +from time import sleep, time_ns +from unittest.mock import patch # Corrected times in seconds STARTING_TIME = 0.001 @@ -140,6 +141,24 @@ async def async_analyzing_operation() -> None: ), "State should be ANALYZING_PROTOCOL." +def test_sync_operation_timing_accuracy( + robot_context_tracker: RobotContextTracker, +) -> None: + """Tests the timing accuracy of a synchronous operation tracking.""" + + @robot_context_tracker.track(state=RobotContextState.RUNNING_PROTOCOL) + def running_operation() -> None: + sleep(RUNNING_TIME) + + running_operation() + + duration_data = robot_context_tracker._storage[0] + measured_duration = duration_data.duration_end - duration_data.duration_start + assert ( + abs(measured_duration - RUNNING_TIME * 1e9) < 1e7 + ), "Measured duration for sync operation should closely match the expected duration." + + @pytest.mark.asyncio async def test_async_operation_timing_accuracy( robot_context_tracker: RobotContextTracker, @@ -249,3 +268,39 @@ def analyzing_protocol() -> None: assert ( len(lines) == 4 ), "All stored data + header should be written to the file." + + +@patch( + "performance_metrics.robot_context_tracker._get_timing_function", + return_value=time_ns, +) +def test_using_non_linux_time_functions(tmp_path: Path) -> None: + """Tests tracking operations using non-Linux time functions.""" + file_path = tmp_path / "test_file.csv" + robot_context_tracker = RobotContextTracker(file_path, should_track=True) + + @robot_context_tracker.track(state=RobotContextState.STARTING_UP) + def starting_robot() -> None: + sleep(STARTING_TIME) + + @robot_context_tracker.track(state=RobotContextState.CALIBRATING) + def calibrating_robot() -> None: + sleep(CALIBRATING_TIME) + + starting_robot() + calibrating_robot() + + storage = robot_context_tracker._storage + assert all( + data.func_start > 0 for data in storage + ), "All function start times should be greater than 0." + assert all( + data.duration_start > 0 for data in storage + ), "All duration start times should be greater than 0." + assert all( + data.duration_end > 0 for data in storage + ), "All duration end times should be greater than 0." + assert all( + data.duration_end > data.duration_start for data in storage + ), "Duration end times should be greater than duration start times." + assert len(storage) == 2, "Both operations should be tracked." From 385d123ac06063a7861185227b8eb433755ce97c Mon Sep 17 00:00:00 2001 From: Sarah Breen Date: Tue, 16 Apr 2024 15:10:08 -0400 Subject: [PATCH 143/194] feat(app): add pipette selection screen to quick transfer flow (#14912) fix PLAT-174 --- .../localization/en/quick_transfer.json | 3 + app/src/atoms/buttons/LargeButton.stories.tsx | 13 ++ app/src/atoms/buttons/LargeButton.tsx | 44 +++--- .../QuickTransferFlow/SelectPipette.tsx | 114 ++++++++++++++++ .../__tests__/SelectPipette.test.tsx | 126 ++++++++++++++++++ app/src/organisms/QuickTransferFlow/index.tsx | 91 +++---------- app/src/organisms/QuickTransferFlow/types.ts | 10 +- app/src/organisms/QuickTransferFlow/utils.ts | 75 +++++++++++ robot-server/simulators/test-flex.json | 8 +- 9 files changed, 385 insertions(+), 99 deletions(-) create mode 100644 app/src/organisms/QuickTransferFlow/SelectPipette.tsx create mode 100644 app/src/organisms/QuickTransferFlow/__tests__/SelectPipette.test.tsx create mode 100644 app/src/organisms/QuickTransferFlow/utils.ts diff --git a/app/src/assets/localization/en/quick_transfer.json b/app/src/assets/localization/en/quick_transfer.json index 45732a28114..b0e9e294dc4 100644 --- a/app/src/assets/localization/en/quick_transfer.json +++ b/app/src/assets/localization/en/quick_transfer.json @@ -1,5 +1,8 @@ { "create_new_transfer": "Create new quick transfer", + "left_mount": "Left Mount", + "both_mounts": "Left + Right Mount", + "right_mount": "Right Mount", "select_attached_pipette": "Select attached pipette", "select_dest_labware": "Select destination labware", "select_dest_wells": "Select destination wells", diff --git a/app/src/atoms/buttons/LargeButton.stories.tsx b/app/src/atoms/buttons/LargeButton.stories.tsx index fa3a5e9d2fb..d60e89d81f3 100644 --- a/app/src/atoms/buttons/LargeButton.stories.tsx +++ b/app/src/atoms/buttons/LargeButton.stories.tsx @@ -45,3 +45,16 @@ export const Alert: Story = { iconName: 'reset', }, } +export const PrimaryNoIcon: Story = { + args: { + buttonText: 'Button text', + disabled: false, + }, +} +export const PrimaryWithSubtext: Story = { + args: { + buttonText: 'Button text', + disabled: false, + subtext: 'Button subtext', + }, +} diff --git a/app/src/atoms/buttons/LargeButton.tsx b/app/src/atoms/buttons/LargeButton.tsx index 6bfcf857d84..c5e45d3b731 100644 --- a/app/src/atoms/buttons/LargeButton.tsx +++ b/app/src/atoms/buttons/LargeButton.tsx @@ -7,6 +7,7 @@ import { DIRECTION_COLUMN, DISPLAY_FLEX, Icon, + Flex, JUSTIFY_SPACE_BETWEEN, SPACING, StyledText, @@ -20,7 +21,8 @@ interface LargeButtonProps extends StyleProps { onClick: () => void buttonType?: LargeButtonTypes buttonText: React.ReactNode - iconName: IconName + iconName?: IconName + subtext?: string disabled?: boolean } @@ -29,6 +31,7 @@ export function LargeButton(props: LargeButtonProps): JSX.Element { buttonType = 'primary', buttonText, iconName, + subtext, disabled = false, ...buttonProps } = props @@ -110,23 +113,28 @@ export function LargeButton(props: LargeButtonProps): JSX.Element { disabled={disabled} {...buttonProps} > - - {buttonText} - - + + + {buttonText} + + {subtext ? ( + + {subtext} + + ) : null} + + {iconName ? ( + + ) : null} ) } diff --git a/app/src/organisms/QuickTransferFlow/SelectPipette.tsx b/app/src/organisms/QuickTransferFlow/SelectPipette.tsx new file mode 100644 index 00000000000..0f92ca0d508 --- /dev/null +++ b/app/src/organisms/QuickTransferFlow/SelectPipette.tsx @@ -0,0 +1,114 @@ +import * as React from 'react' +import { useTranslation } from 'react-i18next' +import { + Flex, + SPACING, + StyledText, + TYPOGRAPHY, + DIRECTION_COLUMN, +} from '@opentrons/components' +import { useInstrumentsQuery } from '@opentrons/react-api-client' +import { getPipetteSpecsV2, RIGHT, LEFT } from '@opentrons/shared-data' +import { SmallButton, LargeButton } from '../../atoms/buttons' +import { ChildNavigation } from '../ChildNavigation' + +import type { PipetteData, Mount } from '@opentrons/api-client' +import type { + QuickTransferSetupState, + QuickTransferWizardAction, +} from './types' + +interface SelectPipetteProps { + onNext: () => void + onBack: () => void + exitButtonProps: React.ComponentProps + state: QuickTransferSetupState + dispatch: React.Dispatch +} + +export function SelectPipette(props: SelectPipetteProps): JSX.Element { + const { onNext, onBack, exitButtonProps, state, dispatch } = props + const { i18n, t } = useTranslation(['quick_transfer', 'shared']) + const { data: attachedInstruments } = useInstrumentsQuery() + + const leftPipette = attachedInstruments?.data.find( + (i): i is PipetteData => i.ok && i.mount === LEFT + ) + const leftPipetteSpecs = + leftPipette != null ? getPipetteSpecsV2(leftPipette.instrumentModel) : null + + const rightPipette = attachedInstruments?.data.find( + (i): i is PipetteData => i.ok && i.mount === RIGHT + ) + const rightPipetteSpecs = + rightPipette != null + ? getPipetteSpecsV2(rightPipette.instrumentModel) + : null + + // automatically select 96 channel if it is attached + const [selectedPipette, setSelectedPipette] = React.useState< + Mount | undefined + >(leftPipetteSpecs?.channels === 96 ? LEFT : state.mount) + + const handleClickNext = (): void => { + const selectedPipetteSpecs = + selectedPipette === LEFT ? leftPipetteSpecs : rightPipetteSpecs + + // the button will be disabled if these values are null + if (selectedPipette != null && selectedPipetteSpecs != null) { + dispatch({ + type: 'SELECT_PIPETTE', + pipette: selectedPipetteSpecs, + mount: selectedPipette, + }) + onNext() + } + } + return ( + + + + + {t('pipette_currently_attached')} + + {leftPipetteSpecs != null ? ( + { + setSelectedPipette(LEFT) + }} + buttonText={ + leftPipetteSpecs.channels === 96 + ? t('both_mounts') + : t('left_mount') + } + subtext={leftPipetteSpecs.displayName} + /> + ) : null} + {rightPipetteSpecs != null ? ( + { + setSelectedPipette(RIGHT) + }} + buttonText={t('right_mount')} + subtext={rightPipetteSpecs.displayName} + /> + ) : null} + + + ) +} diff --git a/app/src/organisms/QuickTransferFlow/__tests__/SelectPipette.test.tsx b/app/src/organisms/QuickTransferFlow/__tests__/SelectPipette.test.tsx new file mode 100644 index 00000000000..2d6faa6ffa7 --- /dev/null +++ b/app/src/organisms/QuickTransferFlow/__tests__/SelectPipette.test.tsx @@ -0,0 +1,126 @@ +import * as React from 'react' +import { fireEvent, screen } from '@testing-library/react' +import { describe, it, expect, afterEach, vi, beforeEach } from 'vitest' +import { useInstrumentsQuery } from '@opentrons/react-api-client' + +import { renderWithProviders } from '../../../__testing-utils__' +import { i18n } from '../../../i18n' +import { SelectPipette } from '../SelectPipette' + +vi.mock('@opentrons/react-api-client') +const render = (props: React.ComponentProps) => { + return renderWithProviders(, { + i18nInstance: i18n, + }) +} + +describe('SelectPipette', () => { + let props: React.ComponentProps + + beforeEach(() => { + props = { + onNext: vi.fn(), + onBack: vi.fn(), + exitButtonProps: { + buttonType: 'tertiaryLowLight', + buttonText: 'Exit', + onClick: vi.fn(), + }, + state: {}, + dispatch: vi.fn(), + } + vi.mocked(useInstrumentsQuery).mockReturnValue({ + data: { + data: [ + { + instrumentType: 'pipette', + mount: 'left', + ok: true, + firmwareVersion: 12, + instrumentName: 'p10_single', + instrumentModel: 'p1000_multi_v3.4', + data: {}, + } as any, + { + instrumentType: 'pipette', + mount: 'right', + ok: true, + firmwareVersion: 12, + instrumentName: 'p10_single', + instrumentModel: 'p1000_multi_v3.4', + data: {}, + } as any, + ], + }, + } as any) + }) + afterEach(() => { + vi.resetAllMocks() + }) + + it('renders the select pipette screen, header, and exit button', () => { + render(props) + screen.getByText('Select attached pipette') + screen.getByText( + 'Quick transfer options depend on the pipettes currently attached to your robot.' + ) + const exitBtn = screen.getByText('Exit') + fireEvent.click(exitBtn) + expect(props.exitButtonProps.onClick).toHaveBeenCalled() + }) + + it('renders continue button and it is disabled if no pipette is selected', () => { + render(props) + screen.getByText('Continue') + const continueBtn = screen.getByTestId('ChildNavigation_Primary_Button') + expect(continueBtn).toBeDisabled() + }) + + it('renders both pipette buttons if there are two attached', () => { + render(props) + screen.getByText('Left Mount') + screen.getByText('Right Mount') + }) + + it('selects pipette by default if there is one in state, button will be enabled', () => { + render({ ...props, state: { mount: 'left' } }) + const continueBtn = screen.getByTestId('ChildNavigation_Primary_Button') + expect(continueBtn).toBeEnabled() + fireEvent.click(continueBtn) + expect(props.onNext).toHaveBeenCalled() + }) + + it('enables continue button if you click a pipette', () => { + render(props) + const continueBtn = screen.getByTestId('ChildNavigation_Primary_Button') + expect(continueBtn).toBeDisabled() + const leftButton = screen.getByText('Left Mount') + fireEvent.click(leftButton) + expect(continueBtn).toBeEnabled() + fireEvent.click(continueBtn) + expect(props.dispatch).toHaveBeenCalled() + expect(props.onNext).toHaveBeenCalled() + }) + + it('renders left and right button if 96 is attached and automatically selects the pipette', () => { + vi.mocked(useInstrumentsQuery).mockReturnValue({ + data: { + data: [ + { + instrumentType: 'pipette', + mount: 'left', + ok: true, + firmwareVersion: 12, + instrumentName: 'p1000_96', + instrumentModel: 'p1000_96_v1', + data: {}, + } as any, + ], + }, + } as any) + render(props) + screen.getByText('Left + Right Mount') + const continueBtn = screen.getByTestId('ChildNavigation_Primary_Button') + expect(continueBtn).toBeEnabled() + }) +}) diff --git a/app/src/organisms/QuickTransferFlow/index.tsx b/app/src/organisms/QuickTransferFlow/index.tsx index 4031c7aa7bf..36d0175b0db 100644 --- a/app/src/organisms/QuickTransferFlow/index.tsx +++ b/app/src/organisms/QuickTransferFlow/index.tsx @@ -5,83 +5,21 @@ import { Flex, StepMeter, SPACING } from '@opentrons/components' import { SmallButton } from '../../atoms/buttons' import { ChildNavigation } from '../ChildNavigation' import { CreateNewTransfer } from './CreateNewTransfer' +import { SelectPipette } from './SelectPipette' +import { quickTransferReducer } from './utils' -import type { - QuickTransferSetupState, - QuickTransferWizardAction, -} from './types' +import type { QuickTransferSetupState } from './types' const QUICK_TRANSFER_WIZARD_STEPS = 8 - -// const initialQuickTransferState: QuickTransferSetupState = {} -export function reducer( - state: QuickTransferSetupState, - action: QuickTransferWizardAction -): QuickTransferSetupState { - switch (action.type) { - case 'SELECT_PIPETTE': { - return { - pipette: action.pipette, - } - } - case 'SELECT_TIP_RACK': { - return { - pipette: state.pipette, - tipRack: action.tipRack, - } - } - case 'SET_SOURCE_LABWARE': { - return { - pipette: state.pipette, - tipRack: state.tipRack, - source: action.labware, - } - } - case 'SET_SOURCE_WELLS': { - return { - pipette: state.pipette, - tipRack: state.tipRack, - source: state.source, - sourceWells: action.wells, - } - } - case 'SET_DEST_LABWARE': { - return { - pipette: state.pipette, - tipRack: state.tipRack, - source: state.source, - sourceWells: state.sourceWells, - destination: action.labware, - } - } - case 'SET_DEST_WELLS': { - return { - pipette: state.pipette, - tipRack: state.tipRack, - source: state.source, - sourceWells: state.sourceWells, - destination: state.destination, - destinationWells: action.wells, - } - } - case 'SET_VOLUME': { - return { - pipette: state.pipette, - tipRack: state.tipRack, - source: state.source, - sourceWells: state.sourceWells, - destination: state.destination, - destinationWells: state.destinationWells, - volume: action.volume, - } - } - } -} +const initialQuickTransferState: QuickTransferSetupState = {} export const QuickTransferFlow = (): JSX.Element => { const history = useHistory() const { i18n, t } = useTranslation(['quick_transfer', 'shared']) - // const [state, dispatch] = React.useReducer(reducer, initialQuickTransferState) + const [state, dispatch] = React.useReducer( + quickTransferReducer, + initialQuickTransferState + ) const [currentStep, setCurrentStep] = React.useState(1) const [continueIsDisabled] = React.useState(false) @@ -96,6 +34,8 @@ export const QuickTransferFlow = (): JSX.Element => { history.push('protocols') }, } + + // these will be moved to the child components once they all exist const ORDERED_STEP_HEADERS: string[] = [ t('create_new_transfer'), t('select_attached_pipette'), @@ -116,12 +56,21 @@ export const QuickTransferFlow = (): JSX.Element => { exitButtonProps={exitButtonProps} /> ) + } else if (currentStep === 2) { + modalContent = ( + setCurrentStep(prevStep => prevStep - 1)} + onNext={() => setCurrentStep(prevStep => prevStep + 1)} + exitButtonProps={exitButtonProps} + /> + ) } else { modalContent = null } // until each page is wired up, show header title with empty screen - return ( <> Date: Tue, 16 Apr 2024 16:06:15 -0400 Subject: [PATCH 144/194] fix(app): set default selected robot on slideout to first valid robot (#14925) closes RQA-2578 --- app/src/organisms/ChooseRobotSlideout/index.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/app/src/organisms/ChooseRobotSlideout/index.tsx b/app/src/organisms/ChooseRobotSlideout/index.tsx index 68b89afddad..066bd28eb61 100644 --- a/app/src/organisms/ChooseRobotSlideout/index.tsx +++ b/app/src/organisms/ChooseRobotSlideout/index.tsx @@ -193,12 +193,18 @@ export function ChooseRobotSlideout( // this useEffect sets the default selection to the first robot in the list. state is managed by the caller React.useEffect(() => { - if (selectedRobot == null && reducerAvailableRobots.length > 0) { + if ( + (selectedRobot == null || + !reducerAvailableRobots.some( + robot => robot.name === selectedRobot.name + )) && + reducerAvailableRobots.length > 0 + ) { setSelectedRobot(reducerAvailableRobots[0]) } else if (reducerAvailableRobots.length === 0) { setSelectedRobot(null) } - }, [healthyReachableRobots, selectedRobot, setSelectedRobot]) + }, [reducerAvailableRobots, selectedRobot, setSelectedRobot]) const unavailableCount = unhealthyReachableRobots.length + unreachableRobots.length From 35e9fe729799077796254ce10664ba044773e2a3 Mon Sep 17 00:00:00 2001 From: Caila Marashaj <98041399+caila-marashaj@users.noreply.github.com> Date: Tue, 16 Apr 2024 18:06:15 -0400 Subject: [PATCH 145/194] refactor(api): only ignore stalls for downward portion of force pickup routine (#14725) --- api/src/opentrons/hardware_control/ot3api.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/api/src/opentrons/hardware_control/ot3api.py b/api/src/opentrons/hardware_control/ot3api.py index 24b613411c1..0d97855045f 100644 --- a/api/src/opentrons/hardware_control/ot3api.py +++ b/api/src/opentrons/hardware_control/ot3api.py @@ -2093,14 +2093,17 @@ async def _force_pick_up_tip( ) -> None: for press in pipette_spec.tip_action_moves: async with self._backend.motor_current(run_currents=press.currents): - target_down = target_position_from_relative( + target = target_position_from_relative( mount, top_types.Point(z=press.distance), self._current_position ) - await self._move(target_down, speed=press.speed, expect_stalls=True) - if press.distance < 0: - # we expect a stall has happened during a downward movement into the tiprack, so - # we want to update the motor estimation - await self._update_position_estimation([Axis.by_mount(mount)]) + if press.distance < 0: + # we expect a stall has happened during a downward movement into the tiprack, so + # we want to update the motor estimation + await self._move(target, speed=press.speed, expect_stalls=True) + await self._update_position_estimation([Axis.by_mount(mount)]) + else: + # we should not ignore stalls that happen during the retract part of the routine + await self._move(target, speed=press.speed, expect_stalls=False) async def _tip_motor_action( self, mount: OT3Mount, pipette_spec: List[TipActionMoveSpec] From 717993b01af816096906a760123d036adf307813 Mon Sep 17 00:00:00 2001 From: Seth Foster Date: Tue, 16 Apr 2024 19:18:19 -0400 Subject: [PATCH 146/194] fix(app): fix various install and version issues (#14926) Fixes various ongoing issues building versions into the system. This is a follow-on to #14844 (61b137132a3130e8cea170c7cf3d4c1931735942) - vite `define` config the way we do it does not hang the defined values off of props of `global` explicitly (or maybe us injecting `'globalThis'` into `global` breaks it) so use them as true globals, altering the way they're accessed and the way they're declared in the typings. - i guess you don't actually have to do type imports in the top level typings? removing the type import of the ipc bridge in the app-shell and app-shell-odd global.d.ts fixed that issue. don't know why - the ESM import for the script that updates the releases.json was wrong which breaks some update stuff ## Testing This is a bit annoying to test. You _must_ test this on a compiled app package. You _cannot_ test this on a dev build. On a compiled app package, - [x] the version should display in the settings tab of the app - [x] you shouldn't have warnings about `include` on undefined in your app logs - [ ] you should get robot update prompts when you use an internal-release build and connect to a robot running 1.3 or previous; you should get robot update prompts when you use a release build and connect to a robot running 7.2.1 or previous (note: couldn't test this in time but the rest of it works) - [x] the help menu should have a bugs url that works (the "report an issue" button; it should pop a browser tab) - [x] the help menu should say "View Opentrons App Logs" or "View Opentrons OT-3 App Logs" as the variant demands - [x] it should NOT say "View App Logs". that means the package name wasn't properly interpolated. I haven't dev-tested the second part because on my home setup making a full compiled app package is broken for some reason, and you can't actually run the node side in dev This once more, Closes EXEC-385 Closes RQA-2579 --- app-shell-odd/src/config/__fixtures__/index.ts | 4 +++- app-shell-odd/src/config/migrate.ts | 3 ++- app-shell-odd/src/update.ts | 14 +++++++------- app-shell-odd/typings/global.d.ts | 12 +++++------- app-shell/src/menu.ts | 9 +++++---- app-shell/src/robot-update/constants.ts | 5 +++-- app-shell/typings/global.d.ts | 9 +++++---- app/src/App/Navbar.tsx | 3 ++- app/src/redux/shell/index.ts | 2 +- app/typings/global.d.ts | 5 +++-- scripts/update-releases-json.js | 3 +-- setup-vitest.ts | 3 +++ 12 files changed, 40 insertions(+), 32 deletions(-) diff --git a/app-shell-odd/src/config/__fixtures__/index.ts b/app-shell-odd/src/config/__fixtures__/index.ts index 5e26ddc99ef..b3ff0cbfbd7 100644 --- a/app-shell-odd/src/config/__fixtures__/index.ts +++ b/app-shell-odd/src/config/__fixtures__/index.ts @@ -11,11 +11,13 @@ import type { ConfigV21, } from '@opentrons/app/src/redux/config/types' +const PKG_VERSION: string = _PKG_VERSION_ + export const MOCK_CONFIG_V12: ConfigV12 = { version: 12, devtools: false, reinstallDevtools: false, - update: { channel: _PKG_VERSION_.includes('beta') ? 'beta' : 'latest' }, + update: { channel: PKG_VERSION.includes('beta') ? 'beta' : 'latest' }, log: { level: { file: 'debug', console: 'info' } }, ui: { width: 1024, diff --git a/app-shell-odd/src/config/migrate.ts b/app-shell-odd/src/config/migrate.ts index 6d9a1c9b82b..9a05df79594 100644 --- a/app-shell-odd/src/config/migrate.ts +++ b/app-shell-odd/src/config/migrate.ts @@ -22,11 +22,12 @@ import type { const CONFIG_VERSION_LATEST = 21 // update this after each config version bump +const PKG_VERSION: string = _PKG_VERSION_ export const DEFAULTS_V12: ConfigV12 = { version: 12, devtools: false, reinstallDevtools: false, - update: { channel: _PKG_VERSION_.includes('beta') ? 'beta' : 'latest' }, + update: { channel: PKG_VERSION.includes('beta') ? 'beta' : 'latest' }, log: { level: { file: 'debug', console: 'info' } }, ui: { width: 1024, diff --git a/app-shell-odd/src/update.ts b/app-shell-odd/src/update.ts index f27ce2eced4..d1ea2f154b3 100644 --- a/app-shell-odd/src/update.ts +++ b/app-shell-odd/src/update.ts @@ -14,15 +14,15 @@ import type { ReleaseSetUrls } from './system-update/types' const log = createLogger('update') +const OPENTRONS_PROJECT: string = _OPENTRONS_PROJECT_ + export const FLEX_MANIFEST_URL = - // @ts-expect-error can't get TS to recognize global.d.ts - global._OPENTRONS_PROJECT_ && - // @ts-expect-error can't get TS to recognize global.d.ts - global._OPENTRONS_PROJECT_.includes('robot-stack') + OPENTRONS_PROJECT && OPENTRONS_PROJECT.includes('robot-stack') ? 'https://builds.opentrons.com/ot3-oe/releases.json' : 'https://ot3-development.builds.opentrons.com/ot3-oe/releases.json' -let LATEST_OT_SYSTEM_VERSION = _PKG_VERSION_ +const PKG_VERSION = _PKG_VERSION_ +let LATEST_OT_SYSTEM_VERSION = PKG_VERSION const channelFinder = (version: string, channel: string): boolean => { // return the latest alpha/beta if a user subscribes to alpha/beta updates @@ -60,7 +60,7 @@ export const updateLatestVersion = (): Promise => { }) .find(verson => channelFinder(verson, channel)) const changed = LATEST_OT_SYSTEM_VERSION !== latestAvailableVersion - LATEST_OT_SYSTEM_VERSION = latestAvailableVersion ?? _PKG_VERSION_ + LATEST_OT_SYSTEM_VERSION = latestAvailableVersion ?? PKG_VERSION if (changed) { log.info( `Update: latest version available from ${FLEX_MANIFEST_URL} is ${latestAvailableVersion}` @@ -80,7 +80,7 @@ export const getLatestVersion = (): string => { return LATEST_OT_SYSTEM_VERSION } -export const getCurrentVersion = (): string => _PKG_VERSION_ +export const getCurrentVersion = (): string => PKG_VERSION export const isUpdateAvailable = (): boolean => getLatestVersion() !== getCurrentVersion() diff --git a/app-shell-odd/typings/global.d.ts b/app-shell-odd/typings/global.d.ts index 8513596d045..3b470870c2b 100644 --- a/app-shell-odd/typings/global.d.ts +++ b/app-shell-odd/typings/global.d.ts @@ -1,11 +1,4 @@ -import type { IpcRenderer } from 'electron' - declare global { - const _PKG_VERSION_: string - const _PKG_PRODUCT_NAME_: string - const _PKG_BUGS_URL_: string - const _OPENTRONS_PROJECT_: string - namespace NodeJS { export interface Global { APP_SHELL_REMOTE: { @@ -14,3 +7,8 @@ declare global { } } } + +declare const _PKG_VERSION_: string +declare const _PKG_PRODUCT_NAME_: string +declare const _PKG_BUGS_URL_: string +declare const _OPENTRONS_PROJECT_: string diff --git a/app-shell/src/menu.ts b/app-shell/src/menu.ts index 71b1318df38..52f04978934 100644 --- a/app-shell/src/menu.ts +++ b/app-shell/src/menu.ts @@ -5,6 +5,9 @@ import type { MenuItemConstructorOptions } from 'electron' import { LOG_DIR } from './log' +const PRODUCT_NAME: string = _PKG_PRODUCT_NAME_ +const BUGS_URL: string = _PKG_BUGS_URL_ + // file or application menu const firstMenu: MenuItemConstructorOptions = { role: process.platform === 'darwin' ? 'appMenu' : 'fileMenu', @@ -27,8 +30,7 @@ const helpMenu: MenuItemConstructorOptions = { }, }, { - // @ts-expect-error can't get TS to recognize global.d.ts - label: `View ${global._PKG_PRODUCT_NAME_} App Logs`, + label: `View ${PRODUCT_NAME} App Logs`, click: () => { shell.openPath(LOG_DIR) }, @@ -37,8 +39,7 @@ const helpMenu: MenuItemConstructorOptions = { label: 'Report an Issue', click: () => { // eslint-disable-next-line @typescript-eslint/no-floating-promises - // @ts-expect-error can't get TS to recognize global.d.ts - shell.openExternal(global._PKG_BUGS_URL_) + shell.openExternal(BUGS_URL) }, }, ], diff --git a/app-shell/src/robot-update/constants.ts b/app-shell/src/robot-update/constants.ts index 48f8ef8e611..22a494d07d7 100644 --- a/app-shell/src/robot-update/constants.ts +++ b/app-shell/src/robot-update/constants.ts @@ -4,6 +4,8 @@ import type { UpdateManifestUrls } from './types' import type { RobotUpdateTarget } from '@opentrons/app/src/redux/robot-update/types' import { CURRENT_VERSION } from '../update' +const OPENTRONS_PROJECT: string = _OPENTRONS_PROJECT_ + const UPDATE_MANIFEST_URLS_RELEASE = { ot2: 'https://builds.opentrons.com/ot2-br/releases.json', flex: 'https://builds.opentrons.com/ot3-oe/releases.json', @@ -15,8 +17,7 @@ const UPDATE_MANIFEST_URLS_INTERNAL_RELEASE = { } export const getUpdateManifestUrls = (): UpdateManifestUrls => - // @ts-expect-error can't get TS to recognize global.d.ts - global._OPENTRONS_PROJECT_.includes('robot-stack') + OPENTRONS_PROJECT.includes('robot-stack') ? UPDATE_MANIFEST_URLS_RELEASE : UPDATE_MANIFEST_URLS_INTERNAL_RELEASE diff --git a/app-shell/typings/global.d.ts b/app-shell/typings/global.d.ts index 8bdea90e637..67f9a5a1955 100644 --- a/app-shell/typings/global.d.ts +++ b/app-shell/typings/global.d.ts @@ -1,8 +1,9 @@ /* eslint-disable no-var */ declare global { - var _PKG_VERSION_: string - var _PKG_PRODUCT_NAME_: string - var _PKG_BUGS_URL_: string - var _OPENTRONS_PROJECT_: string var APP_SHELL_REMOTE: { ipcRenderer: IpcRenderer; [key: string]: any } } + +declare const _PKG_VERSION_: string +declare const _PKG_PRODUCT_NAME_: string +declare const _PKG_BUGS_URL_: string +declare const _OPENTRONS_PROJECT_: string diff --git a/app/src/App/Navbar.tsx b/app/src/App/Navbar.tsx index 8397927392f..f9e79ea65e9 100644 --- a/app/src/App/Navbar.tsx +++ b/app/src/App/Navbar.tsx @@ -28,6 +28,7 @@ import { NAV_BAR_WIDTH } from './constants' import type { RouteProps } from './types' const SALESFORCE_HELP_LINK = 'https://support.opentrons.com/s/' +const PROJECT: string = _OPENTRONS_PROJECT_ const NavbarLink = styled(NavLink)` color: ${COLORS.white}; @@ -128,7 +129,7 @@ export function Navbar({ routes }: { routes: RouteProps[] }): JSX.Element { alignSelf={ALIGN_STRETCH} > {navRoutes.map(({ name, navLinkTo }: RouteProps) => ( diff --git a/app/src/redux/shell/index.ts b/app/src/redux/shell/index.ts index a709a770d7f..5a918f75eb3 100644 --- a/app/src/redux/shell/index.ts +++ b/app/src/redux/shell/index.ts @@ -5,4 +5,4 @@ export * from './update' export * from './is-ready/actions' export * from './is-ready/selectors' -export const CURRENT_VERSION: string = (global as any)._PKG_VERSION_ +export const CURRENT_VERSION: string = _PKG_VERSION_ diff --git a/app/typings/global.d.ts b/app/typings/global.d.ts index 772bcf9ffd0..de736627240 100644 --- a/app/typings/global.d.ts +++ b/app/typings/global.d.ts @@ -1,6 +1,4 @@ declare const global: typeof globalThis & { - _PKG_VERSION_: string - _OPENTRONS_PROJECT_: string APP_SHELL_REMOTE: { // sa 02-02-2024 any typing this because importing the IpcRenderer type // from electron makes this ambient type declaration a module instead of @@ -9,3 +7,6 @@ declare const global: typeof globalThis & { [key: string]: any } } + +declare const _PKG_VERSION_: string +declare const _OPENTRONS_PROJECT_: string diff --git a/scripts/update-releases-json.js b/scripts/update-releases-json.js index 0e529d5447e..d7aa9b0ca21 100644 --- a/scripts/update-releases-json.js +++ b/scripts/update-releases-json.js @@ -4,8 +4,6 @@ const fs = require('fs/promises') // Updates a releases historical manifest with a release's version. -const versionFinder = require('./git-version.mjs') - const parseArgs = require('./deploy/lib/parseArgs') const USAGE = '\nUsage:\n node ./scripts/update-releases-json ' @@ -63,6 +61,7 @@ async function main() { } console.log(`Updating ${releasesPath} with artifacts from ${artifactDirPath}`) const releasesData = await readOrDefaultReleases(releasesPath) + const versionFinder = await import('./git-version.mjs') const version = await versionFinder.versionForProject(project) console.log(`Adding data for ${version}`) releasesData.production[version] = { diff --git a/setup-vitest.ts b/setup-vitest.ts index 07bd135137d..bf9d07a6ba7 100644 --- a/setup-vitest.ts +++ b/setup-vitest.ts @@ -10,6 +10,9 @@ vi.mock('./app/src/redux/shell/remote') process.env.OT_PD_VERSION = 'fake_PD_version' global._PKG_VERSION_ = 'test environment' +global._OPENTRONS_PROJECT_ = 'robotics' +global._PKG_PRODUCT_NAME_ = 'test product' +global._PKG_BUGS_URL_ = 'http://bugs.contoso.com' afterEach(() => { cleanup() From 2fadbf1b37e37c7c5d81e20024eec70fe4152099 Mon Sep 17 00:00:00 2001 From: Josh McVey Date: Tue, 16 Apr 2024 20:25:32 -0500 Subject: [PATCH 147/194] chore(release): ot3@1.4.0-alpha.1 release notes (#14930) # Internal release notes --- api/release-notes-internal.md | 12 ++++++++++++ app-shell/build/release-notes-internal.md | 14 ++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/api/release-notes-internal.md b/api/release-notes-internal.md index 21ec74be010..261d55e2100 100644 --- a/api/release-notes-internal.md +++ b/api/release-notes-internal.md @@ -2,6 +2,18 @@ For more details about this release, please see the full [technical change log][ [technical change log]: https://github.com/Opentrons/opentrons/releases +## Internal Release 1.4.0-alpha.1 + +This internal release is from the `edge` branch to contain rapid dev on new features for 7.3.0. This release is for internal testing purposes and if used may require a factory reset of the robot to return to a stable version. + +This release is primarily to unblock Flex runs. That fix is in + +### All changes + + + +--- + ## Internal Release 1.4.0-alpha.0 This internal release is from the `edge` branch to contain rapid dev on new features for 7.3.0. This release is for internal testing purposes and if used may require a factory reset of the robot to return to a stable version. diff --git a/app-shell/build/release-notes-internal.md b/app-shell/build/release-notes-internal.md index 92e1af7c0d8..591aa411a3c 100644 --- a/app-shell/build/release-notes-internal.md +++ b/app-shell/build/release-notes-internal.md @@ -1,6 +1,20 @@ For more details about this release, please see the full [technical changelog][]. [technical change log]: https://github.com/Opentrons/opentrons/releases +## Internal Release 1.4.0-alpha.1 + +This internal release is from the `edge` branch to contain rapid dev on new features for 7.3.0. This release is for internal testing purposes and if used may require a factory reset of the robot to return to a stable version. + +### Notable bug fixes + +App and robot update prompts should now function properly. However, updating from 1.4.0-alpha.0 to 1.4.0-alpha.1 will still present issues, as the fix is not in 1.4.0-alpha.0. After installing 1.4.0-alpha.1, switch your update channel to "latest" to receive the latest stable internal release prompt, which validates the fix. + +### All changes + + + +--- + ## Internal Release 1.4.0-alpha.0 This internal release is from the `edge` branch to contain rapid dev on new features for 7.3.0. This release is for internal testing purposes and if used may require a factory reset of the robot to return to a stable version. From 3f9cae7b5d3f4059d309b5e54388cdfd33cc7d10 Mon Sep 17 00:00:00 2001 From: koji Date: Wed, 17 Apr 2024 09:17:55 -0400 Subject: [PATCH 148/194] feat(opentrons-ai-client): add ChatDisplay component (#14927) * feat(opentrons-ai-client): add ChatDisplay component --- .../ChatDisplay/ChatDisplay.stories.tsx | 77 +++++++++++++++++++ .../__tests__/ChatDisplay.test.tsx | 42 ++++++++++ .../src/molecules/ChatDisplay/index.tsx | 43 +++++++++++ 3 files changed, 162 insertions(+) create mode 100644 opentrons-ai-client/src/molecules/ChatDisplay/ChatDisplay.stories.tsx create mode 100644 opentrons-ai-client/src/molecules/ChatDisplay/__tests__/ChatDisplay.test.tsx create mode 100644 opentrons-ai-client/src/molecules/ChatDisplay/index.tsx diff --git a/opentrons-ai-client/src/molecules/ChatDisplay/ChatDisplay.stories.tsx b/opentrons-ai-client/src/molecules/ChatDisplay/ChatDisplay.stories.tsx new file mode 100644 index 00000000000..cd4d08a1701 --- /dev/null +++ b/opentrons-ai-client/src/molecules/ChatDisplay/ChatDisplay.stories.tsx @@ -0,0 +1,77 @@ +import React from 'react' +import { I18nextProvider } from 'react-i18next' +import { COLORS, Flex, SPACING } from '@opentrons/components' +import { i18n } from '../../i18n' +import { ChatDisplay } from './index' + +import type { Meta, StoryObj } from '@storybook/react' + +const meta: Meta = { + title: 'AI/molecules/ChatDisplay', + component: ChatDisplay, + decorators: [ + Story => ( + + + + + + ), + ], +} +export default meta +type Story = StoryObj +export const OpentronsAI: Story = { + args: { + text: ` + \`\`\`python +from opentrons import protocol_api + +# Metadata +metadata = { + 'protocolName': 'ThermoPrime Taq DNA Polymerase PCR Amplification', + 'author': 'Name ', + 'description': 'PCR amplification using ThermoPrime Taq DNA Polymerase kit', + 'apiLevel': '2.11' +} + +# Protocol run function +def run(protocol: protocol_api.ProtocolContext): + + # Constants + NO_OF_SAMPLES = 41 + SAMPLE_VOL = 3 # uL + MASTERMIX_VOL = 10 # uL + TC_SAMPLE_MASTERMIX_MIXES = 4 + TC_SAMPLE_MASTERMIX_MIX_VOLUME = SAMPLE_VOL + MASTERMIX_VOL + MASTERMIX_BLOCK_TEMP = 10 # degree C + TEMP_DECK_WAIT_TIME = 50 # seconds +`, + isUserInput: false, + }, +} +export const User: Story = { + args: { + text: ` + - Application: Reagent transfer + - Robot: OT-2 + - API: 2.13 + + Pipette mount: + - P1000 Single-Channel GEN2 is mounted on left + - P300 Single-Channel GEN2 is mounted on right + + Labware: + - Source Labware: Thermo Scientific Nunc 96 Well Plate 2000 µL on slot 7 + - Destination Labware: Opentrons 24 Well Aluminum Block with NEST 0.5 mL Screwcap on slot 3 + - Tiprack: Opentrons 96 Filter Tip Rack 1000 µL on slot 4 + + Commands: + - Using P1000 Single-Channel GEN2 pipette on left mount, transfer 195.0 uL of reagent + from H10, F12, D7, B1, C8 wells in source labware + to first well in the destination labware. + Use new tip for each transfer. + `, + isUserInput: true, + }, +} diff --git a/opentrons-ai-client/src/molecules/ChatDisplay/__tests__/ChatDisplay.test.tsx b/opentrons-ai-client/src/molecules/ChatDisplay/__tests__/ChatDisplay.test.tsx new file mode 100644 index 00000000000..ad9bf527a0b --- /dev/null +++ b/opentrons-ai-client/src/molecules/ChatDisplay/__tests__/ChatDisplay.test.tsx @@ -0,0 +1,42 @@ +import React from 'react' +import { screen } from '@testing-library/react' +import { describe, it, beforeEach } from 'vitest' +import { renderWithProviders } from '../../../__testing-utils__' +import { i18n } from '../../../i18n' + +import { ChatDisplay } from '../index' + +const render = (props: React.ComponentProps) => { + return renderWithProviders(, { i18nInstance: i18n }) +} + +describe('ChatDisplay', () => { + let props: React.ComponentProps + + beforeEach(() => { + props = { + text: 'mock text from the backend', + isUserInput: false, + } + }) + it('should display response from the backend and label', () => { + render(props) + screen.getByText('OpentronsAI') + screen.getByText('mock text from the backend') + // ToDO (kk:04/16/2024) activate the following when jsdom's issue is solved + // const display = screen.getByTextId('ChatDisplay_from_backend') + // expect(display).toHaveStyle(`background-color: ${COLORS.grey30}`) + }) + it('should display input from use and label', () => { + props = { + text: 'mock text from user input', + isUserInput: true, + } + render(props) + screen.getByText('You') + screen.getByText('mock text from user input') + // ToDO (kk:04/16/2024) activate the following when jsdom's issue is solved + // const display = screen.getByTextId('ChatDisplay_from_user') + // expect(display).toHaveStyle(`background-color: ${COLORS.blue}`) + }) +}) diff --git a/opentrons-ai-client/src/molecules/ChatDisplay/index.tsx b/opentrons-ai-client/src/molecules/ChatDisplay/index.tsx new file mode 100644 index 00000000000..f18bc9f4998 --- /dev/null +++ b/opentrons-ai-client/src/molecules/ChatDisplay/index.tsx @@ -0,0 +1,43 @@ +import React from 'react' +import { useTranslation } from 'react-i18next' +import { + BORDERS, + COLORS, + DIRECTION_COLUMN, + Flex, + SPACING, + StyledText, +} from '@opentrons/components' + +interface ChatDisplayProps { + text: string + isUserInput: boolean +} + +export function ChatDisplay({ + text, + isUserInput, +}: ChatDisplayProps): JSX.Element { + const { t } = useTranslation('protocol_generator') + return ( + + {isUserInput ? t('you') : t('opentronsai')} + {/* text should be markdown so this component will have a package or function to parse markdown */} + + {text} + + + ) +} From e20c8e59c65c064e8ab77d525c92c468bc16007c Mon Sep 17 00:00:00 2001 From: koji Date: Wed, 17 Apr 2024 11:21:37 -0400 Subject: [PATCH 149/194] chore: add test workflow for opentrons-ai-client (#14923) * chore: add test workflow for opentrons-ai-client --- ...opentrons-ai-client-test-build-deploy.yaml | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 .github/workflows/opentrons-ai-client-test-build-deploy.yaml diff --git a/.github/workflows/opentrons-ai-client-test-build-deploy.yaml b/.github/workflows/opentrons-ai-client-test-build-deploy.yaml new file mode 100644 index 00000000000..072366ab0d7 --- /dev/null +++ b/.github/workflows/opentrons-ai-client-test-build-deploy.yaml @@ -0,0 +1,78 @@ +# Run tests, build the app, and deploy it cross platform + +name: 'OpentronsAI client test, build, and deploy' + +# ToDo (kk:04/16/2024) Add build and deploy task + +on: + push: + paths: + - 'Makefile' + - 'opentrons-ai-client/**/*' + - 'components/**/*' + - '*.js' + - '*.json' + - 'yarn.lock' + - '.github/workflows/app-test-build-deploy.yaml' + - '.github/workflows/utils.js' + branches: + - '**' + tags: + - 'v*' + - 'ot3@*' + pull_request: + paths: + - 'Makefile' + - 'opentrons-ai-client/**/*' + - 'components/**/*' + - '*.js' + - '*.json' + - 'yarn.lock' + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}-${{ github.ref_name != 'edge' || github.run_id}}-${{ github.ref_type != 'tag' || github.run_id }} + cancel-in-progress: true + +env: + CI: true + +jobs: + js-unit-test: + runs-on: 'ubuntu-22.04' + name: 'opentrons ai frontend unit tests' + timeout-minutes: 60 + steps: + - uses: 'actions/checkout@v3' + - uses: 'actions/setup-node@v3' + with: + node-version: '18.19.0' + - name: 'install udev' + run: sudo apt-get update && sudo apt-get install libudev-dev + - name: 'set complex environment variables' + id: 'set-vars' + uses: actions/github-script@v6 + with: + script: | + const { buildComplexEnvVars } = require(`${process.env.GITHUB_WORKSPACE}/.github/workflows/utils.js`) + buildComplexEnvVars(core, context) + - name: 'cache yarn cache' + uses: actions/cache@v3 + with: + path: | + ${{ github.workspace }}/.npm-cache/_prebuild + ${{ github.workspace }}/.yarn-cache + key: js-${{ secrets.GH_CACHE_VERSION }}-${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }} + - name: 'setup-js' + run: | + npm config set cache ${{ github.workspace }}/.npm-cache + yarn config set cache-folder ${{ github.workspace }}/.yarn-cache + make setup-js + - name: 'test frontend packages' + run: | + make -C opentrons-ai-client test-cov + - name: 'Upload coverage report' + uses: codecov/codecov-action@v3 + with: + files: ./coverage/lcov.info + flags: opentrons-ai-client From 0fcbfb476257d0b353d58337828eadcd68ba1f4a Mon Sep 17 00:00:00 2001 From: Sarah Breen Date: Wed, 17 Apr 2024 12:14:53 -0400 Subject: [PATCH 150/194] fix(robot-server): revert test-flex.json back to two pipettes (#14931) --- robot-server/simulators/test-flex.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/robot-server/simulators/test-flex.json b/robot-server/simulators/test-flex.json index dce76ffc67b..adc9543fc5a 100644 --- a/robot-server/simulators/test-flex.json +++ b/robot-server/simulators/test-flex.json @@ -2,8 +2,12 @@ "machine": "OT-3 Standard", "strict_attached_instruments": false, "attached_instruments": { + "right": { + "model": "p1000_single_3.4", + "id": "321" + }, "left": { - "model": "p1000_96_v1", + "model": "p50_single_3.4", "id": "123" } }, From d4bc2da2d31f7bb26440d576ba188f8d41c6f837 Mon Sep 17 00:00:00 2001 From: Brayan Almonte Date: Wed, 17 Apr 2024 12:27:21 -0400 Subject: [PATCH 151/194] fix(hardware): remove message ignored by filter spammy log (#14932) --- .../opentrons_hardware/drivers/binary_usb/binary_messenger.py | 1 - hardware/opentrons_hardware/drivers/can_bus/can_messenger.py | 1 - 2 files changed, 2 deletions(-) diff --git a/hardware/opentrons_hardware/drivers/binary_usb/binary_messenger.py b/hardware/opentrons_hardware/drivers/binary_usb/binary_messenger.py index 49c1584526d..4c54e5a8632 100644 --- a/hardware/opentrons_hardware/drivers/binary_usb/binary_messenger.py +++ b/hardware/opentrons_hardware/drivers/binary_usb/binary_messenger.py @@ -196,7 +196,6 @@ async def _read_task(self) -> None: if filter and not filter( BinaryMessageId(message_definition.message_id.value) ): - log.debug("message ignored by filter") continue listener(message_definition) if ( diff --git a/hardware/opentrons_hardware/drivers/can_bus/can_messenger.py b/hardware/opentrons_hardware/drivers/can_bus/can_messenger.py index 4446b3b0683..c0b49e376bb 100644 --- a/hardware/opentrons_hardware/drivers/can_bus/can_messenger.py +++ b/hardware/opentrons_hardware/drivers/can_bus/can_messenger.py @@ -379,7 +379,6 @@ async def _read_task(self) -> None: handled = False for listener, filter in self._listeners.values(): if filter and not filter(message.arbitration_id): - log.debug("message ignored by filter") continue listener(message_definition(payload=build), message.arbitration_id) # type: ignore[arg-type] handled = True From 86e1d47f7129bcb98733e17084229014bbe5914b Mon Sep 17 00:00:00 2001 From: Shlok Amin Date: Wed, 17 Apr 2024 11:59:39 -0600 Subject: [PATCH 152/194] ci(components): fix github deploy action (#14935) closes AUTH-331 --- .github/workflows/components-test-build-deploy.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/components-test-build-deploy.yaml b/.github/workflows/components-test-build-deploy.yaml index 78e60426b3f..d1de9d2b619 100644 --- a/.github/workflows/components-test-build-deploy.yaml +++ b/.github/workflows/components-test-build-deploy.yaml @@ -178,6 +178,7 @@ jobs: run: | npm config set cache ./.npm-cache yarn config set cache-folder ./.yarn-cache + make setup-js - name: 'build typescript' run: make build-ts - name: 'build library' From aae8a102fd1d99c079fe3e9b15f6588574d5bd81 Mon Sep 17 00:00:00 2001 From: Jethary Rader <66035149+jerader@users.noreply.github.com> Date: Wed, 17 Apr 2024 14:34:14 -0400 Subject: [PATCH 153/194] feat(shared-data, api): add uiMaxFlowRate key to pipette definitions (#14859) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit closes AUTH-328 This PR add max flow rate values to each GEN2 and GEN3 pipette model definition to be used for FE applications. Rather than having a max flow rate for every pipette's supported tip's volume for aspirate and dispense, we have only 1 max flow rate for every pipette's supported tip that is. That value is based off of the lowest volume minus 2% to account for safety. For example: `p1000_single_v3.6` has a `minVolume` of 5uL. The `t50` tips has a max flow rate at volume 5uL of `801.3`. So the `uiMaxFlowRate` value is 98% of that which is `785.2` 🔈 Additionally, the `lowVolumeDefault` default flow rates for `aspirate`, `dispense`, and `blowout` have been changed to match the `uiMaxFlowRate`. This DOES NOT result in a physical change behavior and the change is needed for ui purposes. --- .../instruments/ot2/pipette.py | 11 +-- .../instruments/ot3/pipette.py | 4 +- shared-data/js/__tests__/pipettes.test.ts | 14 +-- shared-data/js/types.ts | 1 + .../eight_channel/p1000/default/3_3.json | 3 + .../eight_channel/p1000/default/3_4.json | 3 + .../eight_channel/p1000/default/3_5.json | 3 + .../liquid/eight_channel/p20/default/2_1.json | 2 + .../eight_channel/p300/default/2_1.json | 2 + .../liquid/eight_channel/p50/default/3_3.json | 1 + .../liquid/eight_channel/p50/default/3_4.json | 1 + .../liquid/eight_channel/p50/default/3_5.json | 1 + .../p50/lowVolumeDefault/3_3.json | 1 + .../p50/lowVolumeDefault/3_4.json | 13 +-- .../p50/lowVolumeDefault/3_5.json | 13 +-- .../ninety_six_channel/p1000/default/3_3.json | 3 + .../ninety_six_channel/p1000/default/3_4.json | 3 + .../ninety_six_channel/p1000/default/3_5.json | 3 + .../ninety_six_channel/p1000/default/3_6.json | 3 + .../single_channel/p1000/default/2_1.json | 1 + .../single_channel/p1000/default/2_2.json | 1 + .../single_channel/p1000/default/3_3.json | 3 + .../single_channel/p1000/default/3_4.json | 3 + .../single_channel/p1000/default/3_5.json | 3 + .../single_channel/p1000/default/3_6.json | 3 + .../single_channel/p20/default/2_1.json | 2 + .../single_channel/p20/default/2_2.json | 2 + .../single_channel/p300/default/2_1.json | 2 + .../single_channel/p50/default/3_3.json | 1 + .../single_channel/p50/default/3_4.json | 1 + .../single_channel/p50/default/3_5.json | 1 + .../p50/lowVolumeDefault/3_3.json | 1 + .../p50/lowVolumeDefault/3_4.json | 13 +-- .../p50/lowVolumeDefault/3_5.json | 13 +-- .../2/pipetteLiquidPropertiesSchema.json | 14 ++- .../pipette/pipette_definition.py | 13 ++- .../pipette/ul_per_mm.py | 0 .../pipette/test_max_flow_rates_per_volume.py | 88 +++++++++++++++++++ 38 files changed, 207 insertions(+), 43 deletions(-) rename api/src/opentrons/hardware_control/instruments/instrument_helpers.py => shared-data/python/opentrons_shared_data/pipette/ul_per_mm.py (100%) create mode 100644 shared-data/python/tests/pipette/test_max_flow_rates_per_volume.py diff --git a/api/src/opentrons/hardware_control/instruments/ot2/pipette.py b/api/src/opentrons/hardware_control/instruments/ot2/pipette.py index 2d20a4f592a..f8a9d48da60 100644 --- a/api/src/opentrons/hardware_control/instruments/ot2/pipette.py +++ b/api/src/opentrons/hardware_control/instruments/ot2/pipette.py @@ -26,6 +26,11 @@ InvalidLiquidClassName, CommandPreconditionViolated, ) +from opentrons_shared_data.pipette.ul_per_mm import ( + piecewise_volume_conversion, + PIPETTING_FUNCTION_FALLBACK_VERSION, + PIPETTING_FUNCTION_LATEST_VERSION, +) from opentrons.types import Point, Mount @@ -33,11 +38,7 @@ from opentrons.config.types import RobotConfig from opentrons.drivers.types import MoveSplit from ..instrument_abc import AbstractInstrument -from ..instrument_helpers import ( - piecewise_volume_conversion, - PIPETTING_FUNCTION_FALLBACK_VERSION, - PIPETTING_FUNCTION_LATEST_VERSION, -) + from .instrument_calibration import ( PipetteOffsetByPipetteMount, load_pipette_offset, diff --git a/api/src/opentrons/hardware_control/instruments/ot3/pipette.py b/api/src/opentrons/hardware_control/instruments/ot3/pipette.py index b2dc7f01c02..7d72058d1ce 100644 --- a/api/src/opentrons/hardware_control/instruments/ot3/pipette.py +++ b/api/src/opentrons/hardware_control/instruments/ot3/pipette.py @@ -25,12 +25,12 @@ CommandPreconditionViolated, PythonException, ) -from ..instrument_abc import AbstractInstrument -from ..instrument_helpers import ( +from opentrons_shared_data.pipette.ul_per_mm import ( piecewise_volume_conversion, PIPETTING_FUNCTION_FALLBACK_VERSION, PIPETTING_FUNCTION_LATEST_VERSION, ) +from ..instrument_abc import AbstractInstrument from .instrument_calibration import ( save_pipette_offset_calibration, load_pipette_offset, diff --git a/shared-data/js/__tests__/pipettes.test.ts b/shared-data/js/__tests__/pipettes.test.ts index 6eae38eba66..15c72cd9882 100644 --- a/shared-data/js/__tests__/pipettes.test.ts +++ b/shared-data/js/__tests__/pipettes.test.ts @@ -158,6 +158,7 @@ describe('pipette data accessors', () => { minVolume: 5, supportedTips: { t50: { + uiMaxFlowRate: 47, aspirate: { default: { 1: expect.anything(), @@ -205,27 +206,28 @@ describe('pipette data accessors', () => { minVolume: 1, supportedTips: { t50: { + uiMaxFlowRate: 26.7, aspirate: { default: { 1: expect.anything(), }, }, defaultAspirateFlowRate: { - default: 35, + default: 26.7, valuesByApiLevel: { - 2.14: 35, + 2.14: 26.7, }, }, defaultBlowOutFlowRate: { - default: 57, + default: 26.7, valuesByApiLevel: { - 2.14: 57, + 2.14: 26.7, }, }, defaultDispenseFlowRate: { - default: 57, + default: 26.7, valuesByApiLevel: { - 2.14: 57, + 2.14: 26.7, }, }, defaultFlowAcceleration: 1200, diff --git a/shared-data/js/types.ts b/shared-data/js/types.ts index ff956aefaf6..4d51f992f22 100644 --- a/shared-data/js/types.ts +++ b/shared-data/js/types.ts @@ -492,6 +492,7 @@ export interface SupportedTip { } defaultReturnTipHeight?: number defaultFlowAcceleration?: number + uiMaxFlowRate?: number } export interface SupportedTips { diff --git a/shared-data/pipette/definitions/2/liquid/eight_channel/p1000/default/3_3.json b/shared-data/pipette/definitions/2/liquid/eight_channel/p1000/default/3_3.json index 12736030d8e..fd4f29a83bb 100644 --- a/shared-data/pipette/definitions/2/liquid/eight_channel/p1000/default/3_3.json +++ b/shared-data/pipette/definitions/2/liquid/eight_channel/p1000/default/3_3.json @@ -2,6 +2,7 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json", "supportedTips": { "t50": { + "uiMaxFlowRate": 808.3, "defaultAspirateFlowRate": { "default": 6, "valuesByApiLevel": { "2.14": 6 } @@ -116,6 +117,7 @@ "defaultPushOutVolume": 7 }, "t200": { + "uiMaxFlowRate": 905.7, "defaultAspirateFlowRate": { "default": 80, "valuesByApiLevel": { "2.14": 80 } @@ -228,6 +230,7 @@ "defaultPushOutVolume": 5 }, "t1000": { + "uiMaxFlowRate": 787.7, "defaultAspirateFlowRate": { "default": 160, "valuesByApiLevel": { "2.14": 160 } diff --git a/shared-data/pipette/definitions/2/liquid/eight_channel/p1000/default/3_4.json b/shared-data/pipette/definitions/2/liquid/eight_channel/p1000/default/3_4.json index ae95738fb09..dcc9d533490 100644 --- a/shared-data/pipette/definitions/2/liquid/eight_channel/p1000/default/3_4.json +++ b/shared-data/pipette/definitions/2/liquid/eight_channel/p1000/default/3_4.json @@ -2,6 +2,7 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json", "supportedTips": { "t50": { + "uiMaxFlowRate": 808.3, "defaultAspirateFlowRate": { "default": 478, "valuesByApiLevel": { "2.14": 478 } @@ -116,6 +117,7 @@ "defaultPushOutVolume": 7 }, "t200": { + "uiMaxFlowRate": 905.7, "defaultAspirateFlowRate": { "default": 716, "valuesByApiLevel": { "2.14": 716 } @@ -228,6 +230,7 @@ "defaultPushOutVolume": 5 }, "t1000": { + "uiMaxFlowRate": 787.7, "defaultAspirateFlowRate": { "default": 716, "valuesByApiLevel": { "2.14": 716 } diff --git a/shared-data/pipette/definitions/2/liquid/eight_channel/p1000/default/3_5.json b/shared-data/pipette/definitions/2/liquid/eight_channel/p1000/default/3_5.json index 1906adc8372..83026842153 100644 --- a/shared-data/pipette/definitions/2/liquid/eight_channel/p1000/default/3_5.json +++ b/shared-data/pipette/definitions/2/liquid/eight_channel/p1000/default/3_5.json @@ -2,6 +2,7 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json", "supportedTips": { "t50": { + "uiMaxFlowRate": 802.9, "defaultAspirateFlowRate": { "default": 478, "valuesByApiLevel": { "2.14": 478 } @@ -82,6 +83,7 @@ "defaultPushOutVolume": 7 }, "t200": { + "uiMaxFlowRate": 847.9, "defaultAspirateFlowRate": { "default": 716, "valuesByApiLevel": { "2.14": 716 } @@ -160,6 +162,7 @@ "defaultPushOutVolume": 5 }, "t1000": { + "uiMaxFlowRate": 744.6, "defaultAspirateFlowRate": { "default": 716, "valuesByApiLevel": { "2.14": 716 } diff --git a/shared-data/pipette/definitions/2/liquid/eight_channel/p20/default/2_1.json b/shared-data/pipette/definitions/2/liquid/eight_channel/p20/default/2_1.json index 22950b76875..aa83a2e5bda 100644 --- a/shared-data/pipette/definitions/2/liquid/eight_channel/p20/default/2_1.json +++ b/shared-data/pipette/definitions/2/liquid/eight_channel/p20/default/2_1.json @@ -2,6 +2,7 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json", "supportedTips": { "t20": { + "uiMaxFlowRate": 25, "defaultAspirateFlowRate": { "default": 7.6, "valuesByApiLevel": { "2.0": 7.6 } @@ -86,6 +87,7 @@ "defaultPushOutVolume": 0 }, "t10": { + "uiMaxFlowRate": 23, "defaultAspirateFlowRate": { "default": 7.6, "valuesByApiLevel": { "2.0": 7.6 } diff --git a/shared-data/pipette/definitions/2/liquid/eight_channel/p300/default/2_1.json b/shared-data/pipette/definitions/2/liquid/eight_channel/p300/default/2_1.json index a7d91165db7..4fee623f602 100644 --- a/shared-data/pipette/definitions/2/liquid/eight_channel/p300/default/2_1.json +++ b/shared-data/pipette/definitions/2/liquid/eight_channel/p300/default/2_1.json @@ -2,6 +2,7 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json", "supportedTips": { "t200": { + "uiMaxFlowRate": 335.3, "defaultAspirateFlowRate": { "default": 94, "valuesByApiLevel": { "2.0": 94 } @@ -89,6 +90,7 @@ "defaultPushOutVolume": 0 }, "t300": { + "uiMaxFlowRate": 335.3, "defaultAspirateFlowRate": { "default": 94, "valuesByApiLevel": { "2.0": 94 } diff --git a/shared-data/pipette/definitions/2/liquid/eight_channel/p50/default/3_3.json b/shared-data/pipette/definitions/2/liquid/eight_channel/p50/default/3_3.json index ac12e0bea1e..38a4b01df80 100644 --- a/shared-data/pipette/definitions/2/liquid/eight_channel/p50/default/3_3.json +++ b/shared-data/pipette/definitions/2/liquid/eight_channel/p50/default/3_3.json @@ -2,6 +2,7 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json", "supportedTips": { "t50": { + "uiMaxFlowRate": 46.8, "defaultAspirateFlowRate": { "default": 8, "valuesByApiLevel": { "2.14": 8 } diff --git a/shared-data/pipette/definitions/2/liquid/eight_channel/p50/default/3_4.json b/shared-data/pipette/definitions/2/liquid/eight_channel/p50/default/3_4.json index 352f61bae30..32131ee1982 100644 --- a/shared-data/pipette/definitions/2/liquid/eight_channel/p50/default/3_4.json +++ b/shared-data/pipette/definitions/2/liquid/eight_channel/p50/default/3_4.json @@ -2,6 +2,7 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json", "supportedTips": { "t50": { + "uiMaxFlowRate": 46.8, "defaultAspirateFlowRate": { "default": 35, "valuesByApiLevel": { "2.14": 35 } diff --git a/shared-data/pipette/definitions/2/liquid/eight_channel/p50/default/3_5.json b/shared-data/pipette/definitions/2/liquid/eight_channel/p50/default/3_5.json index 49b2a7b549d..ca2a48db274 100644 --- a/shared-data/pipette/definitions/2/liquid/eight_channel/p50/default/3_5.json +++ b/shared-data/pipette/definitions/2/liquid/eight_channel/p50/default/3_5.json @@ -2,6 +2,7 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json", "supportedTips": { "t50": { + "uiMaxFlowRate": 46.7, "defaultAspirateFlowRate": { "default": 35, "valuesByApiLevel": { "2.14": 35 } diff --git a/shared-data/pipette/definitions/2/liquid/eight_channel/p50/lowVolumeDefault/3_3.json b/shared-data/pipette/definitions/2/liquid/eight_channel/p50/lowVolumeDefault/3_3.json index 4e83eee5d81..cc629f28316 100644 --- a/shared-data/pipette/definitions/2/liquid/eight_channel/p50/lowVolumeDefault/3_3.json +++ b/shared-data/pipette/definitions/2/liquid/eight_channel/p50/lowVolumeDefault/3_3.json @@ -2,6 +2,7 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json", "supportedTips": { "t50": { + "uiMaxFlowRate": 32.6, "defaultAspirateFlowRate": { "default": 8, "valuesByApiLevel": { "2.14": 8 } diff --git a/shared-data/pipette/definitions/2/liquid/eight_channel/p50/lowVolumeDefault/3_4.json b/shared-data/pipette/definitions/2/liquid/eight_channel/p50/lowVolumeDefault/3_4.json index 881e9583aa5..0e9284b04e6 100644 --- a/shared-data/pipette/definitions/2/liquid/eight_channel/p50/lowVolumeDefault/3_4.json +++ b/shared-data/pipette/definitions/2/liquid/eight_channel/p50/lowVolumeDefault/3_4.json @@ -2,17 +2,18 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json", "supportedTips": { "t50": { + "uiMaxFlowRate": 32.6, "defaultAspirateFlowRate": { - "default": 35, - "valuesByApiLevel": { "2.14": 35 } + "default": 32.6, + "valuesByApiLevel": { "2.14": 32.6 } }, "defaultDispenseFlowRate": { - "default": 57, - "valuesByApiLevel": { "2.14": 57 } + "default": 32.6, + "valuesByApiLevel": { "2.14": 32.6 } }, "defaultBlowOutFlowRate": { - "default": 57, - "valuesByApiLevel": { "2.14": 57 } + "default": 32.6, + "valuesByApiLevel": { "2.14": 32.6 } }, "defaultFlowAcceleration": 1200.0, "defaultTipLength": 57.9, diff --git a/shared-data/pipette/definitions/2/liquid/eight_channel/p50/lowVolumeDefault/3_5.json b/shared-data/pipette/definitions/2/liquid/eight_channel/p50/lowVolumeDefault/3_5.json index 881e9583aa5..0e9284b04e6 100644 --- a/shared-data/pipette/definitions/2/liquid/eight_channel/p50/lowVolumeDefault/3_5.json +++ b/shared-data/pipette/definitions/2/liquid/eight_channel/p50/lowVolumeDefault/3_5.json @@ -2,17 +2,18 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json", "supportedTips": { "t50": { + "uiMaxFlowRate": 32.6, "defaultAspirateFlowRate": { - "default": 35, - "valuesByApiLevel": { "2.14": 35 } + "default": 32.6, + "valuesByApiLevel": { "2.14": 32.6 } }, "defaultDispenseFlowRate": { - "default": 57, - "valuesByApiLevel": { "2.14": 57 } + "default": 32.6, + "valuesByApiLevel": { "2.14": 32.6 } }, "defaultBlowOutFlowRate": { - "default": 57, - "valuesByApiLevel": { "2.14": 57 } + "default": 32.6, + "valuesByApiLevel": { "2.14": 32.6 } }, "defaultFlowAcceleration": 1200.0, "defaultTipLength": 57.9, diff --git a/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/3_3.json b/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/3_3.json index 0f3f56f6494..899d08aeaee 100644 --- a/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/3_3.json +++ b/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/3_3.json @@ -2,6 +2,7 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json", "supportedTips": { "t50": { + "uiMaxFlowRate": 189.1, "defaultAspirateFlowRate": { "default": 6, "valuesByApiLevel": { "2.14": 6 } @@ -56,6 +57,7 @@ "defaultPushOutVolume": 7 }, "t200": { + "uiMaxFlowRate": 185.1, "defaultAspirateFlowRate": { "default": 80, "valuesByApiLevel": { "2.14": 80 } @@ -108,6 +110,7 @@ "defaultPushOutVolume": 5 }, "t1000": { + "uiMaxFlowRate": 184.8, "defaultAspirateFlowRate": { "default": 160, "valuesByApiLevel": { "2.14": 160 } diff --git a/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/3_4.json b/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/3_4.json index 0f3f56f6494..899d08aeaee 100644 --- a/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/3_4.json +++ b/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/3_4.json @@ -2,6 +2,7 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json", "supportedTips": { "t50": { + "uiMaxFlowRate": 189.1, "defaultAspirateFlowRate": { "default": 6, "valuesByApiLevel": { "2.14": 6 } @@ -56,6 +57,7 @@ "defaultPushOutVolume": 7 }, "t200": { + "uiMaxFlowRate": 185.1, "defaultAspirateFlowRate": { "default": 80, "valuesByApiLevel": { "2.14": 80 } @@ -108,6 +110,7 @@ "defaultPushOutVolume": 5 }, "t1000": { + "uiMaxFlowRate": 184.8, "defaultAspirateFlowRate": { "default": 160, "valuesByApiLevel": { "2.14": 160 } diff --git a/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/3_5.json b/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/3_5.json index 0f3f56f6494..1b9c88edf92 100644 --- a/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/3_5.json +++ b/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/3_5.json @@ -2,6 +2,7 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json", "supportedTips": { "t50": { + "uiMaxFlowRate": 189.1, "defaultAspirateFlowRate": { "default": 6, "valuesByApiLevel": { "2.14": 6 } @@ -56,6 +57,7 @@ "defaultPushOutVolume": 7 }, "t200": { + "uiMaxFlowRate": 185.1, "defaultAspirateFlowRate": { "default": 80, "valuesByApiLevel": { "2.14": 80 } @@ -108,6 +110,7 @@ "defaultPushOutVolume": 5 }, "t1000": { + "uiMaxFlowRate": 185.1, "defaultAspirateFlowRate": { "default": 160, "valuesByApiLevel": { "2.14": 160 } diff --git a/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/3_6.json b/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/3_6.json index cac57c41844..cd27abd81f0 100644 --- a/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/3_6.json +++ b/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/3_6.json @@ -2,6 +2,7 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json", "supportedTips": { "t50": { + "uiMaxFlowRate": 194, "defaultAspirateFlowRate": { "default": 6, "valuesByApiLevel": { "2.14": 6 } @@ -56,6 +57,7 @@ "defaultPushOutVolume": 7 }, "t200": { + "uiMaxFlowRate": 194, "defaultAspirateFlowRate": { "default": 80, "valuesByApiLevel": { "2.14": 80 } @@ -108,6 +110,7 @@ "defaultPushOutVolume": 5 }, "t1000": { + "uiMaxFlowRate": 187.2, "defaultAspirateFlowRate": { "default": 160, "valuesByApiLevel": { "2.14": 160 } diff --git a/shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/2_1.json b/shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/2_1.json index 9a281ac618c..46563177001 100644 --- a/shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/2_1.json +++ b/shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/2_1.json @@ -2,6 +2,7 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json", "supportedTips": { "t1000": { + "uiMaxFlowRate": 1018.6, "defaultAspirateFlowRate": { "default": 274.7, "valuesByApiLevel": { "2.0": 137.35, "2.6": 274.7 } diff --git a/shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/2_2.json b/shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/2_2.json index bc51e5751d9..bfb9c6e83e8 100644 --- a/shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/2_2.json +++ b/shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/2_2.json @@ -2,6 +2,7 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json", "supportedTips": { "t1000": { + "uiMaxFlowRate": 1020.7, "defaultAspirateFlowRate": { "default": 274.7, "valuesByApiLevel": { "2.0": 137.35, "2.6": 274.7 } diff --git a/shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/3_3.json b/shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/3_3.json index 476cb96cc69..e4e765c999c 100644 --- a/shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/3_3.json +++ b/shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/3_3.json @@ -2,6 +2,7 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json", "supportedTips": { "t50": { + "uiMaxFlowRate": 808.3, "defaultAspirateFlowRate": { "default": 6, "valuesByApiLevel": { "2.14": 6 } @@ -116,6 +117,7 @@ "defaultPushOutVolume": 7 }, "t200": { + "uiMaxFlowRate": 905.7, "defaultAspirateFlowRate": { "default": 80, "valuesByApiLevel": { "2.14": 80 } @@ -228,6 +230,7 @@ "defaultPushOutVolume": 5 }, "t1000": { + "uiMaxFlowRate": 787.7, "defaultAspirateFlowRate": { "default": 160, "valuesByApiLevel": { "2.14": 160 } diff --git a/shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/3_4.json b/shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/3_4.json index 28226b82e4d..f48e41f37f2 100644 --- a/shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/3_4.json +++ b/shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/3_4.json @@ -2,6 +2,7 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json", "supportedTips": { "t50": { + "uiMaxFlowRate": 762.1, "defaultAspirateFlowRate": { "default": 478, "valuesByApiLevel": { "2.14": 478 } @@ -90,6 +91,7 @@ "defaultPushOutVolume": 7 }, "t200": { + "uiMaxFlowRate": 745, "defaultAspirateFlowRate": { "default": 716, "valuesByApiLevel": { "2.14": 716 } @@ -178,6 +180,7 @@ "defaultPushOutVolume": 5 }, "t1000": { + "uiMaxFlowRate": 763.3, "defaultAspirateFlowRate": { "default": 716, "valuesByApiLevel": { "2.14": 716 } diff --git a/shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/3_5.json b/shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/3_5.json index 65456da3a9d..9c939ba9c7c 100644 --- a/shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/3_5.json +++ b/shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/3_5.json @@ -2,6 +2,7 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json", "supportedTips": { "t50": { + "uiMaxFlowRate": 785.2, "defaultAspirateFlowRate": { "default": 478, "valuesByApiLevel": { "2.14": 478 } @@ -70,6 +71,7 @@ "defaultPushOutVolume": 7 }, "t200": { + "uiMaxFlowRate": 802.5, "defaultAspirateFlowRate": { "default": 716, "valuesByApiLevel": { "2.14": 716 } @@ -128,6 +130,7 @@ "defaultPushOutVolume": 5 }, "t1000": { + "uiMaxFlowRate": 753.5, "defaultAspirateFlowRate": { "default": 716, "valuesByApiLevel": { "2.14": 716 } diff --git a/shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/3_6.json b/shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/3_6.json index 29caae1b15b..72187981c26 100644 --- a/shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/3_6.json +++ b/shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/3_6.json @@ -2,6 +2,7 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json", "supportedTips": { "t50": { + "uiMaxFlowRate": 785.2, "defaultAspirateFlowRate": { "default": 478, "valuesByApiLevel": { "2.14": 478 } @@ -70,6 +71,7 @@ "defaultPushOutVolume": 7 }, "t200": { + "uiMaxFlowRate": 802.5, "defaultAspirateFlowRate": { "default": 716, "valuesByApiLevel": { "2.14": 716 } @@ -128,6 +130,7 @@ "defaultPushOutVolume": 5 }, "t1000": { + "uiMaxFlowRate": 727.3, "defaultAspirateFlowRate": { "default": 716, "valuesByApiLevel": { "2.14": 716 } diff --git a/shared-data/pipette/definitions/2/liquid/single_channel/p20/default/2_1.json b/shared-data/pipette/definitions/2/liquid/single_channel/p20/default/2_1.json index 8acb156a2af..e10b9ba735d 100644 --- a/shared-data/pipette/definitions/2/liquid/single_channel/p20/default/2_1.json +++ b/shared-data/pipette/definitions/2/liquid/single_channel/p20/default/2_1.json @@ -2,6 +2,7 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json", "supportedTips": { "t20": { + "uiMaxFlowRate": 25.3, "defaultAspirateFlowRate": { "default": 3.78, "valuesByApiLevel": { "2.0": 3.78, "2.6": 7.56 } @@ -146,6 +147,7 @@ "defaultPushOutVolume": 0 }, "t10": { + "uiMaxFlowRate": 25.3, "defaultAspirateFlowRate": { "default": 3.78, "valuesByApiLevel": { "2.0": 3.78, "2.6": 7.56 } diff --git a/shared-data/pipette/definitions/2/liquid/single_channel/p20/default/2_2.json b/shared-data/pipette/definitions/2/liquid/single_channel/p20/default/2_2.json index 907d4546520..eba2ed05cb1 100644 --- a/shared-data/pipette/definitions/2/liquid/single_channel/p20/default/2_2.json +++ b/shared-data/pipette/definitions/2/liquid/single_channel/p20/default/2_2.json @@ -2,6 +2,7 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json", "supportedTips": { "t20": { + "uiMaxFlowRate": 25, "defaultAspirateFlowRate": { "default": 3.78, "valuesByApiLevel": { "2.0": 3.78, "2.6": 7.56 } @@ -146,6 +147,7 @@ "defaultPushOutVolume": 0 }, "t10": { + "uiMaxFlowRate": 25, "defaultAspirateFlowRate": { "default": 3.78, "valuesByApiLevel": { "2.0": 3.78, "2.6": 7.56 } diff --git a/shared-data/pipette/definitions/2/liquid/single_channel/p300/default/2_1.json b/shared-data/pipette/definitions/2/liquid/single_channel/p300/default/2_1.json index 14b514edf8d..0478ea9c0e5 100644 --- a/shared-data/pipette/definitions/2/liquid/single_channel/p300/default/2_1.json +++ b/shared-data/pipette/definitions/2/liquid/single_channel/p300/default/2_1.json @@ -2,6 +2,7 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json", "supportedTips": { "t200": { + "uiMaxFlowRate": 329.3, "defaultAspirateFlowRate": { "default": 46.43, "valuesByApiLevel": { "2.0": 46.43, "2.6": 92.86 } @@ -88,6 +89,7 @@ "defaultPushOutVolume": 0 }, "t300": { + "uiMaxFlowRate": 329.3, "defaultAspirateFlowRate": { "default": 46.43, "valuesByApiLevel": { "2.0": 46.43, "2.6": 92.86 } diff --git a/shared-data/pipette/definitions/2/liquid/single_channel/p50/default/3_3.json b/shared-data/pipette/definitions/2/liquid/single_channel/p50/default/3_3.json index f5492d8809a..a5d87c485ba 100644 --- a/shared-data/pipette/definitions/2/liquid/single_channel/p50/default/3_3.json +++ b/shared-data/pipette/definitions/2/liquid/single_channel/p50/default/3_3.json @@ -2,6 +2,7 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json", "supportedTips": { "t50": { + "uiMaxFlowRate": 46.8, "defaultAspirateFlowRate": { "default": 8, "valuesByApiLevel": { "2.14": 8 } diff --git a/shared-data/pipette/definitions/2/liquid/single_channel/p50/default/3_4.json b/shared-data/pipette/definitions/2/liquid/single_channel/p50/default/3_4.json index df9fc3d784b..464eb213798 100644 --- a/shared-data/pipette/definitions/2/liquid/single_channel/p50/default/3_4.json +++ b/shared-data/pipette/definitions/2/liquid/single_channel/p50/default/3_4.json @@ -2,6 +2,7 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json", "supportedTips": { "t50": { + "uiMaxFlowRate": 46.3, "defaultAspirateFlowRate": { "default": 35, "valuesByApiLevel": { "2.14": 35 } diff --git a/shared-data/pipette/definitions/2/liquid/single_channel/p50/default/3_5.json b/shared-data/pipette/definitions/2/liquid/single_channel/p50/default/3_5.json index c798ce421a6..2fca659b070 100644 --- a/shared-data/pipette/definitions/2/liquid/single_channel/p50/default/3_5.json +++ b/shared-data/pipette/definitions/2/liquid/single_channel/p50/default/3_5.json @@ -2,6 +2,7 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json", "supportedTips": { "t50": { + "uiMaxFlowRate": 47, "defaultAspirateFlowRate": { "default": 35, "valuesByApiLevel": { "2.14": 35 } diff --git a/shared-data/pipette/definitions/2/liquid/single_channel/p50/lowVolumeDefault/3_3.json b/shared-data/pipette/definitions/2/liquid/single_channel/p50/lowVolumeDefault/3_3.json index 2a292477578..deae3998fe9 100644 --- a/shared-data/pipette/definitions/2/liquid/single_channel/p50/lowVolumeDefault/3_3.json +++ b/shared-data/pipette/definitions/2/liquid/single_channel/p50/lowVolumeDefault/3_3.json @@ -2,6 +2,7 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json", "supportedTips": { "t50": { + "uiMaxFlowRate": 31.8, "defaultAspirateFlowRate": { "default": 8, "valuesByApiLevel": { "2.14": 8 } diff --git a/shared-data/pipette/definitions/2/liquid/single_channel/p50/lowVolumeDefault/3_4.json b/shared-data/pipette/definitions/2/liquid/single_channel/p50/lowVolumeDefault/3_4.json index 771ff88cf22..397dc63b230 100644 --- a/shared-data/pipette/definitions/2/liquid/single_channel/p50/lowVolumeDefault/3_4.json +++ b/shared-data/pipette/definitions/2/liquid/single_channel/p50/lowVolumeDefault/3_4.json @@ -2,17 +2,18 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json", "supportedTips": { "t50": { + "uiMaxFlowRate": 31.8, "defaultAspirateFlowRate": { - "default": 35, - "valuesByApiLevel": { "2.14": 35 } + "default": 31.8, + "valuesByApiLevel": { "2.14": 31.8 } }, "defaultDispenseFlowRate": { - "default": 57, - "valuesByApiLevel": { "2.14": 57 } + "default": 31.8, + "valuesByApiLevel": { "2.14": 31.8 } }, "defaultBlowOutFlowRate": { - "default": 57, - "valuesByApiLevel": { "2.14": 57 } + "default": 31.8, + "valuesByApiLevel": { "2.14": 31.8 } }, "defaultFlowAcceleration": 1200.0, "defaultTipLength": 57.9, diff --git a/shared-data/pipette/definitions/2/liquid/single_channel/p50/lowVolumeDefault/3_5.json b/shared-data/pipette/definitions/2/liquid/single_channel/p50/lowVolumeDefault/3_5.json index 644d93354e8..e1b92133bd6 100644 --- a/shared-data/pipette/definitions/2/liquid/single_channel/p50/lowVolumeDefault/3_5.json +++ b/shared-data/pipette/definitions/2/liquid/single_channel/p50/lowVolumeDefault/3_5.json @@ -2,17 +2,18 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json", "supportedTips": { "t50": { + "uiMaxFlowRate": 26.7, "defaultAspirateFlowRate": { - "default": 35, - "valuesByApiLevel": { "2.14": 35 } + "default": 26.7, + "valuesByApiLevel": { "2.14": 26.7 } }, "defaultDispenseFlowRate": { - "default": 57, - "valuesByApiLevel": { "2.14": 57 } + "default": 26.7, + "valuesByApiLevel": { "2.14": 26.7 } }, "defaultBlowOutFlowRate": { - "default": 57, - "valuesByApiLevel": { "2.14": 57 } + "default": 26.7, + "valuesByApiLevel": { "2.14": 26.7 } }, "defaultFlowAcceleration": 1200.0, "defaultTipLength": 57.9, diff --git a/shared-data/pipette/schemas/2/pipetteLiquidPropertiesSchema.json b/shared-data/pipette/schemas/2/pipetteLiquidPropertiesSchema.json index f7a76e0cde0..a4ba8e659f1 100644 --- a/shared-data/pipette/schemas/2/pipetteLiquidPropertiesSchema.json +++ b/shared-data/pipette/schemas/2/pipetteLiquidPropertiesSchema.json @@ -68,13 +68,16 @@ ], "properties": { "defaultAspirateFlowRate": { - "$ref": "#/definitions/flowRate" + "$ref": "#/definitions/flowRate", + "$comment": "for lowVolumeDefault only, the flowRate matches the uiMaxFlowRate. This does not change physical behavior and was made for UI purposes" }, "defaultDispenseFlowRate": { - "$ref": "#/definitions/flowRate" + "$ref": "#/definitions/flowRate", + "$comment": "for lowVolumeDefault only, the flowRate matches the uiMaxFlowRate. This does not change physical behavior and was made for UI purposes" }, "defaultBlowOutFlowRate": { - "$ref": "#/definitions/flowRate" + "$ref": "#/definitions/flowRate", + "$comment": "for lowVolumeDefault only, the flowRate matches the uiMaxFlowRate. This does not change physical behavior and was made for UI purposes" }, "defaultFlowAcceleration": { "$ref": "#/definitions/positiveNumber" @@ -92,6 +95,11 @@ "dispense": { "type": "array", "items": { "$ref": "#/definitions/liquidHandlingSpecs" } + }, + "uiMaxFlowRate": { + "$ref": "#/definitions/positiveNumber", + "$comment": "To be used in frontend applications only since it is the limit-to-lowest-max", + "description": "An optional number to represent each pipette's supported tip max flow rate for aspirate and dispense. The limit is the lowest max flow rate given all the tip's volumes minus 2% for safety." } } } diff --git a/shared-data/python/opentrons_shared_data/pipette/pipette_definition.py b/shared-data/python/opentrons_shared_data/pipette/pipette_definition.py index d7f3435ec73..a7b43663884 100644 --- a/shared-data/python/opentrons_shared_data/pipette/pipette_definition.py +++ b/shared-data/python/opentrons_shared_data/pipette/pipette_definition.py @@ -72,17 +72,17 @@ class SupportedTipsDefinition(BaseModel): default_aspirate_flowrate: FlowRateDefinition = Field( ..., - description="The flowrate used in aspirations by default.", + description="The flowrate used in aspirations by default. For lowVolumeDefault only, the flowrate matches uiMaxFlowRate for ui purposes, it does not change physical behavior.", alias="defaultAspirateFlowRate", ) default_dispense_flowrate: FlowRateDefinition = Field( ..., - description="The flowrate used in dispenses by default.", + description="The flowrate used in dispenses by default. For lowVolumeDefault only, the flowrate matches uiMaxFlowRate for ui purposes, it does not change physical behavior.", alias="defaultDispenseFlowRate", ) default_blowout_flowrate: FlowRateDefinition = Field( ..., - description="The flowrate used in blowouts by default.", + description="The flowrate used in blowouts by default. For lowVolumeDefault only, the flowrate matches uiMaxFlowRate for ui purposes, it does not change physical behavior.", alias="defaultBlowOutFlowRate", ) default_flow_acceleration: float = Field( @@ -111,6 +111,13 @@ class SupportedTipsDefinition(BaseModel): description="The default volume for a push-out during dispense.", alias="defaultPushOutVolume", ) + ui_max_flow_rate: float = Field( + float( + "inf" + ), # some pipettes (GEN1, unreleased prototype models) don't have a max flow rate + description="The lowest volume max flow rate for a pipette's given supported tip, minus 2 percent for safety.", + alias="uiMaxFlowRate", + ) class MotorConfigurations(BaseModel): diff --git a/api/src/opentrons/hardware_control/instruments/instrument_helpers.py b/shared-data/python/opentrons_shared_data/pipette/ul_per_mm.py similarity index 100% rename from api/src/opentrons/hardware_control/instruments/instrument_helpers.py rename to shared-data/python/opentrons_shared_data/pipette/ul_per_mm.py diff --git a/shared-data/python/tests/pipette/test_max_flow_rates_per_volume.py b/shared-data/python/tests/pipette/test_max_flow_rates_per_volume.py new file mode 100644 index 00000000000..ff731ec0e3c --- /dev/null +++ b/shared-data/python/tests/pipette/test_max_flow_rates_per_volume.py @@ -0,0 +1,88 @@ +import os +import pytest +from typing import Iterator +from opentrons_shared_data import get_shared_data_root +from opentrons_shared_data.pipette.pipette_load_name_conversions import ( + convert_pipette_model, +) +from opentrons_shared_data.pipette.load_data import load_definition +from opentrons_shared_data.pipette.ul_per_mm import piecewise_volume_conversion + +from opentrons_shared_data.pipette.dev_types import PipetteModel +from opentrons_shared_data.pipette.pipette_definition import ( + ulPerMMDefinition, +) + + +DEFAULT_MAX_SPEED_HIGH_THROUGHPUT_OT3_AXIS_KIND_P = 15 +DEFAULT_MAX_SPEED_LOW_THROUGHPUT_OT3_AXIS_KIND_P = 70 +B_MAX_SPEED = 40 + + +def _get_plunger_max_speed(pipette_model: PipetteModel) -> float: + if "v2" in pipette_model: + return B_MAX_SPEED + else: + if "96" in pipette_model: + return DEFAULT_MAX_SPEED_HIGH_THROUGHPUT_OT3_AXIS_KIND_P + else: + return DEFAULT_MAX_SPEED_LOW_THROUGHPUT_OT3_AXIS_KIND_P + + +def _get_max_flow_rate_at_volume( + ul_per_mm_definition: ulPerMMDefinition, + pipette_model: PipetteModel, + volume: float, +) -> float: + max_speed = _get_plunger_max_speed(pipette_model) + map = list(ul_per_mm_definition.default.values())[-1] + ul_per_mm = piecewise_volume_conversion(volume, map) + return round(ul_per_mm * max_speed, 1) + + +def get_all_pipette_models() -> Iterator[PipetteModel]: + paths_to_validate = ( + get_shared_data_root() / "pipette" / "definitions" / "2" / "liquid" + ) + + _channel_model_str = { + "single_channel": "single", + "ninety_six_channel": "96", + "eight_channel": "multi", + } + assert os.listdir(paths_to_validate), "You have a path wrong" + for channel_dir in os.listdir(paths_to_validate): + for model_dir in os.listdir(paths_to_validate / channel_dir): + for liquid_file in os.listdir(paths_to_validate / channel_dir / model_dir): + for version_file in os.listdir( + paths_to_validate / channel_dir / model_dir / liquid_file + ): + version_list = version_file.split(".json")[0].split("_") + built_model: PipetteModel = PipetteModel( + f"{model_dir}_{_channel_model_str[channel_dir]}_v{version_list[0]}.{version_list[1]}" + ) + if version_list[0] != "1" and version_list[1] != "0": + yield built_model + + +@pytest.mark.parametrize("pipette", list(get_all_pipette_models())) +@pytest.mark.parametrize("action", ["aspirate", "dispense"]) +def test_max_flow_rates_per_volume(pipette: PipetteModel, action: str) -> None: + """Verify the max flow rate values for each pipette's supported tip is in range""" + pipette_model_version = convert_pipette_model(pipette) + definition = load_definition( + pipette_model_version.pipette_type, + pipette_model_version.pipette_channels, + pipette_model_version.pipette_version, + ) + for liquid_name, liquid_properties in definition.liquid_properties.items(): + for ( + tip_type, + supported_tip, + ) in liquid_properties.supported_tips.items(): + assert supported_tip.ui_max_flow_rate < _get_max_flow_rate_at_volume( + supported_tip.aspirate, pipette, liquid_properties.min_volume + ) + assert supported_tip.ui_max_flow_rate < _get_max_flow_rate_at_volume( + supported_tip.dispense, pipette, liquid_properties.min_volume + ) From 0940e7cca79126970b2fbfd7f5cbe62eabe3426d Mon Sep 17 00:00:00 2001 From: Derek Maggio Date: Wed, 17 Apr 2024 11:55:05 -0700 Subject: [PATCH 154/194] chore(api): handle performance-metrics package not existing (#14922) # Overview Closes https://opentrons.atlassian.net/browse/EXEC-407 We do not want performance-metrics to be a required dependency for the publicly available opentrons package on PyPI. But we want to utilize performance-metrics when running the opentrons package on the robot itself. The issue is, that since the performance-metrics package uses decorators to track running code, the decorators will always need to exist. This PR handles the case where the performance-metrics package does not exist and injects stubs where necessary. # Changelog - Created the `SupportsTracking` mypy Protocol to define an interface both `RobotContextTracker` and the stubbed class, `StubbedTracker` implement - Added performance-metrics as a dev dependency so tests using performance-metrics can still run - Created `performance_helpers.py` - Contains `StubbedTracker` defininition - Handles loading `StubberTracker` if performance-metrics library fails to load - Provides `_get_robot_context_tracker` private singleton function for eventual public-facing functions to use. # Test Plan - Testing to ensure stubbed `track` decorator returns the decorated function unchanged - Validate singleton logic of _get_robot_context_tracker # Risk assessment Low, still not actually being used anywhere --------- Co-authored-by: Jethary Rader <66035149+jerader@users.noreply.github.com> Co-authored-by: Jamey Huffnagle --- api/Pipfile | 1 + api/Pipfile.lock | 1027 ++++++++++------- api/src/opentrons/cli/analyze.py | 2 + api/src/opentrons/config/__init__.py | 11 + api/src/opentrons/util/performance_helpers.py | 76 ++ api/tests/opentrons/cli/test_cli.py | 34 +- .../util/test_performance_helpers.py | 28 + .../src/performance_metrics/__init__.py | 4 + .../src/performance_metrics/datashapes.py | 34 +- .../robot_context_tracker.py | 16 +- .../test_robot_context_tracker.py | 9 +- robot-server/Pipfile | 1 + robot-server/Pipfile.lock | 587 +++++----- .../performance/dev_types.py | 56 + 14 files changed, 1176 insertions(+), 710 deletions(-) create mode 100644 api/src/opentrons/util/performance_helpers.py create mode 100644 api/tests/opentrons/util/test_performance_helpers.py create mode 100644 shared-data/python/opentrons_shared_data/performance/dev_types.py diff --git a/api/Pipfile b/api/Pipfile index 710a5cb6f22..7be11b82934 100755 --- a/api/Pipfile +++ b/api/Pipfile @@ -48,3 +48,4 @@ pytest-profiling = "~=1.7.0" # TODO(mc, 2022-03-31): upgrade sphinx, remove this subdep pin jinja2 = ">=2.3,<3.1" hypothesis = "==6.96.1" +performance-metrics = {file = "../performance-metrics", editable = true} diff --git a/api/Pipfile.lock b/api/Pipfile.lock index cc9f3163e51..94643ce22a7 100644 --- a/api/Pipfile.lock +++ b/api/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "f0d4979ecb4f125cef848e0ce31e3a5e9cded69abaf773ad90d00016f6d2a65d" + "sha256": "a531665bfd7452ea19565ee95137118966532a8ab5475b7d5ee086ada333e627" }, "pipfile-spec": 6, "requires": {}, @@ -56,11 +56,11 @@ }, "idna": { "hashes": [ - "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", - "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f" + "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc", + "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0" ], "markers": "python_version >= '3.5'", - "version": "==3.6" + "version": "==3.7" }, "jsonschema": { "hashes": [ @@ -73,65 +73,65 @@ }, "msgpack": { "hashes": [ - "sha256:04ad6069c86e531682f9e1e71b71c1c3937d6014a7c3e9edd2aa81ad58842862", - "sha256:0bfdd914e55e0d2c9e1526de210f6fe8ffe9705f2b1dfcc4aecc92a4cb4b533d", - "sha256:1dc93e8e4653bdb5910aed79f11e165c85732067614f180f70534f056da97db3", - "sha256:1e2d69948e4132813b8d1131f29f9101bc2c915f26089a6d632001a5c1349672", - "sha256:235a31ec7db685f5c82233bddf9858748b89b8119bf4538d514536c485c15fe0", - "sha256:27dcd6f46a21c18fa5e5deed92a43d4554e3df8d8ca5a47bf0615d6a5f39dbc9", - "sha256:28efb066cde83c479dfe5a48141a53bc7e5f13f785b92ddde336c716663039ee", - "sha256:3476fae43db72bd11f29a5147ae2f3cb22e2f1a91d575ef130d2bf49afd21c46", - "sha256:36e17c4592231a7dbd2ed09027823ab295d2791b3b1efb2aee874b10548b7524", - "sha256:384d779f0d6f1b110eae74cb0659d9aa6ff35aaf547b3955abf2ab4c901c4819", - "sha256:38949d30b11ae5f95c3c91917ee7a6b239f5ec276f271f28638dec9156f82cfc", - "sha256:3967e4ad1aa9da62fd53e346ed17d7b2e922cba5ab93bdd46febcac39be636fc", - "sha256:3e7bf4442b310ff154b7bb9d81eb2c016b7d597e364f97d72b1acc3817a0fdc1", - "sha256:3f0c8c6dfa6605ab8ff0611995ee30d4f9fcff89966cf562733b4008a3d60d82", - "sha256:484ae3240666ad34cfa31eea7b8c6cd2f1fdaae21d73ce2974211df099a95d81", - "sha256:4a7b4f35de6a304b5533c238bee86b670b75b03d31b7797929caa7a624b5dda6", - "sha256:4cb14ce54d9b857be9591ac364cb08dc2d6a5c4318c1182cb1d02274029d590d", - "sha256:4e71bc4416de195d6e9b4ee93ad3f2f6b2ce11d042b4d7a7ee00bbe0358bd0c2", - "sha256:52700dc63a4676669b341ba33520f4d6e43d3ca58d422e22ba66d1736b0a6e4c", - "sha256:572efc93db7a4d27e404501975ca6d2d9775705c2d922390d878fcf768d92c87", - "sha256:576eb384292b139821c41995523654ad82d1916da6a60cff129c715a6223ea84", - "sha256:5b0bf0effb196ed76b7ad883848143427a73c355ae8e569fa538365064188b8e", - "sha256:5b6ccc0c85916998d788b295765ea0e9cb9aac7e4a8ed71d12e7d8ac31c23c95", - "sha256:5ed82f5a7af3697b1c4786053736f24a0efd0a1b8a130d4c7bfee4b9ded0f08f", - "sha256:6d4c80667de2e36970ebf74f42d1088cc9ee7ef5f4e8c35eee1b40eafd33ca5b", - "sha256:730076207cb816138cf1af7f7237b208340a2c5e749707457d70705715c93b93", - "sha256:7687e22a31e976a0e7fc99c2f4d11ca45eff652a81eb8c8085e9609298916dcf", - "sha256:822ea70dc4018c7e6223f13affd1c5c30c0f5c12ac1f96cd8e9949acddb48a61", - "sha256:84b0daf226913133f899ea9b30618722d45feffa67e4fe867b0b5ae83a34060c", - "sha256:85765fdf4b27eb5086f05ac0491090fc76f4f2b28e09d9350c31aac25a5aaff8", - "sha256:8dd178c4c80706546702c59529ffc005681bd6dc2ea234c450661b205445a34d", - "sha256:8f5b234f567cf76ee489502ceb7165c2a5cecec081db2b37e35332b537f8157c", - "sha256:98bbd754a422a0b123c66a4c341de0474cad4a5c10c164ceed6ea090f3563db4", - "sha256:993584fc821c58d5993521bfdcd31a4adf025c7d745bbd4d12ccfecf695af5ba", - "sha256:a40821a89dc373d6427e2b44b572efc36a2778d3f543299e2f24eb1a5de65415", - "sha256:b291f0ee7961a597cbbcc77709374087fa2a9afe7bdb6a40dbbd9b127e79afee", - "sha256:b573a43ef7c368ba4ea06050a957c2a7550f729c31f11dd616d2ac4aba99888d", - "sha256:b610ff0f24e9f11c9ae653c67ff8cc03c075131401b3e5ef4b82570d1728f8a9", - "sha256:bdf38ba2d393c7911ae989c3bbba510ebbcdf4ecbdbfec36272abe350c454075", - "sha256:bfef2bb6ef068827bbd021017a107194956918ab43ce4d6dc945ffa13efbc25f", - "sha256:cab3db8bab4b7e635c1c97270d7a4b2a90c070b33cbc00c99ef3f9be03d3e1f7", - "sha256:cb70766519500281815dfd7a87d3a178acf7ce95390544b8c90587d76b227681", - "sha256:cca1b62fe70d761a282496b96a5e51c44c213e410a964bdffe0928e611368329", - "sha256:ccf9a39706b604d884d2cb1e27fe973bc55f2890c52f38df742bc1d79ab9f5e1", - "sha256:dc43f1ec66eb8440567186ae2f8c447d91e0372d793dfe8c222aec857b81a8cf", - "sha256:dd632777ff3beaaf629f1ab4396caf7ba0bdd075d948a69460d13d44357aca4c", - "sha256:e45ae4927759289c30ccba8d9fdce62bb414977ba158286b5ddaf8df2cddb5c5", - "sha256:e50ebce52f41370707f1e21a59514e3375e3edd6e1832f5e5235237db933c98b", - "sha256:ebbbba226f0a108a7366bf4b59bf0f30a12fd5e75100c630267d94d7f0ad20e5", - "sha256:ec79ff6159dffcc30853b2ad612ed572af86c92b5168aa3fc01a67b0fa40665e", - "sha256:f0936e08e0003f66bfd97e74ee530427707297b0d0361247e9b4f59ab78ddc8b", - "sha256:f26a07a6e877c76a88e3cecac8531908d980d3d5067ff69213653649ec0f60ad", - "sha256:f64e376cd20d3f030190e8c32e1c64582eba56ac6dc7d5b0b49a9d44021b52fd", - "sha256:f6ffbc252eb0d229aeb2f9ad051200668fc3a9aaa8994e49f0cb2ffe2b7867e7", - "sha256:f9a7c509542db4eceed3dcf21ee5267ab565a83555c9b88a8109dcecc4709002", - "sha256:ff1d0899f104f3921d94579a5638847f783c9b04f2d5f229392ca77fba5b82fc" + "sha256:00e073efcba9ea99db5acef3959efa45b52bc67b61b00823d2a1a6944bf45982", + "sha256:0726c282d188e204281ebd8de31724b7d749adebc086873a59efb8cf7ae27df3", + "sha256:0ceea77719d45c839fd73abcb190b8390412a890df2f83fb8cf49b2a4b5c2f40", + "sha256:114be227f5213ef8b215c22dde19532f5da9652e56e8ce969bf0a26d7c419fee", + "sha256:13577ec9e247f8741c84d06b9ece5f654920d8365a4b636ce0e44f15e07ec693", + "sha256:1876b0b653a808fcd50123b953af170c535027bf1d053b59790eebb0aeb38950", + "sha256:1ab0bbcd4d1f7b6991ee7c753655b481c50084294218de69365f8f1970d4c151", + "sha256:1cce488457370ffd1f953846f82323cb6b2ad2190987cd4d70b2713e17268d24", + "sha256:26ee97a8261e6e35885c2ecd2fd4a6d38252246f94a2aec23665a4e66d066305", + "sha256:3528807cbbb7f315bb81959d5961855e7ba52aa60a3097151cb21956fbc7502b", + "sha256:374a8e88ddab84b9ada695d255679fb99c53513c0a51778796fcf0944d6c789c", + "sha256:376081f471a2ef24828b83a641a02c575d6103a3ad7fd7dade5486cad10ea659", + "sha256:3923a1778f7e5ef31865893fdca12a8d7dc03a44b33e2a5f3295416314c09f5d", + "sha256:4916727e31c28be8beaf11cf117d6f6f188dcc36daae4e851fee88646f5b6b18", + "sha256:493c5c5e44b06d6c9268ce21b302c9ca055c1fd3484c25ba41d34476c76ee746", + "sha256:505fe3d03856ac7d215dbe005414bc28505d26f0c128906037e66d98c4e95868", + "sha256:5845fdf5e5d5b78a49b826fcdc0eb2e2aa7191980e3d2cfd2a30303a74f212e2", + "sha256:5c330eace3dd100bdb54b5653b966de7f51c26ec4a7d4e87132d9b4f738220ba", + "sha256:5dbf059fb4b7c240c873c1245ee112505be27497e90f7c6591261c7d3c3a8228", + "sha256:5e390971d082dba073c05dbd56322427d3280b7cc8b53484c9377adfbae67dc2", + "sha256:5fbb160554e319f7b22ecf530a80a3ff496d38e8e07ae763b9e82fadfe96f273", + "sha256:64d0fcd436c5683fdd7c907eeae5e2cbb5eb872fafbc03a43609d7941840995c", + "sha256:69284049d07fce531c17404fcba2bb1df472bc2dcdac642ae71a2d079d950653", + "sha256:6a0e76621f6e1f908ae52860bdcb58e1ca85231a9b0545e64509c931dd34275a", + "sha256:73ee792784d48aa338bba28063e19a27e8d989344f34aad14ea6e1b9bd83f596", + "sha256:74398a4cf19de42e1498368c36eed45d9528f5fd0155241e82c4082b7e16cffd", + "sha256:7938111ed1358f536daf311be244f34df7bf3cdedb3ed883787aca97778b28d8", + "sha256:82d92c773fbc6942a7a8b520d22c11cfc8fd83bba86116bfcf962c2f5c2ecdaa", + "sha256:83b5c044f3eff2a6534768ccfd50425939e7a8b5cf9a7261c385de1e20dcfc85", + "sha256:8db8e423192303ed77cff4dce3a4b88dbfaf43979d280181558af5e2c3c71afc", + "sha256:9517004e21664f2b5a5fd6333b0731b9cf0817403a941b393d89a2f1dc2bd836", + "sha256:95c02b0e27e706e48d0e5426d1710ca78e0f0628d6e89d5b5a5b91a5f12274f3", + "sha256:99881222f4a8c2f641f25703963a5cefb076adffd959e0558dc9f803a52d6a58", + "sha256:9ee32dcb8e531adae1f1ca568822e9b3a738369b3b686d1477cbc643c4a9c128", + "sha256:a22e47578b30a3e199ab067a4d43d790249b3c0587d9a771921f86250c8435db", + "sha256:b5505774ea2a73a86ea176e8a9a4a7c8bf5d521050f0f6f8426afe798689243f", + "sha256:bd739c9251d01e0279ce729e37b39d49a08c0420d3fee7f2a4968c0576678f77", + "sha256:d16a786905034e7e34098634b184a7d81f91d4c3d246edc6bd7aefb2fd8ea6ad", + "sha256:d3420522057ebab1728b21ad473aa950026d07cb09da41103f8e597dfbfaeb13", + "sha256:d56fd9f1f1cdc8227d7b7918f55091349741904d9520c65f0139a9755952c9e8", + "sha256:d661dc4785affa9d0edfdd1e59ec056a58b3dbb9f196fa43587f3ddac654ac7b", + "sha256:dfe1f0f0ed5785c187144c46a292b8c34c1295c01da12e10ccddfc16def4448a", + "sha256:e1dd7839443592d00e96db831eddb4111a2a81a46b028f0facd60a09ebbdd543", + "sha256:e2872993e209f7ed04d963e4b4fbae72d034844ec66bc4ca403329db2074377b", + "sha256:e2f879ab92ce502a1e65fce390eab619774dda6a6ff719718069ac94084098ce", + "sha256:e3aa7e51d738e0ec0afbed661261513b38b3014754c9459508399baf14ae0c9d", + "sha256:e532dbd6ddfe13946de050d7474e3f5fb6ec774fbb1a188aaf469b08cf04189a", + "sha256:e6b7842518a63a9f17107eb176320960ec095a8ee3b4420b5f688e24bf50c53c", + "sha256:e75753aeda0ddc4c28dce4c32ba2f6ec30b1b02f6c0b14e547841ba5b24f753f", + "sha256:eadb9f826c138e6cf3c49d6f8de88225a3c0ab181a9b4ba792e006e5292d150e", + "sha256:ed59dd52075f8fc91da6053b12e8c89e37aa043f8986efd89e61fae69dc1b011", + "sha256:ef254a06bcea461e65ff0373d8a0dd1ed3aa004af48839f002a0c994a6f72d04", + "sha256:f3709997b228685fe53e8c433e2df9f0cdb5f4542bd5114ed17ac3c0129b0480", + "sha256:f51bab98d52739c50c56658cc303f190785f9a2cd97b823357e7aeae54c8f68a", + "sha256:f9904e24646570539a8950400602d66d2b2c492b9010ea7e965025cb71d0c86d", + "sha256:f9af38a89b6a5c04b7d18c492c8ccf2aee7048aff1ce8437c4683bb5a1df893d" ], "markers": "platform_system != 'Windows'", - "version": "==1.0.7" + "version": "==1.0.8" }, "numpy": { "hashes": [ @@ -162,6 +162,7 @@ }, "opentrons": { "editable": true, + "markers": "python_version >= '3.8'", "path": "." }, "opentrons-hardware": { @@ -173,15 +174,16 @@ }, "opentrons-shared-data": { "editable": true, + "markers": "python_version >= '3.8'", "path": "../shared-data/python" }, "packaging": { "hashes": [ - "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5", - "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7" + "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5", + "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9" ], "markers": "python_version >= '3.7'", - "version": "==23.2" + "version": "==24.0" }, "pydantic": { "hashes": [ @@ -276,32 +278,31 @@ "sha256:6ad50f4613289f3c4d276b6d2ac8901d776dcb929994cce93f55a69e858c595f", "sha256:7eea9b81b0ff908000a825db024313f622895bd578e8a17433e0474cd7d2da83" ], - "markers": "python_version >= '3.7'", "version": "==4.2.2" }, "setuptools": { "hashes": [ - "sha256:385eb4edd9c9d5c17540511303e39a147ce2fc04bc55289c322b9e5904fe2c05", - "sha256:be1af57fc409f93647f2e8e4573a142ed38724b8cdd389706a867bb4efcf1e78" + "sha256:6c1fccdac05a97e598fb0ae3bbed5904ccb317337a51139dcd51453611bbb987", + "sha256:c636ac361bc47580504644275c9ad802c50415c7522212252c033bd15f301f32" ], "markers": "python_version >= '3.8'", - "version": "==69.0.3" + "version": "==69.5.1" }, "sniffio": { "hashes": [ - "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101", - "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384" + "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", + "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc" ], "markers": "python_version >= '3.7'", - "version": "==1.3.0" + "version": "==1.3.1" }, "typing-extensions": { "hashes": [ - "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783", - "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd" + "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0", + "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a" ], "markers": "python_version >= '3.8'", - "version": "==4.9.0" + "version": "==4.11.0" }, "wrapt": { "hashes": [ @@ -413,6 +414,14 @@ "markers": "python_version >= '3.7'", "version": "==2.14.0" }, + "backports.tarfile": { + "hashes": [ + "sha256:2688f159c21afd56a07b75f01306f9f52c79aebcc5f4a117fb8fbb4445352c75", + "sha256:bcd36290d9684beb524d3fe74f4a2db056824c47746583f090b8e55daf0776e4" + ], + "markers": "python_version < '3.12'", + "version": "==1.0.0" + }, "black": { "hashes": [ "sha256:06f9d8846f2340dfac80ceb20200ea5d1b3f181dd0556b47af4e8e0b24fa0a6b", @@ -445,11 +454,69 @@ }, "certifi": { "hashes": [ - "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1", - "sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474" + "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f", + "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1" ], "markers": "python_version >= '3.6'", - "version": "==2023.11.17" + "version": "==2024.2.2" + }, + "cffi": { + "hashes": [ + "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc", + "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a", + "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417", + "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab", + "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520", + "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36", + "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743", + "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8", + "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed", + "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684", + "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56", + "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324", + "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d", + "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235", + "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e", + "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088", + "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000", + "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7", + "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e", + "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673", + "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c", + "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe", + "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2", + "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098", + "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8", + "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a", + "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0", + "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b", + "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896", + "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e", + "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9", + "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2", + "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b", + "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6", + "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404", + "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f", + "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0", + "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4", + "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc", + "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936", + "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba", + "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872", + "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb", + "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614", + "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1", + "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d", + "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969", + "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b", + "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4", + "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627", + "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956", + "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357" + ], + "markers": "platform_python_implementation != 'PyPy'", + "version": "==1.16.0" }, "charset-normalizer": { "hashes": [ @@ -565,53 +632,53 @@ }, "contourpy": { "hashes": [ - "sha256:0274c1cb63625972c0c007ab14dd9ba9e199c36ae1a231ce45d725cbcbfd10a8", - "sha256:0d7e03c0f9a4f90dc18d4e77e9ef4ec7b7bbb437f7f675be8e530d65ae6ef956", - "sha256:11f8d2554e52f459918f7b8e6aa20ec2a3bce35ce95c1f0ef4ba36fbda306df5", - "sha256:139d8d2e1c1dd52d78682f505e980f592ba53c9f73bd6be102233e358b401063", - "sha256:16a7380e943a6d52472096cb7ad5264ecee36ed60888e2a3d3814991a0107286", - "sha256:171f311cb758de7da13fc53af221ae47a5877be5a0843a9fe150818c51ed276a", - "sha256:18fc2b4ed8e4a8fe849d18dce4bd3c7ea637758c6343a1f2bae1e9bd4c9f4686", - "sha256:1c203f617abc0dde5792beb586f827021069fb6d403d7f4d5c2b543d87edceb9", - "sha256:1c2559d6cffc94890b0529ea7eeecc20d6fadc1539273aa27faf503eb4656d8f", - "sha256:1c88dfb9e0c77612febebb6ac69d44a8d81e3dc60f993215425b62c1161353f4", - "sha256:1e9dc350fb4c58adc64df3e0703ab076f60aac06e67d48b3848c23647ae4310e", - "sha256:247b9d16535acaa766d03037d8e8fb20866d054d3c7fbf6fd1f993f11fc60ca0", - "sha256:266270c6f6608340f6c9836a0fb9b367be61dde0c9a9a18d5ece97774105ff3e", - "sha256:34b9071c040d6fe45d9826cbbe3727d20d83f1b6110d219b83eb0e2a01d79488", - "sha256:3d7d1f8871998cdff5d2ff6a087e5e1780139abe2838e85b0b46b7ae6cc25399", - "sha256:461e3ae84cd90b30f8d533f07d87c00379644205b1d33a5ea03381edc4b69431", - "sha256:464b423bc2a009088f19bdf1f232299e8b6917963e2b7e1d277da5041f33a779", - "sha256:491b1917afdd8638a05b611a56d46587d5a632cabead889a5440f7c638bc6ed9", - "sha256:4a1b1208102be6e851f20066bf0e7a96b7d48a07c9b0cfe6d0d4545c2f6cadab", - "sha256:575bcaf957a25d1194903a10bc9f316c136c19f24e0985a2b9b5608bdf5dbfe0", - "sha256:5c6b28956b7b232ae801406e529ad7b350d3f09a4fde958dfdf3c0520cdde0dd", - "sha256:5d16edfc3fc09968e09ddffada434b3bf989bf4911535e04eada58469873e28e", - "sha256:5fd1810973a375ca0e097dee059c407913ba35723b111df75671a1976efa04bc", - "sha256:67b7f17679fa62ec82b7e3e611c43a016b887bd64fb933b3ae8638583006c6d6", - "sha256:68ce4788b7d93e47f84edd3f1f95acdcd142ae60bc0e5493bfd120683d2d4316", - "sha256:6d3364b999c62f539cd403f8123ae426da946e142312a514162adb2addd8d808", - "sha256:6e739530c662a8d6d42c37c2ed52a6f0932c2d4a3e8c1f90692ad0ce1274abe0", - "sha256:6fdd887f17c2f4572ce548461e4f96396681212d858cae7bd52ba3310bc6f00f", - "sha256:78e6ad33cf2e2e80c5dfaaa0beec3d61face0fb650557100ee36db808bfa6843", - "sha256:884c3f9d42d7218304bc74a8a7693d172685c84bd7ab2bab1ee567b769696df9", - "sha256:8d8faf05be5ec8e02a4d86f616fc2a0322ff4a4ce26c0f09d9f7fb5330a35c95", - "sha256:999c71939aad2780f003979b25ac5b8f2df651dac7b38fb8ce6c46ba5abe6ae9", - "sha256:99ad97258985328b4f207a5e777c1b44a83bfe7cf1f87b99f9c11d4ee477c4de", - "sha256:9e6c93b5b2dbcedad20a2f18ec22cae47da0d705d454308063421a3b290d9ea4", - "sha256:ab459a1cbbf18e8698399c595a01f6dcc5c138220ca3ea9e7e6126232d102bb4", - "sha256:b69303ceb2e4d4f146bf82fda78891ef7bcd80c41bf16bfca3d0d7eb545448aa", - "sha256:b7caf9b241464c404613512d5594a6e2ff0cc9cb5615c9475cc1d9b514218ae8", - "sha256:b95a225d4948b26a28c08307a60ac00fb8671b14f2047fc5476613252a129776", - "sha256:bd2f1ae63998da104f16a8b788f685e55d65760cd1929518fd94cd682bf03e41", - "sha256:be16975d94c320432657ad2402f6760990cb640c161ae6da1363051805fa8108", - "sha256:ce96dd400486e80ac7d195b2d800b03e3e6a787e2a522bfb83755938465a819e", - "sha256:dbd50d0a0539ae2e96e537553aff6d02c10ed165ef40c65b0e27e744a0f10af8", - "sha256:dd10c26b4eadae44783c45ad6655220426f971c61d9b239e6f7b16d5cdaaa727", - "sha256:ebeac59e9e1eb4b84940d076d9f9a6cec0064e241818bcb6e32124cc5c3e377a" + "sha256:00e5388f71c1a0610e6fe56b5c44ab7ba14165cdd6d695429c5cd94021e390b2", + "sha256:10a37ae557aabf2509c79715cd20b62e4c7c28b8cd62dd7d99e5ed3ce28c3fd9", + "sha256:11959f0ce4a6f7b76ec578576a0b61a28bdc0696194b6347ba3f1c53827178b9", + "sha256:187fa1d4c6acc06adb0fae5544c59898ad781409e61a926ac7e84b8f276dcef4", + "sha256:1a07fc092a4088ee952ddae19a2b2a85757b923217b7eed584fdf25f53a6e7ce", + "sha256:1cac0a8f71a041aa587410424ad46dfa6a11f6149ceb219ce7dd48f6b02b87a7", + "sha256:1d59e739ab0e3520e62a26c60707cc3ab0365d2f8fecea74bfe4de72dc56388f", + "sha256:2855c8b0b55958265e8b5888d6a615ba02883b225f2227461aa9127c578a4922", + "sha256:2e785e0f2ef0d567099b9ff92cbfb958d71c2d5b9259981cd9bee81bd194c9a4", + "sha256:309be79c0a354afff9ff7da4aaed7c3257e77edf6c1b448a779329431ee79d7e", + "sha256:39f3ecaf76cd98e802f094e0d4fbc6dc9c45a8d0c4d185f0f6c2234e14e5f75b", + "sha256:457499c79fa84593f22454bbd27670227874cd2ff5d6c84e60575c8b50a69619", + "sha256:49e70d111fee47284d9dd867c9bb9a7058a3c617274900780c43e38d90fe1205", + "sha256:4c75507d0a55378240f781599c30e7776674dbaf883a46d1c90f37e563453480", + "sha256:4c863140fafc615c14a4bf4efd0f4425c02230eb8ef02784c9a156461e62c965", + "sha256:4d8908b3bee1c889e547867ca4cdc54e5ab6be6d3e078556814a22457f49423c", + "sha256:5b9eb0ca724a241683c9685a484da9d35c872fd42756574a7cfbf58af26677fd", + "sha256:6022cecf8f44e36af10bd9118ca71f371078b4c168b6e0fab43d4a889985dbb5", + "sha256:6150ffa5c767bc6332df27157d95442c379b7dce3a38dff89c0f39b63275696f", + "sha256:62828cada4a2b850dbef89c81f5a33741898b305db244904de418cc957ff05dc", + "sha256:7b4182299f251060996af5249c286bae9361fa8c6a9cda5efc29fe8bfd6062ec", + "sha256:94b34f32646ca0414237168d68a9157cb3889f06b096612afdd296003fdd32fd", + "sha256:9ce6889abac9a42afd07a562c2d6d4b2b7134f83f18571d859b25624a331c90b", + "sha256:9cffe0f850e89d7c0012a1fb8730f75edd4320a0a731ed0c183904fe6ecfc3a9", + "sha256:a12a813949e5066148712a0626895c26b2578874e4cc63160bb007e6df3436fe", + "sha256:a1eea9aecf761c661d096d39ed9026574de8adb2ae1c5bd7b33558af884fb2ce", + "sha256:a31f94983fecbac95e58388210427d68cd30fe8a36927980fab9c20062645609", + "sha256:ac58bdee53cbeba2ecad824fa8159493f0bf3b8ea4e93feb06c9a465d6c87da8", + "sha256:af3f4485884750dddd9c25cb7e3915d83c2db92488b38ccb77dd594eac84c4a0", + "sha256:b33d2bc4f69caedcd0a275329eb2198f560b325605810895627be5d4b876bf7f", + "sha256:b59c0ffceff8d4d3996a45f2bb6f4c207f94684a96bf3d9728dbb77428dd8cb8", + "sha256:bb6834cbd983b19f06908b45bfc2dad6ac9479ae04abe923a275b5f48f1a186b", + "sha256:bd3db01f59fdcbce5b22afad19e390260d6d0222f35a1023d9adc5690a889364", + "sha256:bd7c23df857d488f418439686d3b10ae2fbf9bc256cd045b37a8c16575ea1040", + "sha256:c2528d60e398c7c4c799d56f907664673a807635b857df18f7ae64d3e6ce2d9f", + "sha256:d31a63bc6e6d87f77d71e1abbd7387ab817a66733734883d1fc0021ed9bfa083", + "sha256:d4492d82b3bc7fbb7e3610747b159869468079fe149ec5c4d771fa1f614a14df", + "sha256:ddcb8581510311e13421b1f544403c16e901c4e8f09083c881fab2be80ee31ba", + "sha256:e1d59258c3c67c865435d8fbeb35f8c59b8bef3d6f46c1f29f6123556af28445", + "sha256:eb3315a8a236ee19b6df481fc5f997436e8ade24a9f03dfdc6bd490fea20c6da", + "sha256:ef2b055471c0eb466033760a521efb9d8a32b99ab907fc8358481a1dd29e3bd3", + "sha256:ef5adb9a3b1d0c645ff694f9bca7702ec2c70f4d734f9922ea34de02294fdf72", + "sha256:f32c38afb74bd98ce26de7cc74a67b40afb7b05aae7b42924ea990d51e4dac02", + "sha256:fe0ccca550bb8e5abc22f530ec0466136379c01321fd94f30a22231e8a48d985" ], "markers": "python_version >= '3.9'", - "version": "==1.2.0" + "version": "==1.2.1" }, "coverage": { "extras": [ @@ -675,6 +742,44 @@ "markers": "python_version >= '3.8'", "version": "==7.4.1" }, + "cryptography": { + "hashes": [ + "sha256:0270572b8bd2c833c3981724b8ee9747b3ec96f699a9665470018594301439ee", + "sha256:111a0d8553afcf8eb02a4fea6ca4f59d48ddb34497aa8706a6cf536f1a5ec576", + "sha256:16a48c23a62a2f4a285699dba2e4ff2d1cff3115b9df052cdd976a18856d8e3d", + "sha256:1b95b98b0d2af784078fa69f637135e3c317091b615cd0905f8b8a087e86fa30", + "sha256:1f71c10d1e88467126f0efd484bd44bca5e14c664ec2ede64c32f20875c0d413", + "sha256:2424ff4c4ac7f6b8177b53c17ed5d8fa74ae5955656867f5a8affaca36a27abb", + "sha256:2bce03af1ce5a5567ab89bd90d11e7bbdff56b8af3acbbec1faded8f44cb06da", + "sha256:329906dcc7b20ff3cad13c069a78124ed8247adcac44b10bea1130e36caae0b4", + "sha256:37dd623507659e08be98eec89323469e8c7b4c1407c85112634ae3dbdb926fdd", + "sha256:3eaafe47ec0d0ffcc9349e1708be2aaea4c6dd4978d76bf6eb0cb2c13636c6fc", + "sha256:5e6275c09d2badf57aea3afa80d975444f4be8d3bc58f7f80d2a484c6f9485c8", + "sha256:6fe07eec95dfd477eb9530aef5bead34fec819b3aaf6c5bd6d20565da607bfe1", + "sha256:7367d7b2eca6513681127ebad53b2582911d1736dc2ffc19f2c3ae49997496bc", + "sha256:7cde5f38e614f55e28d831754e8a3bacf9ace5d1566235e39d91b35502d6936e", + "sha256:9481ffe3cf013b71b2428b905c4f7a9a4f76ec03065b05ff499bb5682a8d9ad8", + "sha256:98d8dc6d012b82287f2c3d26ce1d2dd130ec200c8679b6213b3c73c08b2b7940", + "sha256:a011a644f6d7d03736214d38832e030d8268bcff4a41f728e6030325fea3e400", + "sha256:a2913c5375154b6ef2e91c10b5720ea6e21007412f6437504ffea2109b5a33d7", + "sha256:a30596bae9403a342c978fb47d9b0ee277699fa53bbafad14706af51fe543d16", + "sha256:b03c2ae5d2f0fc05f9a2c0c997e1bc18c8229f392234e8a0194f202169ccd278", + "sha256:b6cd2203306b63e41acdf39aa93b86fb566049aeb6dc489b70e34bcd07adca74", + "sha256:b7ffe927ee6531c78f81aa17e684e2ff617daeba7f189f911065b2ea2d526dec", + "sha256:b8cac287fafc4ad485b8a9b67d0ee80c66bf3574f655d3b97ef2e1082360faf1", + "sha256:ba334e6e4b1d92442b75ddacc615c5476d4ad55cc29b15d590cc6b86efa487e2", + "sha256:ba3e4a42397c25b7ff88cdec6e2a16c2be18720f317506ee25210f6d31925f9c", + "sha256:c41fb5e6a5fe9ebcd58ca3abfeb51dffb5d83d6775405305bfa8715b76521922", + "sha256:cd2030f6650c089aeb304cf093f3244d34745ce0cfcc39f20c6fbfe030102e2a", + "sha256:cd65d75953847815962c84a4654a84850b2bb4aed3f26fadcc1c13892e1e29f6", + "sha256:e4985a790f921508f36f81831817cbc03b102d643b5fcb81cd33df3fa291a1a1", + "sha256:e807b3188f9eb0eaa7bbb579b462c5ace579f1cedb28107ce8b48a9f7ad3679e", + "sha256:f12764b8fffc7a123f641d7d049d382b73f96a34117e0b637b80643169cec8ac", + "sha256:f8837fe1d6ac4a8052a9a8ddab256bc006242696f03368a4009be7ee3075cdb7" + ], + "markers": "python_version >= '3.7'", + "version": "==42.0.5" + }, "cycler": { "hashes": [ "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", @@ -710,11 +815,11 @@ }, "execnet": { "hashes": [ - "sha256:88256416ae766bc9e8895c76a87928c0012183da3cc4fc18016e6f050e025f41", - "sha256:cc59bc4423742fd71ad227122eb0dd44db51efb3dc4095b45ac9a08c770096af" + "sha256:26dee51f1b80cebd6d0ca8e74dd8745419761d3bef34163928cbebbdc4749fdc", + "sha256:5189b52c6121c24feae288166ab41b32549c7e2348652736540b9e6e7d4e72e3" ], - "markers": "python_version >= '3.7'", - "version": "==2.0.2" + "markers": "python_version >= '3.8'", + "version": "==2.1.1" }, "flake8": { "hashes": [ @@ -754,51 +859,51 @@ }, "fonttools": { "hashes": [ - "sha256:0255dbc128fee75fb9be364806b940ed450dd6838672a150d501ee86523ac61e", - "sha256:0a00bd0e68e88987dcc047ea31c26d40a3c61185153b03457956a87e39d43c37", - "sha256:0a1d313a415eaaba2b35d6cd33536560deeebd2ed758b9bfb89ab5d97dc5deac", - "sha256:0f750037e02beb8b3569fbff701a572e62a685d2a0e840d75816592280e5feae", - "sha256:13819db8445a0cec8c3ff5f243af6418ab19175072a9a92f6cc8ca7d1452754b", - "sha256:254d9a6f7be00212bf0c3159e0a420eb19c63793b2c05e049eb337f3023c5ecc", - "sha256:29495d6d109cdbabe73cfb6f419ce67080c3ef9ea1e08d5750240fd4b0c4763b", - "sha256:32ab2e9702dff0dd4510c7bb958f265a8d3dd5c0e2547e7b5f7a3df4979abb07", - "sha256:3480eeb52770ff75140fe7d9a2ec33fb67b07efea0ab5129c7e0c6a639c40c70", - "sha256:3a808f3c1d1df1f5bf39be869b6e0c263570cdafb5bdb2df66087733f566ea71", - "sha256:3b629108351d25512d4ea1a8393a2dba325b7b7d7308116b605ea3f8e1be88df", - "sha256:3d71606c9321f6701642bd4746f99b6089e53d7e9817fc6b964e90d9c5f0ecc6", - "sha256:3e2b95dce2ead58fb12524d0ca7d63a63459dd489e7e5838c3cd53557f8933e1", - "sha256:4a5a5318ba5365d992666ac4fe35365f93004109d18858a3e18ae46f67907670", - "sha256:4c811d3c73b6abac275babb8aa439206288f56fdb2c6f8835e3d7b70de8937a7", - "sha256:4e743935139aa485fe3253fc33fe467eab6ea42583fa681223ea3f1a93dd01e6", - "sha256:4ec558c543609e71b2275c4894e93493f65d2f41c15fe1d089080c1d0bb4d635", - "sha256:5465df494f20a7d01712b072ae3ee9ad2887004701b95cb2cc6dcb9c2c97a899", - "sha256:5b60e3afa9635e3dfd3ace2757039593e3bd3cf128be0ddb7a1ff4ac45fa5a50", - "sha256:63fbed184979f09a65aa9c88b395ca539c94287ba3a364517698462e13e457c9", - "sha256:69731e8bea0578b3c28fdb43dbf95b9386e2d49a399e9a4ad736b8e479b08085", - "sha256:6dd58cc03016b281bd2c74c84cdaa6bd3ce54c5a7f47478b7657b930ac3ed8eb", - "sha256:740947906590a878a4bde7dd748e85fefa4d470a268b964748403b3ab2aeed6c", - "sha256:7df26dd3650e98ca45f1e29883c96a0b9f5bb6af8d632a6a108bc744fa0bd9b3", - "sha256:7eb7ad665258fba68fd22228a09f347469d95a97fb88198e133595947a20a184", - "sha256:7ee48bd9d6b7e8f66866c9090807e3a4a56cf43ffad48962725a190e0dd774c8", - "sha256:86e0427864c6c91cf77f16d1fb9bf1bbf7453e824589e8fb8461b6ee1144f506", - "sha256:8f57ecd742545362a0f7186774b2d1c53423ed9ece67689c93a1055b236f638c", - "sha256:90f898cdd67f52f18049250a6474185ef6544c91f27a7bee70d87d77a8daf89c", - "sha256:94208ea750e3f96e267f394d5588579bb64cc628e321dbb1d4243ffbc291b18b", - "sha256:a1c154bb85dc9a4cf145250c88d112d88eb414bad81d4cb524d06258dea1bdc0", - "sha256:a5d77479fb885ef38a16a253a2f4096bc3d14e63a56d6246bfdb56365a12b20c", - "sha256:a86a5ab2873ed2575d0fcdf1828143cfc6b977ac448e3dc616bb1e3d20efbafa", - "sha256:ac71e2e201df041a2891067dc36256755b1229ae167edbdc419b16da78732c2f", - "sha256:b3e1304e5f19ca861d86a72218ecce68f391646d85c851742d265787f55457a4", - "sha256:b8be28c036b9f186e8c7eaf8a11b42373e7e4949f9e9f370202b9da4c4c3f56c", - "sha256:c19044256c44fe299d9a73456aabee4b4d06c6b930287be93b533b4737d70aa1", - "sha256:d49ce3ea7b7173faebc5664872243b40cf88814ca3eb135c4a3cdff66af71946", - "sha256:e040f905d542362e07e72e03612a6270c33d38281fd573160e1003e43718d68d", - "sha256:eabae77a07c41ae0b35184894202305c3ad211a93b2eb53837c2a1143c8bc952", - "sha256:f791446ff297fd5f1e2247c188de53c1bfb9dd7f0549eba55b73a3c2087a2703", - "sha256:f83a4daef6d2a202acb9bf572958f91cfde5b10c8ee7fb1d09a4c81e5d851fd8" + "sha256:0118ef998a0699a96c7b28457f15546815015a2710a1b23a7bf6c1be60c01636", + "sha256:0d145976194a5242fdd22df18a1b451481a88071feadf251221af110ca8f00ce", + "sha256:0e19bd9e9964a09cd2433a4b100ca7f34e34731e0758e13ba9a1ed6e5468cc0f", + "sha256:0f08c901d3866a8905363619e3741c33f0a83a680d92a9f0e575985c2634fcc1", + "sha256:1250e818b5f8a679ad79660855528120a8f0288f8f30ec88b83db51515411fcc", + "sha256:15c94eeef6b095831067f72c825eb0e2d48bb4cea0647c1b05c981ecba2bf39f", + "sha256:1621ee57da887c17312acc4b0e7ac30d3a4fb0fec6174b2e3754a74c26bbed1e", + "sha256:180194c7fe60c989bb627d7ed5011f2bef1c4d36ecf3ec64daec8302f1ae0716", + "sha256:278e50f6b003c6aed19bae2242b364e575bcb16304b53f2b64f6551b9c000e15", + "sha256:32b17504696f605e9e960647c5f64b35704782a502cc26a37b800b4d69ff3c77", + "sha256:3bee3f3bd9fa1d5ee616ccfd13b27ca605c2b4270e45715bd2883e9504735034", + "sha256:4060acc2bfa2d8e98117828a238889f13b6f69d59f4f2d5857eece5277b829ba", + "sha256:54dcf21a2f2d06ded676e3c3f9f74b2bafded3a8ff12f0983160b13e9f2fb4a7", + "sha256:56fc244f2585d6c00b9bcc59e6593e646cf095a96fe68d62cd4da53dd1287b55", + "sha256:599bdb75e220241cedc6faebfafedd7670335d2e29620d207dd0378a4e9ccc5a", + "sha256:5f6bc991d1610f5c3bbe997b0233cbc234b8e82fa99fc0b2932dc1ca5e5afec0", + "sha256:60a3409c9112aec02d5fb546f557bca6efa773dcb32ac147c6baf5f742e6258b", + "sha256:68b3fb7775a923be73e739f92f7e8a72725fd333eab24834041365d2278c3671", + "sha256:76f1777d8b3386479ffb4a282e74318e730014d86ce60f016908d9801af9ca2a", + "sha256:806e7912c32a657fa39d2d6eb1d3012d35f841387c8fc6cf349ed70b7c340039", + "sha256:84d7751f4468dd8cdd03ddada18b8b0857a5beec80bce9f435742abc9a851a74", + "sha256:865a58b6e60b0938874af0968cd0553bcd88e0b2cb6e588727117bd099eef836", + "sha256:8ac27f436e8af7779f0bb4d5425aa3535270494d3bc5459ed27de3f03151e4c2", + "sha256:8b4850fa2ef2cfbc1d1f689bc159ef0f45d8d83298c1425838095bf53ef46308", + "sha256:8b5ad456813d93b9c4b7ee55302208db2b45324315129d85275c01f5cb7e61a2", + "sha256:8e2f1a4499e3b5ee82c19b5ee57f0294673125c65b0a1ff3764ea1f9db2f9ef5", + "sha256:9696fe9f3f0c32e9a321d5268208a7cc9205a52f99b89479d1b035ed54c923f1", + "sha256:96a48e137c36be55e68845fc4284533bda2980f8d6f835e26bca79d7e2006438", + "sha256:a8feca65bab31479d795b0d16c9a9852902e3a3c0630678efb0b2b7941ea9c74", + "sha256:aefa011207ed36cd280babfaa8510b8176f1a77261833e895a9d96e57e44802f", + "sha256:b2b92381f37b39ba2fc98c3a45a9d6383bfc9916a87d66ccb6553f7bdd129097", + "sha256:b3c61423f22165541b9403ee39874dcae84cd57a9078b82e1dce8cb06b07fa2e", + "sha256:b5b48a1121117047d82695d276c2af2ee3a24ffe0f502ed581acc2673ecf1037", + "sha256:c18b49adc721a7d0b8dfe7c3130c89b8704baf599fb396396d07d4aa69b824a1", + "sha256:c5b8cab0c137ca229433570151b5c1fc6af212680b58b15abd797dcdd9dd5051", + "sha256:c7e91abdfae1b5c9e3a543f48ce96013f9a08c6c9668f1e6be0beabf0a569c1b", + "sha256:cadf4e12a608ef1d13e039864f484c8a968840afa0258b0b843a0556497ea9ed", + "sha256:dc0673361331566d7a663d7ce0f6fdcbfbdc1f59c6e3ed1165ad7202ca183c68", + "sha256:de7c29bdbdd35811f14493ffd2534b88f0ce1b9065316433b22d63ca1cd21f14", + "sha256:e9d9298be7a05bb4801f558522adbe2feea1b0b103d5294ebf24a92dd49b78e5", + "sha256:ee1af4be1c5afe4c96ca23badd368d8dc75f611887fb0c0dac9f71ee5d6f110e", + "sha256:f7e89853d8bea103c8e3514b9f9dc86b5b4120afb4583b57eb10dfa5afbe0936" ], "markers": "python_version >= '3.8'", - "version": "==4.47.2" + "version": "==4.51.0" }, "gprof2dot": { "hashes": [ @@ -819,11 +924,11 @@ }, "idna": { "hashes": [ - "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", - "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f" + "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc", + "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0" ], "markers": "python_version >= '3.5'", - "version": "==3.6" + "version": "==3.7" }, "imagesize": { "hashes": [ @@ -835,11 +940,11 @@ }, "importlib-metadata": { "hashes": [ - "sha256:4805911c3a4ec7c3966410053e9ec6a1fecd629117df5adee56dfc9432a1081e", - "sha256:f238736bb06590ae52ac1fab06a3a9ef1d8dce2b7a35b5ab329371d6c8f5d2cc" + "sha256:30962b96c0c223483ed6cc7280e7f0199feb01a0e40cfae4d4450fc6fab1f570", + "sha256:b78938b926ee8d5f020fc4772d487045805a55ddbad2ecf21c6d60938dc7fcd2" ], "markers": "python_version >= '3.8'", - "version": "==7.0.1" + "version": "==7.1.0" }, "iniconfig": { "hashes": [ @@ -851,11 +956,35 @@ }, "jaraco.classes": { "hashes": [ - "sha256:10afa92b6743f25c0cf5f37c6bb6e18e2c5bb84a16527ccfc0040ea377e7aaeb", - "sha256:c063dd08e89217cee02c8d5e5ec560f2c8ce6cdc2fcdc2e68f7b2e5547ed3621" + "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", + "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790" ], "markers": "python_version >= '3.8'", - "version": "==3.3.0" + "version": "==3.4.0" + }, + "jaraco.context": { + "hashes": [ + "sha256:3e16388f7da43d384a1a7cd3452e72e14732ac9fe459678773a3608a812bf266", + "sha256:c2f67165ce1f9be20f32f650f25d8edfc1646a8aeee48ae06fb35f90763576d2" + ], + "markers": "python_version >= '3.8'", + "version": "==5.3.0" + }, + "jaraco.functools": { + "hashes": [ + "sha256:c279cb24c93d694ef7270f970d499cab4d3813f4e08273f95398651a634f0925", + "sha256:daf276ddf234bea897ef14f43c4e1bf9eefeac7b7a82a4dd69228ac20acff68d" + ], + "markers": "python_version >= '3.8'", + "version": "==4.0.0" + }, + "jeepney": { + "hashes": [ + "sha256:5efe48d255973902f6badc3ce55e2aa6c5c3b3bc642059ef3a91247bcfcc5806", + "sha256:c0a454ad016ca575060802ee4d590dd912e35c122fa04e70306de3d076cce755" + ], + "markers": "sys_platform == 'linux'", + "version": "==0.8.0" }, "jinja2": { "hashes": [ @@ -866,13 +995,22 @@ "markers": "python_version >= '3.6'", "version": "==3.0.3" }, + "jsonschema": { + "hashes": [ + "sha256:0f864437ab8b6076ba6707453ef8f98a6a0d512a80e93f8abdb676f737ecb60d", + "sha256:a870ad254da1a8ca84b6a2905cac29d265f805acc57af304784962a2aa6508f6" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==4.17.3" + }, "keyring": { "hashes": [ - "sha256:4446d35d636e6a10b8bce7caa66913dd9eca5fd222ca03a3d42c38608ac30836", - "sha256:e730ecffd309658a08ee82535a3b5ec4b4c8669a9be11efb66249d8e0aeb9a25" + "sha256:26fc12e6a329d61d24aa47b22a7c5c3f35753df7d8f2860973cf94f4e1fb3427", + "sha256:7230ea690525133f6ad536a9b5def74a4bd52642abe594761028fc044d7c7893" ], "markers": "python_version >= '3.8'", - "version": "==24.3.0" + "version": "==25.1.0" }, "kiwisolver": { "hashes": [ @@ -994,103 +1132,103 @@ }, "markupsafe": { "hashes": [ - "sha256:0042d6a9880b38e1dd9ff83146cc3c9c18a059b9360ceae207805567aacccc69", - "sha256:0c26f67b3fe27302d3a412b85ef696792c4a2386293c53ba683a89562f9399b0", - "sha256:0fbad3d346df8f9d72622ac71b69565e621ada2ce6572f37c2eae8dacd60385d", - "sha256:15866d7f2dc60cfdde12ebb4e75e41be862348b4728300c36cdf405e258415ec", - "sha256:1c98c33ffe20e9a489145d97070a435ea0679fddaabcafe19982fe9c971987d5", - "sha256:21e7af8091007bf4bebf4521184f4880a6acab8df0df52ef9e513d8e5db23411", - "sha256:23984d1bdae01bee794267424af55eef4dfc038dc5d1272860669b2aa025c9e3", - "sha256:31f57d64c336b8ccb1966d156932f3daa4fee74176b0fdc48ef580be774aae74", - "sha256:3583a3a3ab7958e354dc1d25be74aee6228938312ee875a22330c4dc2e41beb0", - "sha256:36d7626a8cca4d34216875aee5a1d3d654bb3dac201c1c003d182283e3205949", - "sha256:396549cea79e8ca4ba65525470d534e8a41070e6b3500ce2414921099cb73e8d", - "sha256:3a66c36a3864df95e4f62f9167c734b3b1192cb0851b43d7cc08040c074c6279", - "sha256:3aae9af4cac263007fd6309c64c6ab4506dd2b79382d9d19a1994f9240b8db4f", - "sha256:3ab3a886a237f6e9c9f4f7d272067e712cdb4efa774bef494dccad08f39d8ae6", - "sha256:47bb5f0142b8b64ed1399b6b60f700a580335c8e1c57f2f15587bd072012decc", - "sha256:49a3b78a5af63ec10d8604180380c13dcd870aba7928c1fe04e881d5c792dc4e", - "sha256:4df98d4a9cd6a88d6a585852f56f2155c9cdb6aec78361a19f938810aa020954", - "sha256:5045e892cfdaecc5b4c01822f353cf2c8feb88a6ec1c0adef2a2e705eef0f656", - "sha256:5244324676254697fe5c181fc762284e2c5fceeb1c4e3e7f6aca2b6f107e60dc", - "sha256:54635102ba3cf5da26eb6f96c4b8c53af8a9c0d97b64bdcb592596a6255d8518", - "sha256:54a7e1380dfece8847c71bf7e33da5d084e9b889c75eca19100ef98027bd9f56", - "sha256:55d03fea4c4e9fd0ad75dc2e7e2b6757b80c152c032ea1d1de487461d8140efc", - "sha256:698e84142f3f884114ea8cf83e7a67ca8f4ace8454e78fe960646c6c91c63bfa", - "sha256:6aa5e2e7fc9bc042ae82d8b79d795b9a62bd8f15ba1e7594e3db243f158b5565", - "sha256:7653fa39578957bc42e5ebc15cf4361d9e0ee4b702d7d5ec96cdac860953c5b4", - "sha256:765f036a3d00395a326df2835d8f86b637dbaf9832f90f5d196c3b8a7a5080cb", - "sha256:78bc995e004681246e85e28e068111a4c3f35f34e6c62da1471e844ee1446250", - "sha256:7a07f40ef8f0fbc5ef1000d0c78771f4d5ca03b4953fc162749772916b298fc4", - "sha256:8b570a1537367b52396e53325769608f2a687ec9a4363647af1cded8928af959", - "sha256:987d13fe1d23e12a66ca2073b8d2e2a75cec2ecb8eab43ff5624ba0ad42764bc", - "sha256:9896fca4a8eb246defc8b2a7ac77ef7553b638e04fbf170bff78a40fa8a91474", - "sha256:9e9e3c4020aa2dc62d5dd6743a69e399ce3de58320522948af6140ac959ab863", - "sha256:a0b838c37ba596fcbfca71651a104a611543077156cb0a26fe0c475e1f152ee8", - "sha256:a4d176cfdfde84f732c4a53109b293d05883e952bbba68b857ae446fa3119b4f", - "sha256:a76055d5cb1c23485d7ddae533229039b850db711c554a12ea64a0fd8a0129e2", - "sha256:a76cd37d229fc385738bd1ce4cba2a121cf26b53864c1772694ad0ad348e509e", - "sha256:a7cc49ef48a3c7a0005a949f3c04f8baa5409d3f663a1b36f0eba9bfe2a0396e", - "sha256:abf5ebbec056817057bfafc0445916bb688a255a5146f900445d081db08cbabb", - "sha256:b0fe73bac2fed83839dbdbe6da84ae2a31c11cfc1c777a40dbd8ac8a6ed1560f", - "sha256:b6f14a9cd50c3cb100eb94b3273131c80d102e19bb20253ac7bd7336118a673a", - "sha256:b83041cda633871572f0d3c41dddd5582ad7d22f65a72eacd8d3d6d00291df26", - "sha256:b835aba863195269ea358cecc21b400276747cc977492319fd7682b8cd2c253d", - "sha256:bf1196dcc239e608605b716e7b166eb5faf4bc192f8a44b81e85251e62584bd2", - "sha256:c669391319973e49a7c6230c218a1e3044710bc1ce4c8e6eb71f7e6d43a2c131", - "sha256:c7556bafeaa0a50e2fe7dc86e0382dea349ebcad8f010d5a7dc6ba568eaaa789", - "sha256:c8f253a84dbd2c63c19590fa86a032ef3d8cc18923b8049d91bcdeeb2581fbf6", - "sha256:d18b66fe626ac412d96c2ab536306c736c66cf2a31c243a45025156cc190dc8a", - "sha256:d5291d98cd3ad9a562883468c690a2a238c4a6388ab3bd155b0c75dd55ece858", - "sha256:d5c31fe855c77cad679b302aabc42d724ed87c043b1432d457f4976add1c2c3e", - "sha256:d6e427c7378c7f1b2bef6a344c925b8b63623d3321c09a237b7cc0e77dd98ceb", - "sha256:dac1ebf6983148b45b5fa48593950f90ed6d1d26300604f321c74a9ca1609f8e", - "sha256:de8153a7aae3835484ac168a9a9bdaa0c5eee4e0bc595503c95d53b942879c84", - "sha256:e1a0d1924a5013d4f294087e00024ad25668234569289650929ab871231668e7", - "sha256:e7902211afd0af05fbadcc9a312e4cf10f27b779cf1323e78d52377ae4b72bea", - "sha256:e888ff76ceb39601c59e219f281466c6d7e66bd375b4ec1ce83bcdc68306796b", - "sha256:f06e5a9e99b7df44640767842f414ed5d7bedaaa78cd817ce04bbd6fd86e2dd6", - "sha256:f6be2d708a9d0e9b0054856f07ac7070fbe1754be40ca8525d5adccdbda8f475", - "sha256:f9917691f410a2e0897d1ef99619fd3f7dd503647c8ff2475bf90c3cf222ad74", - "sha256:fc1a75aa8f11b87910ffd98de62b29d6520b6d6e8a3de69a70ca34dea85d2a8a", - "sha256:fe8512ed897d5daf089e5bd010c3dc03bb1bdae00b35588c49b98268d4a01e00" + "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf", + "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff", + "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f", + "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3", + "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532", + "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f", + "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617", + "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df", + "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4", + "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906", + "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f", + "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4", + "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8", + "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371", + "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2", + "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465", + "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52", + "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6", + "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169", + "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad", + "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2", + "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0", + "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029", + "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f", + "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a", + "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced", + "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5", + "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c", + "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf", + "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9", + "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb", + "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad", + "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3", + "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1", + "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46", + "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc", + "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a", + "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee", + "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900", + "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5", + "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea", + "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f", + "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5", + "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e", + "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a", + "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f", + "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50", + "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a", + "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b", + "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4", + "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff", + "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2", + "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46", + "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b", + "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf", + "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5", + "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5", + "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab", + "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd", + "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68" ], "markers": "python_version >= '3.7'", - "version": "==2.1.4" + "version": "==2.1.5" }, "matplotlib": { "hashes": [ - "sha256:01a978b871b881ee76017152f1f1a0cbf6bd5f7b8ff8c96df0df1bd57d8755a1", - "sha256:03f9d160a29e0b65c0790bb07f4f45d6a181b1ac33eb1bb0dd225986450148f0", - "sha256:091275d18d942cf1ee9609c830a1bc36610607d8223b1b981c37d5c9fc3e46a4", - "sha256:09796f89fb71a0c0e1e2f4bdaf63fb2cefc84446bb963ecdeb40dfee7dfa98c7", - "sha256:0f4fc5d72b75e2c18e55eb32292659cf731d9d5b312a6eb036506304f4675630", - "sha256:172f4d0fbac3383d39164c6caafd3255ce6fa58f08fc392513a0b1d3b89c4f89", - "sha256:1b0f3b8ea0e99e233a4bcc44590f01604840d833c280ebb8fe5554fd3e6cfe8d", - "sha256:3773002da767f0a9323ba1a9b9b5d00d6257dbd2a93107233167cfb581f64717", - "sha256:46a569130ff53798ea5f50afce7406e91fdc471ca1e0e26ba976a8c734c9427a", - "sha256:4c318c1e95e2f5926fba326f68177dee364aa791d6df022ceb91b8221bd0a627", - "sha256:4e208f46cf6576a7624195aa047cb344a7f802e113bb1a06cfd4bee431de5e31", - "sha256:533b0e3b0c6768eef8cbe4b583731ce25a91ab54a22f830db2b031e83cca9213", - "sha256:5864bdd7da445e4e5e011b199bb67168cdad10b501750367c496420f2ad00843", - "sha256:5ba9cbd8ac6cf422f3102622b20f8552d601bf8837e49a3afed188d560152788", - "sha256:6f9c6976748a25e8b9be51ea028df49b8e561eed7809146da7a47dbecebab367", - "sha256:7c48d9e221b637c017232e3760ed30b4e8d5dfd081daf327e829bf2a72c731b4", - "sha256:830f00640c965c5b7f6bc32f0d4ce0c36dfe0379f7dd65b07a00c801713ec40a", - "sha256:9a5430836811b7652991939012f43d2808a2db9b64ee240387e8c43e2e5578c8", - "sha256:aa11b3c6928a1e496c1a79917d51d4cd5d04f8a2e75f21df4949eeefdf697f4b", - "sha256:b78e4f2cedf303869b782071b55fdde5987fda3038e9d09e58c91cc261b5ad18", - "sha256:b9576723858a78751d5aacd2497b8aef29ffea6d1c95981505877f7ac28215c6", - "sha256:bddfb1db89bfaa855912261c805bd0e10218923cc262b9159a49c29a7a1c1afa", - "sha256:c7d36c2209d9136cd8e02fab1c0ddc185ce79bc914c45054a9f514e44c787917", - "sha256:d1095fecf99eeb7384dabad4bf44b965f929a5f6079654b681193edf7169ec20", - "sha256:d7b1704a530395aaf73912be741c04d181f82ca78084fbd80bc737be04848331", - "sha256:d86593ccf546223eb75a39b44c32788e6f6440d13cfc4750c1c15d0fcb850b63", - "sha256:deaed9ad4da0b1aea77fe0aa0cebb9ef611c70b3177be936a95e5d01fa05094f", - "sha256:ef8345b48e95cee45ff25192ed1f4857273117917a4dcd48e3905619bcd9c9b8" + "sha256:1c13f041a7178f9780fb61cc3a2b10423d5e125480e4be51beaf62b172413b67", + "sha256:232ce322bfd020a434caaffbd9a95333f7c2491e59cfc014041d95e38ab90d1c", + "sha256:493e9f6aa5819156b58fce42b296ea31969f2aab71c5b680b4ea7a3cb5c07d94", + "sha256:50bac6e4d77e4262c4340d7a985c30912054745ec99756ce213bfbc3cb3808eb", + "sha256:606e3b90897554c989b1e38a258c626d46c873523de432b1462f295db13de6f9", + "sha256:6209e5c9aaccc056e63b547a8152661324404dd92340a6e479b3a7f24b42a5d0", + "sha256:6485ac1f2e84676cff22e693eaa4fbed50ef5dc37173ce1f023daef4687df616", + "sha256:6addbd5b488aedb7f9bc19f91cd87ea476206f45d7116fcfe3d31416702a82fa", + "sha256:72f9322712e4562e792b2961971891b9fbbb0e525011e09ea0d1f416c4645661", + "sha256:7a6769f58ce51791b4cb8b4d7642489df347697cd3e23d88266aaaee93b41d9a", + "sha256:8080d5081a86e690d7688ffa542532e87f224c38a6ed71f8fbed34dd1d9fedae", + "sha256:843cbde2f0946dadd8c5c11c6d91847abd18ec76859dc319362a0964493f0ba6", + "sha256:8aac397d5e9ec158960e31c381c5ffc52ddd52bd9a47717e2a694038167dffea", + "sha256:8f65c9f002d281a6e904976007b2d46a1ee2bcea3a68a8c12dda24709ddc9106", + "sha256:90df07db7b599fe7035d2f74ab7e438b656528c68ba6bb59b7dc46af39ee48ef", + "sha256:9bb0189011785ea794ee827b68777db3ca3f93f3e339ea4d920315a0e5a78d54", + "sha256:a0e47eda4eb2614300fc7bb4657fced3e83d6334d03da2173b09e447418d499f", + "sha256:abc9d838f93583650c35eca41cfcec65b2e7cb50fd486da6f0c49b5e1ed23014", + "sha256:ac24233e8f2939ac4fd2919eed1e9c0871eac8057666070e94cbf0b33dd9c338", + "sha256:b12ba985837e4899b762b81f5b2845bd1a28f4fdd1a126d9ace64e9c4eb2fb25", + "sha256:b7a2a253d3b36d90c8993b4620183b55665a429da8357a4f621e78cd48b2b30b", + "sha256:c7064120a59ce6f64103c9cefba8ffe6fba87f2c61d67c401186423c9a20fd35", + "sha256:c89ee9314ef48c72fe92ce55c4e95f2f39d70208f9f1d9db4e64079420d8d732", + "sha256:cc4ccdc64e3039fc303defd119658148f2349239871db72cd74e2eeaa9b80b71", + "sha256:ce1edd9f5383b504dbc26eeea404ed0a00656c526638129028b758fd43fc5f10", + "sha256:ecd79298550cba13a43c340581a3ec9c707bd895a6a061a78fa2524660482fc0", + "sha256:f51c4c869d4b60d769f7b4406eec39596648d9d70246428745a681c327a8ad30", + "sha256:fb44f53af0a62dc80bba4443d9b27f2fde6acfdac281d95bc872dc148a6509cc" ], "markers": "python_version >= '3.9'", - "version": "==3.8.2" + "version": "==3.8.4" }, "mccabe": { "hashes": [ @@ -1169,24 +1307,24 @@ }, "nh3": { "hashes": [ - "sha256:0d02d0ff79dfd8208ed25a39c12cbda092388fff7f1662466e27d97ad011b770", - "sha256:3277481293b868b2715907310c7be0f1b9d10491d5adf9fce11756a97e97eddf", - "sha256:3b803a5875e7234907f7d64777dfde2b93db992376f3d6d7af7f3bc347deb305", - "sha256:427fecbb1031db085eaac9931362adf4a796428ef0163070c484b5a768e71601", - "sha256:5f0d77272ce6d34db6c87b4f894f037d55183d9518f948bba236fe81e2bb4e28", - "sha256:60684857cfa8fdbb74daa867e5cad3f0c9789415aba660614fe16cd66cbb9ec7", - "sha256:6f42f99f0cf6312e470b6c09e04da31f9abaadcd3eb591d7d1a88ea931dca7f3", - "sha256:86e447a63ca0b16318deb62498db4f76fc60699ce0a1231262880b38b6cff911", - "sha256:8d595df02413aa38586c24811237e95937ef18304e108b7e92c890a06793e3bf", - "sha256:9c0d415f6b7f2338f93035bba5c0d8c1b464e538bfbb1d598acd47d7969284f0", - "sha256:a5167a6403d19c515217b6bcaaa9be420974a6ac30e0da9e84d4fc67a5d474c5", - "sha256:ac19c0d68cd42ecd7ead91a3a032fdfff23d29302dbb1311e641a130dfefba97", - "sha256:b1e97221cedaf15a54f5243f2c5894bb12ca951ae4ddfd02a9d4ea9df9e1a29d", - "sha256:bc2d086fb540d0fa52ce35afaded4ea526b8fc4d3339f783db55c95de40ef02e", - "sha256:d1e30ff2d8d58fb2a14961f7aac1bbb1c51f9bdd7da727be35c63826060b0bf3", - "sha256:f3b53ba93bb7725acab1e030bc2ecd012a817040fd7851b332f86e2f9bb98dc6" - ], - "version": "==0.2.15" + "sha256:0316c25b76289cf23be6b66c77d3608a4fdf537b35426280032f432f14291b9a", + "sha256:1a814dd7bba1cb0aba5bcb9bebcc88fd801b63e21e2450ae6c52d3b3336bc911", + "sha256:1aa52a7def528297f256de0844e8dd680ee279e79583c76d6fa73a978186ddfb", + "sha256:22c26e20acbb253a5bdd33d432a326d18508a910e4dcf9a3316179860d53345a", + "sha256:40015514022af31975c0b3bca4014634fa13cb5dc4dbcbc00570acc781316dcc", + "sha256:40d0741a19c3d645e54efba71cb0d8c475b59135c1e3c580f879ad5514cbf028", + "sha256:551672fd71d06cd828e282abdb810d1be24e1abb7ae2543a8fa36a71c1006fe9", + "sha256:66f17d78826096291bd264f260213d2b3905e3c7fae6dfc5337d49429f1dc9f3", + "sha256:85cdbcca8ef10733bd31f931956f7fbb85145a4d11ab9e6742bbf44d88b7e351", + "sha256:a3f55fabe29164ba6026b5ad5c3151c314d136fd67415a17660b4aaddacf1b10", + "sha256:b4427ef0d2dfdec10b641ed0bdaf17957eb625b2ec0ea9329b3d28806c153d71", + "sha256:ba73a2f8d3a1b966e9cdba7b211779ad8a2561d2dba9674b8a19ed817923f65f", + "sha256:c21bac1a7245cbd88c0b0e4a420221b7bfa838a2814ee5bb924e9c2f10a1120b", + "sha256:c551eb2a3876e8ff2ac63dff1585236ed5dfec5ffd82216a7a174f7c5082a78a", + "sha256:c790769152308421283679a142dbdb3d1c46c79c823008ecea8e8141db1a2062", + "sha256:d7a25fd8c86657f5d9d576268e3b3767c5cd4f42867c9383618be8517f0f022a" + ], + "version": "==0.2.17" }, "numpy": { "hashes": [ @@ -1222,13 +1360,18 @@ "index": "pypi", "version": "==0.9.1" }, + "opentrons-shared-data": { + "editable": true, + "markers": "python_version >= '3.8'", + "path": "../shared-data/python" + }, "packaging": { "hashes": [ - "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5", - "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7" + "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5", + "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9" ], "markers": "python_version >= '3.7'", - "version": "==23.2" + "version": "==24.0" }, "pathspec": { "hashes": [ @@ -1238,87 +1381,92 @@ "markers": "python_version >= '3.8'", "version": "==0.12.1" }, + "performance-metrics": { + "editable": true, + "file": "../performance-metrics" + }, "pillow": { "hashes": [ - "sha256:0304004f8067386b477d20a518b50f3fa658a28d44e4116970abfcd94fac34a8", - "sha256:0689b5a8c5288bc0504d9fcee48f61a6a586b9b98514d7d29b840143d6734f39", - "sha256:0eae2073305f451d8ecacb5474997c08569fb4eb4ac231ffa4ad7d342fdc25ac", - "sha256:0fb3e7fc88a14eacd303e90481ad983fd5b69c761e9e6ef94c983f91025da869", - "sha256:11fa2e5984b949b0dd6d7a94d967743d87c577ff0b83392f17cb3990d0d2fd6e", - "sha256:127cee571038f252a552760076407f9cff79761c3d436a12af6000cd182a9d04", - "sha256:154e939c5f0053a383de4fd3d3da48d9427a7e985f58af8e94d0b3c9fcfcf4f9", - "sha256:15587643b9e5eb26c48e49a7b33659790d28f190fc514a322d55da2fb5c2950e", - "sha256:170aeb00224ab3dc54230c797f8404507240dd868cf52066f66a41b33169bdbe", - "sha256:1b5e1b74d1bd1b78bc3477528919414874748dd363e6272efd5abf7654e68bef", - "sha256:1da3b2703afd040cf65ec97efea81cfba59cdbed9c11d8efc5ab09df9509fc56", - "sha256:1e23412b5c41e58cec602f1135c57dfcf15482013ce6e5f093a86db69646a5aa", - "sha256:2247178effb34a77c11c0e8ac355c7a741ceca0a732b27bf11e747bbc950722f", - "sha256:257d8788df5ca62c980314053197f4d46eefedf4e6175bc9412f14412ec4ea2f", - "sha256:3031709084b6e7852d00479fd1d310b07d0ba82765f973b543c8af5061cf990e", - "sha256:322209c642aabdd6207517e9739c704dc9f9db943015535783239022002f054a", - "sha256:322bdf3c9b556e9ffb18f93462e5f749d3444ce081290352c6070d014c93feb2", - "sha256:33870dc4653c5017bf4c8873e5488d8f8d5f8935e2f1fb9a2208c47cdd66efd2", - "sha256:35bb52c37f256f662abdfa49d2dfa6ce5d93281d323a9af377a120e89a9eafb5", - "sha256:3c31822339516fb3c82d03f30e22b1d038da87ef27b6a78c9549888f8ceda39a", - "sha256:3eedd52442c0a5ff4f887fab0c1c0bb164d8635b32c894bc1faf4c618dd89df2", - "sha256:3ff074fc97dd4e80543a3e91f69d58889baf2002b6be64347ea8cf5533188213", - "sha256:47c0995fc4e7f79b5cfcab1fc437ff2890b770440f7696a3ba065ee0fd496563", - "sha256:49d9ba1ed0ef3e061088cd1e7538a0759aab559e2e0a80a36f9fd9d8c0c21591", - "sha256:51f1a1bffc50e2e9492e87d8e09a17c5eea8409cda8d3f277eb6edc82813c17c", - "sha256:52a50aa3fb3acb9cf7213573ef55d31d6eca37f5709c69e6858fe3bc04a5c2a2", - "sha256:54f1852cd531aa981bc0965b7d609f5f6cc8ce8c41b1139f6ed6b3c54ab82bfb", - "sha256:609448742444d9290fd687940ac0b57fb35e6fd92bdb65386e08e99af60bf757", - "sha256:69ffdd6120a4737710a9eee73e1d2e37db89b620f702754b8f6e62594471dee0", - "sha256:6fad5ff2f13d69b7e74ce5b4ecd12cc0ec530fcee76356cac6742785ff71c452", - "sha256:7049e301399273a0136ff39b84c3678e314f2158f50f517bc50285fb5ec847ad", - "sha256:70c61d4c475835a19b3a5aa42492409878bbca7438554a1f89d20d58a7c75c01", - "sha256:716d30ed977be8b37d3ef185fecb9e5a1d62d110dfbdcd1e2a122ab46fddb03f", - "sha256:753cd8f2086b2b80180d9b3010dd4ed147efc167c90d3bf593fe2af21265e5a5", - "sha256:773efe0603db30c281521a7c0214cad7836c03b8ccff897beae9b47c0b657d61", - "sha256:7823bdd049099efa16e4246bdf15e5a13dbb18a51b68fa06d6c1d4d8b99a796e", - "sha256:7c8f97e8e7a9009bcacbe3766a36175056c12f9a44e6e6f2d5caad06dcfbf03b", - "sha256:823ef7a27cf86df6597fa0671066c1b596f69eba53efa3d1e1cb8b30f3533068", - "sha256:8373c6c251f7ef8bda6675dd6d2b3a0fcc31edf1201266b5cf608b62a37407f9", - "sha256:83b2021f2ade7d1ed556bc50a399127d7fb245e725aa0113ebd05cfe88aaf588", - "sha256:870ea1ada0899fd0b79643990809323b389d4d1d46c192f97342eeb6ee0b8483", - "sha256:8d12251f02d69d8310b046e82572ed486685c38f02176bd08baf216746eb947f", - "sha256:9c23f307202661071d94b5e384e1e1dc7dfb972a28a2310e4ee16103e66ddb67", - "sha256:9d189550615b4948f45252d7f005e53c2040cea1af5b60d6f79491a6e147eef7", - "sha256:a086c2af425c5f62a65e12fbf385f7c9fcb8f107d0849dba5839461a129cf311", - "sha256:a2b56ba36e05f973d450582fb015594aaa78834fefe8dfb8fcd79b93e64ba4c6", - "sha256:aebb6044806f2e16ecc07b2a2637ee1ef67a11840a66752751714a0d924adf72", - "sha256:b1b3020d90c2d8e1dae29cf3ce54f8094f7938460fb5ce8bc5c01450b01fbaf6", - "sha256:b4b6b1e20608493548b1f32bce8cca185bf0480983890403d3b8753e44077129", - "sha256:b6f491cdf80ae540738859d9766783e3b3c8e5bd37f5dfa0b76abdecc5081f13", - "sha256:b792a349405fbc0163190fde0dc7b3fef3c9268292586cf5645598b48e63dc67", - "sha256:b7c2286c23cd350b80d2fc9d424fc797575fb16f854b831d16fd47ceec078f2c", - "sha256:babf5acfede515f176833ed6028754cbcd0d206f7f614ea3447d67c33be12516", - "sha256:c365fd1703040de1ec284b176d6af5abe21b427cb3a5ff68e0759e1e313a5e7e", - "sha256:c4225f5220f46b2fde568c74fca27ae9771536c2e29d7c04f4fb62c83275ac4e", - "sha256:c570f24be1e468e3f0ce7ef56a89a60f0e05b30a3669a459e419c6eac2c35364", - "sha256:c6dafac9e0f2b3c78df97e79af707cdc5ef8e88208d686a4847bab8266870023", - "sha256:c8de2789052ed501dd829e9cae8d3dcce7acb4777ea4a479c14521c942d395b1", - "sha256:cb28c753fd5eb3dd859b4ee95de66cc62af91bcff5db5f2571d32a520baf1f04", - "sha256:cb4c38abeef13c61d6916f264d4845fab99d7b711be96c326b84df9e3e0ff62d", - "sha256:d1b35bcd6c5543b9cb547dee3150c93008f8dd0f1fef78fc0cd2b141c5baf58a", - "sha256:d8e6aeb9201e655354b3ad049cb77d19813ad4ece0df1249d3c793de3774f8c7", - "sha256:d8ecd059fdaf60c1963c58ceb8997b32e9dc1b911f5da5307aab614f1ce5c2fb", - "sha256:da2b52b37dad6d9ec64e653637a096905b258d2fc2b984c41ae7d08b938a67e4", - "sha256:e87f0b2c78157e12d7686b27d63c070fd65d994e8ddae6f328e0dcf4a0cd007e", - "sha256:edca80cbfb2b68d7b56930b84a0e45ae1694aeba0541f798e908a49d66b837f1", - "sha256:f379abd2f1e3dddb2b61bc67977a6b5a0a3f7485538bcc6f39ec76163891ee48", - "sha256:fe4c15f6c9285dc54ce6553a3ce908ed37c8f3825b5a51a15c91442bb955b868" + "sha256:048ad577748b9fa4a99a0548c64f2cb8d672d5bf2e643a739ac8faff1164238c", + "sha256:048eeade4c33fdf7e08da40ef402e748df113fd0b4584e32c4af74fe78baaeb2", + "sha256:0ba26351b137ca4e0db0342d5d00d2e355eb29372c05afd544ebf47c0956ffeb", + "sha256:0ea2a783a2bdf2a561808fe4a7a12e9aa3799b701ba305de596bc48b8bdfce9d", + "sha256:1530e8f3a4b965eb6a7785cf17a426c779333eb62c9a7d1bbcf3ffd5bf77a4aa", + "sha256:16563993329b79513f59142a6b02055e10514c1a8e86dca8b48a893e33cf91e3", + "sha256:19aeb96d43902f0a783946a0a87dbdad5c84c936025b8419da0a0cd7724356b1", + "sha256:1a1d1915db1a4fdb2754b9de292642a39a7fb28f1736699527bb649484fb966a", + "sha256:1b87bd9d81d179bd8ab871603bd80d8645729939f90b71e62914e816a76fc6bd", + "sha256:1dfc94946bc60ea375cc39cff0b8da6c7e5f8fcdc1d946beb8da5c216156ddd8", + "sha256:2034f6759a722da3a3dbd91a81148cf884e91d1b747992ca288ab88c1de15999", + "sha256:261ddb7ca91fcf71757979534fb4c128448b5b4c55cb6152d280312062f69599", + "sha256:2ed854e716a89b1afcedea551cd85f2eb2a807613752ab997b9974aaa0d56936", + "sha256:3102045a10945173d38336f6e71a8dc71bcaeed55c3123ad4af82c52807b9375", + "sha256:339894035d0ede518b16073bdc2feef4c991ee991a29774b33e515f1d308e08d", + "sha256:412444afb8c4c7a6cc11a47dade32982439925537e483be7c0ae0cf96c4f6a0b", + "sha256:4203efca580f0dd6f882ca211f923168548f7ba334c189e9eab1178ab840bf60", + "sha256:45ebc7b45406febf07fef35d856f0293a92e7417ae7933207e90bf9090b70572", + "sha256:4b5ec25d8b17217d635f8935dbc1b9aa5907962fae29dff220f2659487891cd3", + "sha256:4c8e73e99da7db1b4cad7f8d682cf6abad7844da39834c288fbfa394a47bbced", + "sha256:4e6f7d1c414191c1199f8996d3f2282b9ebea0945693fb67392c75a3a320941f", + "sha256:4eaa22f0d22b1a7e93ff0a596d57fdede2e550aecffb5a1ef1106aaece48e96b", + "sha256:50b8eae8f7334ec826d6eeffaeeb00e36b5e24aa0b9df322c247539714c6df19", + "sha256:50fd3f6b26e3441ae07b7c979309638b72abc1a25da31a81a7fbd9495713ef4f", + "sha256:51243f1ed5161b9945011a7360e997729776f6e5d7005ba0c6879267d4c5139d", + "sha256:5d512aafa1d32efa014fa041d38868fda85028e3f930a96f85d49c7d8ddc0383", + "sha256:5f77cf66e96ae734717d341c145c5949c63180842a545c47a0ce7ae52ca83795", + "sha256:6b02471b72526ab8a18c39cb7967b72d194ec53c1fd0a70b050565a0f366d355", + "sha256:6fb1b30043271ec92dc65f6d9f0b7a830c210b8a96423074b15c7bc999975f57", + "sha256:7161ec49ef0800947dc5570f86568a7bb36fa97dd09e9827dc02b718c5643f09", + "sha256:72d622d262e463dfb7595202d229f5f3ab4b852289a1cd09650362db23b9eb0b", + "sha256:74d28c17412d9caa1066f7a31df8403ec23d5268ba46cd0ad2c50fb82ae40462", + "sha256:78618cdbccaa74d3f88d0ad6cb8ac3007f1a6fa5c6f19af64b55ca170bfa1edf", + "sha256:793b4e24db2e8742ca6423d3fde8396db336698c55cd34b660663ee9e45ed37f", + "sha256:798232c92e7665fe82ac085f9d8e8ca98826f8e27859d9a96b41d519ecd2e49a", + "sha256:81d09caa7b27ef4e61cb7d8fbf1714f5aec1c6b6c5270ee53504981e6e9121ad", + "sha256:8ab74c06ffdab957d7670c2a5a6e1a70181cd10b727cd788c4dd9005b6a8acd9", + "sha256:8eb0908e954d093b02a543dc963984d6e99ad2b5e36503d8a0aaf040505f747d", + "sha256:90b9e29824800e90c84e4022dd5cc16eb2d9605ee13f05d47641eb183cd73d45", + "sha256:9797a6c8fe16f25749b371c02e2ade0efb51155e767a971c61734b1bf6293994", + "sha256:9d2455fbf44c914840c793e89aa82d0e1763a14253a000743719ae5946814b2d", + "sha256:9d3bea1c75f8c53ee4d505c3e67d8c158ad4df0d83170605b50b64025917f338", + "sha256:9e2ec1e921fd07c7cda7962bad283acc2f2a9ccc1b971ee4b216b75fad6f0463", + "sha256:9e91179a242bbc99be65e139e30690e081fe6cb91a8e77faf4c409653de39451", + "sha256:a0eaa93d054751ee9964afa21c06247779b90440ca41d184aeb5d410f20ff591", + "sha256:a2c405445c79c3f5a124573a051062300936b0281fee57637e706453e452746c", + "sha256:aa7e402ce11f0885305bfb6afb3434b3cd8f53b563ac065452d9d5654c7b86fd", + "sha256:aff76a55a8aa8364d25400a210a65ff59d0168e0b4285ba6bf2bd83cf675ba32", + "sha256:b09b86b27a064c9624d0a6c54da01c1beaf5b6cadfa609cf63789b1d08a797b9", + "sha256:b14f16f94cbc61215115b9b1236f9c18403c15dd3c52cf629072afa9d54c1cbf", + "sha256:b50811d664d392f02f7761621303eba9d1b056fb1868c8cdf4231279645c25f5", + "sha256:b7bc2176354defba3edc2b9a777744462da2f8e921fbaf61e52acb95bafa9828", + "sha256:c78e1b00a87ce43bb37642c0812315b411e856a905d58d597750eb79802aaaa3", + "sha256:c83341b89884e2b2e55886e8fbbf37c3fa5efd6c8907124aeb72f285ae5696e5", + "sha256:ca2870d5d10d8726a27396d3ca4cf7976cec0f3cb706debe88e3a5bd4610f7d2", + "sha256:ccce24b7ad89adb5a1e34a6ba96ac2530046763912806ad4c247356a8f33a67b", + "sha256:cd5e14fbf22a87321b24c88669aad3a51ec052eb145315b3da3b7e3cc105b9a2", + "sha256:ce49c67f4ea0609933d01c0731b34b8695a7a748d6c8d186f95e7d085d2fe475", + "sha256:d33891be6df59d93df4d846640f0e46f1a807339f09e79a8040bc887bdcd7ed3", + "sha256:d3b2348a78bc939b4fed6552abfd2e7988e0f81443ef3911a4b8498ca084f6eb", + "sha256:d886f5d353333b4771d21267c7ecc75b710f1a73d72d03ca06df49b09015a9ef", + "sha256:d93480005693d247f8346bc8ee28c72a2191bdf1f6b5db469c096c0c867ac015", + "sha256:dc1a390a82755a8c26c9964d457d4c9cbec5405896cba94cf51f36ea0d855002", + "sha256:dd78700f5788ae180b5ee8902c6aea5a5726bac7c364b202b4b3e3ba2d293170", + "sha256:e46f38133e5a060d46bd630faa4d9fa0202377495df1f068a8299fd78c84de84", + "sha256:e4b878386c4bf293578b48fc570b84ecfe477d3b77ba39a6e87150af77f40c57", + "sha256:f0d0591a0aeaefdaf9a5e545e7485f89910c977087e7de2b6c388aec32011e9f", + "sha256:fdcbb4068117dfd9ce0138d068ac512843c52295ed996ae6dd1faf537b6dbc27", + "sha256:ff61bfd9253c3915e6d41c651d5f962da23eda633cf02262990094a18a55371a" ], "markers": "python_version >= '3.8'", - "version": "==10.2.0" + "version": "==10.3.0" }, "pkginfo": { "hashes": [ - "sha256:4b7a555a6d5a22169fcc9cf7bfd78d296b0361adad412a346c1226849af5e546", - "sha256:8fd5896e8718a4372f0ea9cc9d96f6417c9b986e23a4d116dda26b62cc29d046" + "sha256:5df73835398d10db79f8eecd5cd86b1f6d29317589ea70796994d49399af6297", + "sha256:889a6da2ed7ffc58ab5b900d888ddce90bce912f2d2de1dc1c26f4cb9fe65097" ], "markers": "python_version >= '3.6'", - "version": "==1.9.6" + "version": "==1.10.0" }, "platformdirs": { "hashes": [ @@ -1352,6 +1500,57 @@ "markers": "python_version >= '3.8'", "version": "==2.11.1" }, + "pycparser": { + "hashes": [ + "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", + "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc" + ], + "markers": "python_version >= '3.8'", + "version": "==2.22" + }, + "pydantic": { + "hashes": [ + "sha256:0fe8a415cea8f340e7a9af9c54fc71a649b43e8ca3cc732986116b3cb135d303", + "sha256:1289c180abd4bd4555bb927c42ee42abc3aee02b0fb2d1223fb7c6e5bef87dbe", + "sha256:1eb2085c13bce1612da8537b2d90f549c8cbb05c67e8f22854e201bde5d98a47", + "sha256:2031de0967c279df0d8a1c72b4ffc411ecd06bac607a212892757db7462fc494", + "sha256:2a7bac939fa326db1ab741c9d7f44c565a1d1e80908b3797f7f81a4f86bc8d33", + "sha256:2d5a58feb9a39f481eda4d5ca220aa8b9d4f21a41274760b9bc66bfd72595b86", + "sha256:2f9a6fab5f82ada41d56b0602606a5506aab165ca54e52bc4545028382ef1c5d", + "sha256:2fcfb5296d7877af406ba1547dfde9943b1256d8928732267e2653c26938cd9c", + "sha256:549a8e3d81df0a85226963611950b12d2d334f214436a19537b2efed61b7639a", + "sha256:598da88dfa127b666852bef6d0d796573a8cf5009ffd62104094a4fe39599565", + "sha256:5d1197e462e0364906cbc19681605cb7c036f2475c899b6f296104ad42b9f5fb", + "sha256:69328e15cfda2c392da4e713443c7dbffa1505bc9d566e71e55abe14c97ddc62", + "sha256:6a9dfa722316f4acf4460afdf5d41d5246a80e249c7ff475c43a3a1e9d75cf62", + "sha256:6b30bcb8cbfccfcf02acb8f1a261143fab622831d9c0989707e0e659f77a18e0", + "sha256:6c076be61cd0177a8433c0adcb03475baf4ee91edf5a4e550161ad57fc90f523", + "sha256:771735dc43cf8383959dc9b90aa281f0b6092321ca98677c5fb6125a6f56d58d", + "sha256:795e34e6cc065f8f498c89b894a3c6da294a936ee71e644e4bd44de048af1405", + "sha256:87afda5539d5140cb8ba9e8b8c8865cb5b1463924d38490d73d3ccfd80896b3f", + "sha256:8fb2aa3ab3728d950bcc885a2e9eff6c8fc40bc0b7bb434e555c215491bcf48b", + "sha256:a1fcb59f2f355ec350073af41d927bf83a63b50e640f4dbaa01053a28b7a7718", + "sha256:a5e7add47a5b5a40c49b3036d464e3c7802f8ae0d1e66035ea16aa5b7a3923ed", + "sha256:a73f489aebd0c2121ed974054cb2759af8a9f747de120acd2c3394cf84176ccb", + "sha256:ab26038b8375581dc832a63c948f261ae0aa21f1d34c1293469f135fa92972a5", + "sha256:b0d191db0f92dfcb1dec210ca244fdae5cbe918c6050b342d619c09d31eea0cc", + "sha256:b749a43aa51e32839c9d71dc67eb1e4221bb04af1033a32e3923d46f9effa942", + "sha256:b7ccf02d7eb340b216ec33e53a3a629856afe1c6e0ef91d84a4e6f2fb2ca70fe", + "sha256:ba5b2e6fe6ca2b7e013398bc7d7b170e21cce322d266ffcd57cca313e54fb246", + "sha256:ba5c4a8552bff16c61882db58544116d021d0b31ee7c66958d14cf386a5b5350", + "sha256:c79e6a11a07da7374f46970410b41d5e266f7f38f6a17a9c4823db80dadf4303", + "sha256:ca48477862372ac3770969b9d75f1bf66131d386dba79506c46d75e6b48c1e09", + "sha256:dea7adcc33d5d105896401a1f37d56b47d443a2b2605ff8a969a0ed5543f7e33", + "sha256:e0a16d274b588767602b7646fa05af2782576a6cf1022f4ba74cbb4db66f6ca8", + "sha256:e4129b528c6baa99a429f97ce733fff478ec955513630e61b49804b6cf9b224a", + "sha256:e5f805d2d5d0a41633651a73fa4ecdd0b3d7a49de4ec3fadf062fe16501ddbf1", + "sha256:ef6c96b2baa2100ec91a4b428f80d8f28a3c9e53568219b6c298c1125572ebc6", + "sha256:fdbdd1d630195689f325c9ef1a12900524dceb503b00a987663ff4f58669b93d" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==1.10.12" + }, "pydocstyle": { "hashes": [ "sha256:118762d452a49d6b05e194ef344a55822987a462831ade91ec5c06fd2169d019", @@ -1378,11 +1577,49 @@ }, "pyparsing": { "hashes": [ - "sha256:32c7c0b711493c72ff18a981d24f28aaf9c1fb7ed5e9667c9e84e3db623bdbfb", - "sha256:ede28a1a32462f5a9705e07aea48001a08f7cf81a021585011deba701581a0db" + "sha256:a1bac0ce561155ecc3ed78ca94d3c9378656ad4c94c1270de543f621420f94ad", + "sha256:f9db75911801ed778fe61bb643079ff86601aca99fcae6345aa67292038fb742" ], "markers": "python_full_version >= '3.6.8'", - "version": "==3.1.1" + "version": "==3.1.2" + }, + "pyrsistent": { + "hashes": [ + "sha256:0724c506cd8b63c69c7f883cc233aac948c1ea946ea95996ad8b1380c25e1d3f", + "sha256:09848306523a3aba463c4b49493a760e7a6ca52e4826aa100ee99d8d39b7ad1e", + "sha256:0f3b1bcaa1f0629c978b355a7c37acd58907390149b7311b5db1b37648eb6958", + "sha256:21cc459636983764e692b9eba7144cdd54fdec23ccdb1e8ba392a63666c60c34", + "sha256:2e14c95c16211d166f59c6611533d0dacce2e25de0f76e4c140fde250997b3ca", + "sha256:2e2c116cc804d9b09ce9814d17df5edf1df0c624aba3b43bc1ad90411487036d", + "sha256:4021a7f963d88ccd15b523787d18ed5e5269ce57aa4037146a2377ff607ae87d", + "sha256:4c48f78f62ab596c679086084d0dd13254ae4f3d6c72a83ffdf5ebdef8f265a4", + "sha256:4f5c2d012671b7391803263419e31b5c7c21e7c95c8760d7fc35602353dee714", + "sha256:58b8f6366e152092194ae68fefe18b9f0b4f89227dfd86a07770c3d86097aebf", + "sha256:59a89bccd615551391f3237e00006a26bcf98a4d18623a19909a2c48b8e986ee", + "sha256:5cdd7ef1ea7a491ae70d826b6cc64868de09a1d5ff9ef8d574250d0940e275b8", + "sha256:6288b3fa6622ad8a91e6eb759cfc48ff3089e7c17fb1d4c59a919769314af224", + "sha256:6d270ec9dd33cdb13f4d62c95c1a5a50e6b7cdd86302b494217137f760495b9d", + "sha256:79ed12ba79935adaac1664fd7e0e585a22caa539dfc9b7c7c6d5ebf91fb89054", + "sha256:7d29c23bdf6e5438c755b941cef867ec2a4a172ceb9f50553b6ed70d50dfd656", + "sha256:8441cf9616d642c475684d6cf2520dd24812e996ba9af15e606df5f6fd9d04a7", + "sha256:881bbea27bbd32d37eb24dd320a5e745a2a5b092a17f6debc1349252fac85423", + "sha256:8c3aba3e01235221e5b229a6c05f585f344734bd1ad42a8ac51493d74722bbce", + "sha256:a14798c3005ec892bbada26485c2eea3b54109cb2533713e355c806891f63c5e", + "sha256:b14decb628fac50db5e02ee5a35a9c0772d20277824cfe845c8a8b717c15daa3", + "sha256:b318ca24db0f0518630e8b6f3831e9cba78f099ed5c1d65ffe3e023003043ba0", + "sha256:c1beb78af5423b879edaf23c5591ff292cf7c33979734c99aa66d5914ead880f", + "sha256:c55acc4733aad6560a7f5f818466631f07efc001fd023f34a6c203f8b6df0f0b", + "sha256:ca52d1ceae015859d16aded12584c59eb3825f7b50c6cfd621d4231a6cc624ce", + "sha256:cae40a9e3ce178415040a0383f00e8d68b569e97f31928a3a8ad37e3fde6df6a", + "sha256:e78d0c7c1e99a4a45c99143900ea0546025e41bb59ebc10182e947cf1ece9174", + "sha256:ef3992833fbd686ee783590639f4b8343a57f1f75de8633749d984dc0eb16c86", + "sha256:f058a615031eea4ef94ead6456f5ec2026c19fb5bd6bfe86e9665c4158cf802f", + "sha256:f5ac696f02b3fc01a710427585c855f65cd9c640e14f52abe52020722bb4906b", + "sha256:f920385a11207dc372a028b3f1e1038bb244b3ec38d448e6d8e43c6b3ba20e98", + "sha256:fed2c3216a605dc9a6ea50c7e84c82906e3684c4e80d2908208f662a6cbf9022" + ], + "markers": "python_version >= '3.8'", + "version": "==0.20.0" }, "pytest": { "hashes": [ @@ -1395,12 +1632,12 @@ }, "pytest-asyncio": { "hashes": [ - "sha256:2143d9d9375bf372a73260e4114541485e84fca350b0b6b92674ca56ff5f7ea2", - "sha256:b0079dfac14b60cd1ce4691fbfb1748fe939db7d0234b5aba97197d10fbe0fef" + "sha256:68516fdd1018ac57b846c9846b954f0393b26f094764a28c955eabb0536a4e8a", + "sha256:ffe523a89c1c222598c76856e76852b787504ddb72dd5d9b6617ffa8aa2cde5f" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==0.23.4" + "version": "==0.23.6" }, "pytest-cov": { "hashes": [ @@ -1448,19 +1685,19 @@ }, "python-dateutil": { "hashes": [ - "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", - "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" + "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", + "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.8.2" + "version": "==2.9.0.post0" }, "readme-renderer": { "hashes": [ - "sha256:13d039515c1f24de668e2c93f2e877b9dbe6c6c32328b90a40a49d8b2b85f36d", - "sha256:2d55489f83be4992fe4454939d1a051c33edbab778e82761d060c9fc6b308cd1" + "sha256:1818dd28140813509eeed8d62687f7cd4f7bad90d4db586001c5dc09d4fde311", + "sha256:19db308d86ecd60e5affa3b2a98f017af384678c63c88e5d4556a380e674f3f9" ], "markers": "python_version >= '3.8'", - "version": "==42.0" + "version": "==43.0" }, "requests": { "hashes": [ @@ -1488,11 +1725,19 @@ }, "rich": { "hashes": [ - "sha256:5cb5123b5cf9ee70584244246816e9114227e0b98ad9176eede6ad54bf5403fa", - "sha256:6da14c108c4866ee9520bbffa71f6fe3962e193b7da68720583850cd4548e235" + "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222", + "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432" ], "markers": "python_full_version >= '3.7.0'", - "version": "==13.7.0" + "version": "==13.7.1" + }, + "secretstorage": { + "hashes": [ + "sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77", + "sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99" + ], + "markers": "sys_platform == 'linux'", + "version": "==3.3.3" }, "six": { "hashes": [ @@ -1633,12 +1878,12 @@ }, "types-mock": { "hashes": [ - "sha256:13ca379d5710ccb3f18f69ade5b08881874cb83383d8fb49b1d4dac9d5c5d090", - "sha256:3d116955495935b0bcba14954b38d97e507cd43eca3e3700fc1b8e4f5c6bf2c7" + "sha256:0769cb376dfc75b45215619f17a9fd6333d771cc29ce4a38937f060b1e45530f", + "sha256:7472797986d83016f96fde7f73577d129b0cd8a8d0b783487a7be330d57ba431" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==5.1.0.20240106" + "version": "==5.1.0.20240311" }, "types-setuptools": { "hashes": [ @@ -1650,19 +1895,19 @@ }, "typing-extensions": { "hashes": [ - "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783", - "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd" + "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0", + "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a" ], "markers": "python_version >= '3.8'", - "version": "==4.9.0" + "version": "==4.11.0" }, "urllib3": { "hashes": [ - "sha256:051d961ad0c62a94e50ecf1af379c3aba230c66c710493493560c0c223c49f20", - "sha256:ce3711610ddce217e6d113a2732fafad960a03fd0318c91faa79481e35c11224" + "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d", + "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19" ], "markers": "python_version >= '3.8'", - "version": "==2.2.0" + "version": "==2.2.1" }, "wheel": { "hashes": [ @@ -1675,11 +1920,11 @@ }, "zipp": { "hashes": [ - "sha256:0e923e726174922dce09c53c59ad483ff7bbb8e572e00c7f7c46b88556409f31", - "sha256:84e64a1c28cf7e91ed2078bb8cc8c259cb19b76942096c8d7b84947690cabaf0" + "sha256:206f5a15f2af3dbaee80769fb7dc6f249695e940acca08dfb2a4769fe61e538b", + "sha256:2884ed22e7d8961de1c9a05142eb69a247f120291bc0206a00a7642f09b5b715" ], "markers": "python_version >= '3.8'", - "version": "==3.17.0" + "version": "==3.18.1" } } } diff --git a/api/src/opentrons/cli/analyze.py b/api/src/opentrons/cli/analyze.py index a42a4f5f868..42ca29a2b81 100644 --- a/api/src/opentrons/cli/analyze.py +++ b/api/src/opentrons/cli/analyze.py @@ -28,6 +28,7 @@ ) from opentrons_shared_data.robot.dev_types import RobotType +from opentrons.util.performance_helpers import track_analysis @click.command() @@ -63,6 +64,7 @@ def _get_input_files(files_and_dirs: Sequence[Path]) -> List[Path]: return results +@track_analysis async def _analyze( files_and_dirs: Sequence[Path], json_output: Optional[AsyncPath], diff --git a/api/src/opentrons/config/__init__.py b/api/src/opentrons/config/__init__.py index ce867677777..a4571521211 100644 --- a/api/src/opentrons/config/__init__.py +++ b/api/src/opentrons/config/__init__.py @@ -284,6 +284,13 @@ class ConfigElement(NamedTuple): ConfigElementType.DIR, "The dir where module calibration is stored", ), + ConfigElement( + "performance_metrics_dir", + "Performance Metrics Directory", + Path("performance_metrics_data"), + ConfigElementType.DIR, + "The dir where performance metrics are stored", + ), ) #: The available configuration file elements to modify. All of these can be #: changed by editing opentrons.json, where the keys are the name elements, @@ -602,3 +609,7 @@ def get_tip_length_cal_path() -> Path: def get_custom_tiprack_def_path() -> Path: return get_opentrons_path("custom_tiprack_dir") + + +def get_performance_metrics_data_dir() -> Path: + return get_opentrons_path("performance_metrics_dir") diff --git a/api/src/opentrons/util/performance_helpers.py b/api/src/opentrons/util/performance_helpers.py new file mode 100644 index 00000000000..ddd547e2ce7 --- /dev/null +++ b/api/src/opentrons/util/performance_helpers.py @@ -0,0 +1,76 @@ +"""Performance helpers for tracking robot context.""" + +from pathlib import Path +from opentrons_shared_data.performance.dev_types import ( + SupportsTracking, + F, + RobotContextState, +) +from opentrons_shared_data.robot.dev_types import RobotTypeEnum +from typing import Callable, Type +from opentrons.config import ( + feature_flags as ff, + get_performance_metrics_data_dir, + robot_configs, +) + + +_should_track = ff.enable_performance_metrics( + RobotTypeEnum.robot_literal_to_enum(robot_configs.load().model) +) + + +def _handle_package_import() -> Type[SupportsTracking]: + """Handle the import of the performance_metrics package. + + If the package is not available, return a stubbed tracker. + """ + try: + from performance_metrics import RobotContextTracker + + return RobotContextTracker + except ImportError: + return StubbedTracker + + +package_to_use = _handle_package_import() +_robot_context_tracker: SupportsTracking | None = None + + +class StubbedTracker(SupportsTracking): + """A stubbed tracker that does nothing.""" + + def __init__(self, storage_location: Path, should_track: bool) -> None: + """Initialize the stubbed tracker.""" + pass + + def track(self, state: RobotContextState) -> Callable[[F], F]: + """Return the function unchanged.""" + + def inner_decorator(func: F) -> F: + """Return the function unchanged.""" + return func + + return inner_decorator + + def store(self) -> None: + """Do nothing.""" + pass + + +def _get_robot_context_tracker() -> SupportsTracking: + """Singleton for the robot context tracker.""" + global _robot_context_tracker + if _robot_context_tracker is None: + # TODO: replace with path lookup and should_store lookup + _robot_context_tracker = package_to_use( + get_performance_metrics_data_dir(), _should_track + ) + return _robot_context_tracker + + +def track_analysis(func: F) -> F: + """Track the analysis of a protocol.""" + return _get_robot_context_tracker().track(RobotContextState.ANALYZING_PROTOCOL)( + func + ) diff --git a/api/tests/opentrons/cli/test_cli.py b/api/tests/opentrons/cli/test_cli.py index eae5aa31ccc..007a7dd6a03 100644 --- a/api/tests/opentrons/cli/test_cli.py +++ b/api/tests/opentrons/cli/test_cli.py @@ -1,4 +1,6 @@ """Test cli execution.""" + + import json import tempfile import textwrap @@ -9,8 +11,17 @@ import pytest from click.testing import CliRunner +from opentrons.util.performance_helpers import _get_robot_context_tracker + -from opentrons.cli.analyze import analyze +# Enable tracking for the RobotContextTracker +# This must come before the import of the analyze CLI +context_tracker = _get_robot_context_tracker() + +# Ignore the type error for the next line, as we're setting a private attribute for testing purposes +context_tracker._should_track = True # type: ignore[attr-defined] + +from opentrons.cli.analyze import analyze # noqa: E402 def _list_fixtures(version: int) -> Iterator[Path]: @@ -242,3 +253,24 @@ def test_python_error_line_numbers( assert result.json_output is not None [error] = result.json_output["errors"] assert error["detail"] == expected_detail + + +def test_track_analysis(tmp_path: Path) -> None: + """Test that the RobotContextTracker tracks analysis.""" + protocol_source = textwrap.dedent( + """ + requirements = {"apiLevel": "2.15"} + + def run(protocol): + pass + """ + ) + + protocol_source_file = tmp_path / "protocol.py" + protocol_source_file.write_text(protocol_source, encoding="utf-8") + + before_analysis = len(context_tracker._storage) # type: ignore[attr-defined] + + _get_analysis_result([protocol_source_file]) + + assert len(context_tracker._storage) == before_analysis + 1 # type: ignore[attr-defined] diff --git a/api/tests/opentrons/util/test_performance_helpers.py b/api/tests/opentrons/util/test_performance_helpers.py new file mode 100644 index 00000000000..57a42ef6a71 --- /dev/null +++ b/api/tests/opentrons/util/test_performance_helpers.py @@ -0,0 +1,28 @@ +"""Tests for performance_helpers.""" + +from pathlib import Path +from opentrons_shared_data.performance.dev_types import RobotContextState +from opentrons.util.performance_helpers import ( + StubbedTracker, + _get_robot_context_tracker, +) + + +def test_return_function_unchanged() -> None: + """Test that the function is returned unchanged when using StubbedTracker.""" + tracker = StubbedTracker(Path("/path/to/storage"), True) + + def func_to_track() -> None: + pass + + assert ( + tracker.track(RobotContextState.ANALYZING_PROTOCOL)(func_to_track) + is func_to_track + ) + + +def test_singleton_tracker() -> None: + """Test that the tracker is a singleton.""" + tracker = _get_robot_context_tracker() + tracker2 = _get_robot_context_tracker() + assert tracker is tracker2 diff --git a/performance-metrics/src/performance_metrics/__init__.py b/performance-metrics/src/performance_metrics/__init__.py index a92b39b6d7b..b5f2e760c19 100644 --- a/performance-metrics/src/performance_metrics/__init__.py +++ b/performance-metrics/src/performance_metrics/__init__.py @@ -1 +1,5 @@ """Opentrons performance metrics library.""" + +from .robot_context_tracker import RobotContextTracker + +__all__ = ["RobotContextTracker"] diff --git a/performance-metrics/src/performance_metrics/datashapes.py b/performance-metrics/src/performance_metrics/datashapes.py index 81b0234a723..7743ed1723d 100644 --- a/performance-metrics/src/performance_metrics/datashapes.py +++ b/performance-metrics/src/performance_metrics/datashapes.py @@ -1,40 +1,8 @@ """Defines data classes and enums used in the performance metrics module.""" -from enum import Enum import dataclasses from typing import Tuple - - -class RobotContextState(Enum): - """Enum representing different states of a robot's operation context.""" - - STARTING_UP = 0, "STARTING_UP" - CALIBRATING = 1, "CALIBRATING" - ANALYZING_PROTOCOL = 2, "ANALYZING_PROTOCOL" - RUNNING_PROTOCOL = 3, "RUNNING_PROTOCOL" - SHUTTING_DOWN = 4, "SHUTTING_DOWN" - - def __init__(self, state_id: int, state_name: str) -> None: - self.state_id = state_id - self.state_name = state_name - - @classmethod - def from_id(cls, state_id: int) -> "RobotContextState": - """Returns the enum member matching the given state ID. - - Args: - state_id: The ID of the state to retrieve. - - Returns: - RobotContextStates: The enum member corresponding to the given ID. - - Raises: - ValueError: If no matching state is found. - """ - for state in RobotContextState: - if state.state_id == state_id: - return state - raise ValueError(f"Invalid state id: {state_id}") +from opentrons_shared_data.performance.dev_types import RobotContextState @dataclasses.dataclass(frozen=True) diff --git a/performance-metrics/src/performance_metrics/robot_context_tracker.py b/performance-metrics/src/performance_metrics/robot_context_tracker.py index 606be71e649..99dc502c9ad 100644 --- a/performance-metrics/src/performance_metrics/robot_context_tracker.py +++ b/performance-metrics/src/performance_metrics/robot_context_tracker.py @@ -12,7 +12,13 @@ from typing_extensions import ParamSpec from collections import deque -from performance_metrics.datashapes import RawContextData, RobotContextState +from performance_metrics.datashapes import ( + RawContextData, +) +from opentrons_shared_data.performance.dev_types import ( + RobotContextState, + SupportsTracking, +) P = ParamSpec("P") R = TypeVar("R") @@ -38,13 +44,15 @@ def _get_timing_function() -> Callable[[], int]: timing_function = _get_timing_function() -class RobotContextTracker: +class RobotContextTracker(SupportsTracking): """Tracks and stores robot context and execution duration for different operations.""" - def __init__(self, storage_file_path: Path, should_track: bool = False) -> None: + FILE_NAME = "context_data.csv" + + def __init__(self, storage_location: Path, should_track: bool = False) -> None: """Initializes the RobotContextTracker with an empty storage list.""" self._storage: deque[RawContextData] = deque() - self._storage_file_path = storage_file_path + self._storage_file_path = storage_location / self.FILE_NAME self._should_track = should_track def track(self, state: RobotContextState) -> Callable: # type: ignore diff --git a/performance-metrics/tests/performance_metrics/test_robot_context_tracker.py b/performance-metrics/tests/performance_metrics/test_robot_context_tracker.py index 5345004eb44..2c112410063 100644 --- a/performance-metrics/tests/performance_metrics/test_robot_context_tracker.py +++ b/performance-metrics/tests/performance_metrics/test_robot_context_tracker.py @@ -4,7 +4,7 @@ from pathlib import Path import pytest from performance_metrics.robot_context_tracker import RobotContextTracker -from performance_metrics.datashapes import RobotContextState +from opentrons_shared_data.performance.dev_types import RobotContextState from time import sleep, time_ns from unittest.mock import patch @@ -19,7 +19,7 @@ @pytest.fixture def robot_context_tracker(tmp_path: Path) -> RobotContextTracker: """Fixture to provide a fresh instance of RobotContextTracker for each test.""" - return RobotContextTracker(storage_file_path=tmp_path, should_track=True) + return RobotContextTracker(storage_location=tmp_path, should_track=True) def test_robot_context_tracker(robot_context_tracker: RobotContextTracker) -> None: @@ -242,8 +242,7 @@ def operation_without_tracking() -> None: async def test_storing_to_file(tmp_path: Path) -> None: """Tests storing the tracked data to a file.""" - file_path = tmp_path / "test_file.csv" - robot_context_tracker = RobotContextTracker(file_path, should_track=True) + robot_context_tracker = RobotContextTracker(tmp_path, should_track=True) @robot_context_tracker.track(state=RobotContextState.STARTING_UP) def starting_robot() -> None: @@ -263,7 +262,7 @@ def analyzing_protocol() -> None: robot_context_tracker.store() - with open(file_path, "r") as file: + with open(robot_context_tracker._storage_file_path, "r") as file: lines = file.readlines() assert ( len(lines) == 4 diff --git a/robot-server/Pipfile b/robot-server/Pipfile index 9461d736de2..2d22c6dc34c 100755 --- a/robot-server/Pipfile +++ b/robot-server/Pipfile @@ -36,6 +36,7 @@ sqlalchemy2-stubs = "==0.0.2a21" # limited by tavern python-box = "==6.1.0" types-paho-mqtt = "==1.6.0.20240106" +performance-metrics = {file = "../performance-metrics", editable = true} [packages] anyio = "==3.7.1" diff --git a/robot-server/Pipfile.lock b/robot-server/Pipfile.lock index 6306e3dfb27..2ea9f545696 100644 --- a/robot-server/Pipfile.lock +++ b/robot-server/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "d56512f7ae8f68fd80ec6eff41af08576468087a45578f5b2c8241e42d95b887" + "sha256": "9f64ba7d87b9c9fd510aac5c4a22fa748c1bb3b9936826ef2b4b13454c1c5e2b" }, "pipfile-spec": 6, "requires": { @@ -252,6 +252,70 @@ "markers": "python_version >= '3.8'", "version": "==1.4.1" }, + "greenlet": { + "hashes": [ + "sha256:01bc7ea167cf943b4c802068e178bbf70ae2e8c080467070d01bfa02f337ee67", + "sha256:0448abc479fab28b00cb472d278828b3ccca164531daab4e970a0458786055d6", + "sha256:086152f8fbc5955df88382e8a75984e2bb1c892ad2e3c80a2508954e52295257", + "sha256:098d86f528c855ead3479afe84b49242e174ed262456c342d70fc7f972bc13c4", + "sha256:149e94a2dd82d19838fe4b2259f1b6b9957d5ba1b25640d2380bea9c5df37676", + "sha256:1551a8195c0d4a68fac7a4325efac0d541b48def35feb49d803674ac32582f61", + "sha256:15d79dd26056573940fcb8c7413d84118086f2ec1a8acdfa854631084393efcc", + "sha256:1996cb9306c8595335bb157d133daf5cf9f693ef413e7673cb07e3e5871379ca", + "sha256:1a7191e42732df52cb5f39d3527217e7ab73cae2cb3694d241e18f53d84ea9a7", + "sha256:1ea188d4f49089fc6fb283845ab18a2518d279c7cd9da1065d7a84e991748728", + "sha256:1f672519db1796ca0d8753f9e78ec02355e862d0998193038c7073045899f305", + "sha256:2516a9957eed41dd8f1ec0c604f1cdc86758b587d964668b5b196a9db5bfcde6", + "sha256:2797aa5aedac23af156bbb5a6aa2cd3427ada2972c828244eb7d1b9255846379", + "sha256:2dd6e660effd852586b6a8478a1d244b8dc90ab5b1321751d2ea15deb49ed414", + "sha256:3ddc0f794e6ad661e321caa8d2f0a55ce01213c74722587256fb6566049a8b04", + "sha256:3ed7fb269f15dc662787f4119ec300ad0702fa1b19d2135a37c2c4de6fadfd4a", + "sha256:419b386f84949bf0e7c73e6032e3457b82a787c1ab4a0e43732898a761cc9dbf", + "sha256:43374442353259554ce33599da8b692d5aa96f8976d567d4badf263371fbe491", + "sha256:52f59dd9c96ad2fc0d5724107444f76eb20aaccb675bf825df6435acb7703559", + "sha256:57e8974f23e47dac22b83436bdcf23080ade568ce77df33159e019d161ce1d1e", + "sha256:5b51e85cb5ceda94e79d019ed36b35386e8c37d22f07d6a751cb659b180d5274", + "sha256:649dde7de1a5eceb258f9cb00bdf50e978c9db1b996964cd80703614c86495eb", + "sha256:64d7675ad83578e3fc149b617a444fab8efdafc9385471f868eb5ff83e446b8b", + "sha256:68834da854554926fbedd38c76e60c4a2e3198c6fbed520b106a8986445caaf9", + "sha256:6b66c9c1e7ccabad3a7d037b2bcb740122a7b17a53734b7d72a344ce39882a1b", + "sha256:70fb482fdf2c707765ab5f0b6655e9cfcf3780d8d87355a063547b41177599be", + "sha256:7170375bcc99f1a2fbd9c306f5be8764eaf3ac6b5cb968862cad4c7057756506", + "sha256:73a411ef564e0e097dbe7e866bb2dda0f027e072b04da387282b02c308807405", + "sha256:77457465d89b8263bca14759d7c1684df840b6811b2499838cc5b040a8b5b113", + "sha256:7f362975f2d179f9e26928c5b517524e89dd48530a0202570d55ad6ca5d8a56f", + "sha256:81bb9c6d52e8321f09c3d165b2a78c680506d9af285bfccbad9fb7ad5a5da3e5", + "sha256:881b7db1ebff4ba09aaaeae6aa491daeb226c8150fc20e836ad00041bcb11230", + "sha256:894393ce10ceac937e56ec00bb71c4c2f8209ad516e96033e4b3b1de270e200d", + "sha256:99bf650dc5d69546e076f413a87481ee1d2d09aaaaaca058c9251b6d8c14783f", + "sha256:9da2bd29ed9e4f15955dd1595ad7bc9320308a3b766ef7f837e23ad4b4aac31a", + "sha256:afaff6cf5200befd5cec055b07d1c0a5a06c040fe5ad148abcd11ba6ab9b114e", + "sha256:b1b5667cced97081bf57b8fa1d6bfca67814b0afd38208d52538316e9422fc61", + "sha256:b37eef18ea55f2ffd8f00ff8fe7c8d3818abd3e25fb73fae2ca3b672e333a7a6", + "sha256:b542be2440edc2d48547b5923c408cbe0fc94afb9f18741faa6ae970dbcb9b6d", + "sha256:b7dcbe92cc99f08c8dd11f930de4d99ef756c3591a5377d1d9cd7dd5e896da71", + "sha256:b7f009caad047246ed379e1c4dbcb8b020f0a390667ea74d2387be2998f58a22", + "sha256:bba5387a6975598857d86de9eac14210a49d554a77eb8261cc68b7d082f78ce2", + "sha256:c5e1536de2aad7bf62e27baf79225d0d64360d4168cf2e6becb91baf1ed074f3", + "sha256:c5ee858cfe08f34712f548c3c363e807e7186f03ad7a5039ebadb29e8c6be067", + "sha256:c9db1c18f0eaad2f804728c67d6c610778456e3e1cc4ab4bbd5eeb8e6053c6fc", + "sha256:d353cadd6083fdb056bb46ed07e4340b0869c305c8ca54ef9da3421acbdf6881", + "sha256:d46677c85c5ba00a9cb6f7a00b2bfa6f812192d2c9f7d9c4f6a55b60216712f3", + "sha256:d4d1ac74f5c0c0524e4a24335350edad7e5f03b9532da7ea4d3c54d527784f2e", + "sha256:d73a9fe764d77f87f8ec26a0c85144d6a951a6c438dfe50487df5595c6373eac", + "sha256:da70d4d51c8b306bb7a031d5cff6cc25ad253affe89b70352af5f1cb68e74b53", + "sha256:daf3cb43b7cf2ba96d614252ce1684c1bccee6b2183a01328c98d36fcd7d5cb0", + "sha256:dca1e2f3ca00b84a396bc1bce13dd21f680f035314d2379c4160c98153b2059b", + "sha256:dd4f49ae60e10adbc94b45c0b5e6a179acc1736cf7a90160b404076ee283cf83", + "sha256:e1f145462f1fa6e4a4ae3c0f782e580ce44d57c8f2c7aae1b6fa88c0b2efdb41", + "sha256:e3391d1e16e2a5a1507d83e4a8b100f4ee626e8eca43cf2cadb543de69827c4c", + "sha256:fcd2469d6a2cf298f198f0487e0a5b1a47a42ca0fa4dfd1b6862c999f018ebbf", + "sha256:fd096eb7ffef17c456cfa587523c5f92321ae02427ff955bebe9e3c63bc9f0da", + "sha256:fe754d231288e1e64323cfad462fcee8f0288654c10bdf4f603a39ed923bef33" + ], + "markers": "python_version >= '3' and platform_machine == 'aarch64' or (platform_machine == 'ppc64le' or (platform_machine == 'x86_64' or (platform_machine == 'amd64' or (platform_machine == 'AMD64' or (platform_machine == 'win32' or platform_machine == 'WIN32')))))", + "version": "==3.0.3" + }, "h11": { "hashes": [ "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", @@ -279,65 +343,65 @@ }, "msgpack": { "hashes": [ - "sha256:04ad6069c86e531682f9e1e71b71c1c3937d6014a7c3e9edd2aa81ad58842862", - "sha256:0bfdd914e55e0d2c9e1526de210f6fe8ffe9705f2b1dfcc4aecc92a4cb4b533d", - "sha256:1dc93e8e4653bdb5910aed79f11e165c85732067614f180f70534f056da97db3", - "sha256:1e2d69948e4132813b8d1131f29f9101bc2c915f26089a6d632001a5c1349672", - "sha256:235a31ec7db685f5c82233bddf9858748b89b8119bf4538d514536c485c15fe0", - "sha256:27dcd6f46a21c18fa5e5deed92a43d4554e3df8d8ca5a47bf0615d6a5f39dbc9", - "sha256:28efb066cde83c479dfe5a48141a53bc7e5f13f785b92ddde336c716663039ee", - "sha256:3476fae43db72bd11f29a5147ae2f3cb22e2f1a91d575ef130d2bf49afd21c46", - "sha256:36e17c4592231a7dbd2ed09027823ab295d2791b3b1efb2aee874b10548b7524", - "sha256:384d779f0d6f1b110eae74cb0659d9aa6ff35aaf547b3955abf2ab4c901c4819", - "sha256:38949d30b11ae5f95c3c91917ee7a6b239f5ec276f271f28638dec9156f82cfc", - "sha256:3967e4ad1aa9da62fd53e346ed17d7b2e922cba5ab93bdd46febcac39be636fc", - "sha256:3e7bf4442b310ff154b7bb9d81eb2c016b7d597e364f97d72b1acc3817a0fdc1", - "sha256:3f0c8c6dfa6605ab8ff0611995ee30d4f9fcff89966cf562733b4008a3d60d82", - "sha256:484ae3240666ad34cfa31eea7b8c6cd2f1fdaae21d73ce2974211df099a95d81", - "sha256:4a7b4f35de6a304b5533c238bee86b670b75b03d31b7797929caa7a624b5dda6", - "sha256:4cb14ce54d9b857be9591ac364cb08dc2d6a5c4318c1182cb1d02274029d590d", - "sha256:4e71bc4416de195d6e9b4ee93ad3f2f6b2ce11d042b4d7a7ee00bbe0358bd0c2", - "sha256:52700dc63a4676669b341ba33520f4d6e43d3ca58d422e22ba66d1736b0a6e4c", - "sha256:572efc93db7a4d27e404501975ca6d2d9775705c2d922390d878fcf768d92c87", - "sha256:576eb384292b139821c41995523654ad82d1916da6a60cff129c715a6223ea84", - "sha256:5b0bf0effb196ed76b7ad883848143427a73c355ae8e569fa538365064188b8e", - "sha256:5b6ccc0c85916998d788b295765ea0e9cb9aac7e4a8ed71d12e7d8ac31c23c95", - "sha256:5ed82f5a7af3697b1c4786053736f24a0efd0a1b8a130d4c7bfee4b9ded0f08f", - "sha256:6d4c80667de2e36970ebf74f42d1088cc9ee7ef5f4e8c35eee1b40eafd33ca5b", - "sha256:730076207cb816138cf1af7f7237b208340a2c5e749707457d70705715c93b93", - "sha256:7687e22a31e976a0e7fc99c2f4d11ca45eff652a81eb8c8085e9609298916dcf", - "sha256:822ea70dc4018c7e6223f13affd1c5c30c0f5c12ac1f96cd8e9949acddb48a61", - "sha256:84b0daf226913133f899ea9b30618722d45feffa67e4fe867b0b5ae83a34060c", - "sha256:85765fdf4b27eb5086f05ac0491090fc76f4f2b28e09d9350c31aac25a5aaff8", - "sha256:8dd178c4c80706546702c59529ffc005681bd6dc2ea234c450661b205445a34d", - "sha256:8f5b234f567cf76ee489502ceb7165c2a5cecec081db2b37e35332b537f8157c", - "sha256:98bbd754a422a0b123c66a4c341de0474cad4a5c10c164ceed6ea090f3563db4", - "sha256:993584fc821c58d5993521bfdcd31a4adf025c7d745bbd4d12ccfecf695af5ba", - "sha256:a40821a89dc373d6427e2b44b572efc36a2778d3f543299e2f24eb1a5de65415", - "sha256:b291f0ee7961a597cbbcc77709374087fa2a9afe7bdb6a40dbbd9b127e79afee", - "sha256:b573a43ef7c368ba4ea06050a957c2a7550f729c31f11dd616d2ac4aba99888d", - "sha256:b610ff0f24e9f11c9ae653c67ff8cc03c075131401b3e5ef4b82570d1728f8a9", - "sha256:bdf38ba2d393c7911ae989c3bbba510ebbcdf4ecbdbfec36272abe350c454075", - "sha256:bfef2bb6ef068827bbd021017a107194956918ab43ce4d6dc945ffa13efbc25f", - "sha256:cab3db8bab4b7e635c1c97270d7a4b2a90c070b33cbc00c99ef3f9be03d3e1f7", - "sha256:cb70766519500281815dfd7a87d3a178acf7ce95390544b8c90587d76b227681", - "sha256:cca1b62fe70d761a282496b96a5e51c44c213e410a964bdffe0928e611368329", - "sha256:ccf9a39706b604d884d2cb1e27fe973bc55f2890c52f38df742bc1d79ab9f5e1", - "sha256:dc43f1ec66eb8440567186ae2f8c447d91e0372d793dfe8c222aec857b81a8cf", - "sha256:dd632777ff3beaaf629f1ab4396caf7ba0bdd075d948a69460d13d44357aca4c", - "sha256:e45ae4927759289c30ccba8d9fdce62bb414977ba158286b5ddaf8df2cddb5c5", - "sha256:e50ebce52f41370707f1e21a59514e3375e3edd6e1832f5e5235237db933c98b", - "sha256:ebbbba226f0a108a7366bf4b59bf0f30a12fd5e75100c630267d94d7f0ad20e5", - "sha256:ec79ff6159dffcc30853b2ad612ed572af86c92b5168aa3fc01a67b0fa40665e", - "sha256:f0936e08e0003f66bfd97e74ee530427707297b0d0361247e9b4f59ab78ddc8b", - "sha256:f26a07a6e877c76a88e3cecac8531908d980d3d5067ff69213653649ec0f60ad", - "sha256:f64e376cd20d3f030190e8c32e1c64582eba56ac6dc7d5b0b49a9d44021b52fd", - "sha256:f6ffbc252eb0d229aeb2f9ad051200668fc3a9aaa8994e49f0cb2ffe2b7867e7", - "sha256:f9a7c509542db4eceed3dcf21ee5267ab565a83555c9b88a8109dcecc4709002", - "sha256:ff1d0899f104f3921d94579a5638847f783c9b04f2d5f229392ca77fba5b82fc" + "sha256:00e073efcba9ea99db5acef3959efa45b52bc67b61b00823d2a1a6944bf45982", + "sha256:0726c282d188e204281ebd8de31724b7d749adebc086873a59efb8cf7ae27df3", + "sha256:0ceea77719d45c839fd73abcb190b8390412a890df2f83fb8cf49b2a4b5c2f40", + "sha256:114be227f5213ef8b215c22dde19532f5da9652e56e8ce969bf0a26d7c419fee", + "sha256:13577ec9e247f8741c84d06b9ece5f654920d8365a4b636ce0e44f15e07ec693", + "sha256:1876b0b653a808fcd50123b953af170c535027bf1d053b59790eebb0aeb38950", + "sha256:1ab0bbcd4d1f7b6991ee7c753655b481c50084294218de69365f8f1970d4c151", + "sha256:1cce488457370ffd1f953846f82323cb6b2ad2190987cd4d70b2713e17268d24", + "sha256:26ee97a8261e6e35885c2ecd2fd4a6d38252246f94a2aec23665a4e66d066305", + "sha256:3528807cbbb7f315bb81959d5961855e7ba52aa60a3097151cb21956fbc7502b", + "sha256:374a8e88ddab84b9ada695d255679fb99c53513c0a51778796fcf0944d6c789c", + "sha256:376081f471a2ef24828b83a641a02c575d6103a3ad7fd7dade5486cad10ea659", + "sha256:3923a1778f7e5ef31865893fdca12a8d7dc03a44b33e2a5f3295416314c09f5d", + "sha256:4916727e31c28be8beaf11cf117d6f6f188dcc36daae4e851fee88646f5b6b18", + "sha256:493c5c5e44b06d6c9268ce21b302c9ca055c1fd3484c25ba41d34476c76ee746", + "sha256:505fe3d03856ac7d215dbe005414bc28505d26f0c128906037e66d98c4e95868", + "sha256:5845fdf5e5d5b78a49b826fcdc0eb2e2aa7191980e3d2cfd2a30303a74f212e2", + "sha256:5c330eace3dd100bdb54b5653b966de7f51c26ec4a7d4e87132d9b4f738220ba", + "sha256:5dbf059fb4b7c240c873c1245ee112505be27497e90f7c6591261c7d3c3a8228", + "sha256:5e390971d082dba073c05dbd56322427d3280b7cc8b53484c9377adfbae67dc2", + "sha256:5fbb160554e319f7b22ecf530a80a3ff496d38e8e07ae763b9e82fadfe96f273", + "sha256:64d0fcd436c5683fdd7c907eeae5e2cbb5eb872fafbc03a43609d7941840995c", + "sha256:69284049d07fce531c17404fcba2bb1df472bc2dcdac642ae71a2d079d950653", + "sha256:6a0e76621f6e1f908ae52860bdcb58e1ca85231a9b0545e64509c931dd34275a", + "sha256:73ee792784d48aa338bba28063e19a27e8d989344f34aad14ea6e1b9bd83f596", + "sha256:74398a4cf19de42e1498368c36eed45d9528f5fd0155241e82c4082b7e16cffd", + "sha256:7938111ed1358f536daf311be244f34df7bf3cdedb3ed883787aca97778b28d8", + "sha256:82d92c773fbc6942a7a8b520d22c11cfc8fd83bba86116bfcf962c2f5c2ecdaa", + "sha256:83b5c044f3eff2a6534768ccfd50425939e7a8b5cf9a7261c385de1e20dcfc85", + "sha256:8db8e423192303ed77cff4dce3a4b88dbfaf43979d280181558af5e2c3c71afc", + "sha256:9517004e21664f2b5a5fd6333b0731b9cf0817403a941b393d89a2f1dc2bd836", + "sha256:95c02b0e27e706e48d0e5426d1710ca78e0f0628d6e89d5b5a5b91a5f12274f3", + "sha256:99881222f4a8c2f641f25703963a5cefb076adffd959e0558dc9f803a52d6a58", + "sha256:9ee32dcb8e531adae1f1ca568822e9b3a738369b3b686d1477cbc643c4a9c128", + "sha256:a22e47578b30a3e199ab067a4d43d790249b3c0587d9a771921f86250c8435db", + "sha256:b5505774ea2a73a86ea176e8a9a4a7c8bf5d521050f0f6f8426afe798689243f", + "sha256:bd739c9251d01e0279ce729e37b39d49a08c0420d3fee7f2a4968c0576678f77", + "sha256:d16a786905034e7e34098634b184a7d81f91d4c3d246edc6bd7aefb2fd8ea6ad", + "sha256:d3420522057ebab1728b21ad473aa950026d07cb09da41103f8e597dfbfaeb13", + "sha256:d56fd9f1f1cdc8227d7b7918f55091349741904d9520c65f0139a9755952c9e8", + "sha256:d661dc4785affa9d0edfdd1e59ec056a58b3dbb9f196fa43587f3ddac654ac7b", + "sha256:dfe1f0f0ed5785c187144c46a292b8c34c1295c01da12e10ccddfc16def4448a", + "sha256:e1dd7839443592d00e96db831eddb4111a2a81a46b028f0facd60a09ebbdd543", + "sha256:e2872993e209f7ed04d963e4b4fbae72d034844ec66bc4ca403329db2074377b", + "sha256:e2f879ab92ce502a1e65fce390eab619774dda6a6ff719718069ac94084098ce", + "sha256:e3aa7e51d738e0ec0afbed661261513b38b3014754c9459508399baf14ae0c9d", + "sha256:e532dbd6ddfe13946de050d7474e3f5fb6ec774fbb1a188aaf469b08cf04189a", + "sha256:e6b7842518a63a9f17107eb176320960ec095a8ee3b4420b5f688e24bf50c53c", + "sha256:e75753aeda0ddc4c28dce4c32ba2f6ec30b1b02f6c0b14e547841ba5b24f753f", + "sha256:eadb9f826c138e6cf3c49d6f8de88225a3c0ab181a9b4ba792e006e5292d150e", + "sha256:ed59dd52075f8fc91da6053b12e8c89e37aa043f8986efd89e61fae69dc1b011", + "sha256:ef254a06bcea461e65ff0373d8a0dd1ed3aa004af48839f002a0c994a6f72d04", + "sha256:f3709997b228685fe53e8c433e2df9f0cdb5f4542bd5114ed17ac3c0129b0480", + "sha256:f51bab98d52739c50c56658cc303f190785f9a2cd97b823357e7aeae54c8f68a", + "sha256:f9904e24646570539a8950400602d66d2b2c492b9010ea7e965025cb71d0c86d", + "sha256:f9af38a89b6a5c04b7d18c492c8ccf2aee7048aff1ce8437c4683bb5a1df893d" ], "markers": "platform_system != 'Windows'", - "version": "==1.0.7" + "version": "==1.0.8" }, "multidict": { "hashes": [ @@ -464,6 +528,7 @@ }, "opentrons": { "editable": true, + "markers": "python_version >= '3.8'", "path": "../api" }, "opentrons-hardware": { @@ -475,15 +540,16 @@ }, "opentrons-shared-data": { "editable": true, + "markers": "python_version >= '3.8'", "path": "../shared-data/python" }, "packaging": { "hashes": [ - "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5", - "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7" + "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5", + "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9" ], "markers": "python_version >= '3.7'", - "version": "==23.2" + "version": "==24.0" }, "paho-mqtt": { "hashes": [ @@ -615,19 +681,19 @@ }, "setuptools": { "hashes": [ - "sha256:385eb4edd9c9d5c17540511303e39a147ce2fc04bc55289c322b9e5904fe2c05", - "sha256:be1af57fc409f93647f2e8e4573a142ed38724b8cdd389706a867bb4efcf1e78" + "sha256:6c1fccdac05a97e598fb0ae3bbed5904ccb317337a51139dcd51453611bbb987", + "sha256:c636ac361bc47580504644275c9ad802c50415c7522212252c033bd15f301f32" ], "markers": "python_version >= '3.8'", - "version": "==69.0.3" + "version": "==69.5.1" }, "sniffio": { "hashes": [ - "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101", - "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384" + "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", + "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc" ], "markers": "python_version >= '3.7'", - "version": "==1.3.0" + "version": "==1.3.1" }, "sqlalchemy": { "hashes": [ @@ -699,12 +765,12 @@ }, "typing-extensions": { "hashes": [ - "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783", - "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd" + "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0", + "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==4.9.0" + "version": "==4.11.0" }, "uvicorn": { "hashes": [ @@ -972,11 +1038,11 @@ }, "charset-normalizer": { "hashes": [ - "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597", - "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df" + "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845", + "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f" ], - "markers": "python_version >= '3'", - "version": "==2.0.12" + "markers": "python_full_version >= '3.6.0'", + "version": "==2.1.1" }, "click": { "hashes": [ @@ -1000,61 +1066,61 @@ "toml" ], "hashes": [ - "sha256:0193657651f5399d433c92f8ae264aff31fc1d066deee4b831549526433f3f61", - "sha256:02f2edb575d62172aa28fe00efe821ae31f25dc3d589055b3fb64d51e52e4ab1", - "sha256:0491275c3b9971cdbd28a4595c2cb5838f08036bca31765bad5e17edf900b2c7", - "sha256:077d366e724f24fc02dbfe9d946534357fda71af9764ff99d73c3c596001bbd7", - "sha256:10e88e7f41e6197ea0429ae18f21ff521d4f4490aa33048f6c6f94c6045a6a75", - "sha256:18e961aa13b6d47f758cc5879383d27b5b3f3dcd9ce8cdbfdc2571fe86feb4dd", - "sha256:1a78b656a4d12b0490ca72651fe4d9f5e07e3c6461063a9b6265ee45eb2bdd35", - "sha256:1ed4b95480952b1a26d863e546fa5094564aa0065e1e5f0d4d0041f293251d04", - "sha256:23b27b8a698e749b61809fb637eb98ebf0e505710ec46a8aa6f1be7dc0dc43a6", - "sha256:23f5881362dcb0e1a92b84b3c2809bdc90db892332daab81ad8f642d8ed55042", - "sha256:32a8d985462e37cfdab611a6f95b09d7c091d07668fdc26e47a725ee575fe166", - "sha256:3468cc8720402af37b6c6e7e2a9cdb9f6c16c728638a2ebc768ba1ef6f26c3a1", - "sha256:379d4c7abad5afbe9d88cc31ea8ca262296480a86af945b08214eb1a556a3e4d", - "sha256:3cacfaefe6089d477264001f90f55b7881ba615953414999c46cc9713ff93c8c", - "sha256:3e3424c554391dc9ef4a92ad28665756566a28fecf47308f91841f6c49288e66", - "sha256:46342fed0fff72efcda77040b14728049200cbba1279e0bf1188f1f2078c1d70", - "sha256:536d609c6963c50055bab766d9951b6c394759190d03311f3e9fcf194ca909e1", - "sha256:5d6850e6e36e332d5511a48a251790ddc545e16e8beaf046c03985c69ccb2676", - "sha256:6008adeca04a445ea6ef31b2cbaf1d01d02986047606f7da266629afee982630", - "sha256:64e723ca82a84053dd7bfcc986bdb34af8d9da83c521c19d6b472bc6880e191a", - "sha256:6b00e21f86598b6330f0019b40fb397e705135040dbedc2ca9a93c7441178e74", - "sha256:6d224f0c4c9c98290a6990259073f496fcec1b5cc613eecbd22786d398ded3ad", - "sha256:6dceb61d40cbfcf45f51e59933c784a50846dc03211054bd76b421a713dcdf19", - "sha256:7ac8f8eb153724f84885a1374999b7e45734bf93a87d8df1e7ce2146860edef6", - "sha256:85ccc5fa54c2ed64bd91ed3b4a627b9cce04646a659512a051fa82a92c04a448", - "sha256:869b5046d41abfea3e381dd143407b0d29b8282a904a19cb908fa24d090cc018", - "sha256:8bdb0285a0202888d19ec6b6d23d5990410decb932b709f2b0dfe216d031d218", - "sha256:8dfc5e195bbef80aabd81596ef52a1277ee7143fe419efc3c4d8ba2754671756", - "sha256:8e738a492b6221f8dcf281b67129510835461132b03024830ac0e554311a5c54", - "sha256:918440dea04521f499721c039863ef95433314b1db00ff826a02580c1f503e45", - "sha256:9641e21670c68c7e57d2053ddf6c443e4f0a6e18e547e86af3fad0795414a628", - "sha256:9d2f9d4cc2a53b38cabc2d6d80f7f9b7e3da26b2f53d48f05876fef7956b6968", - "sha256:a07f61fc452c43cd5328b392e52555f7d1952400a1ad09086c4a8addccbd138d", - "sha256:a3277f5fa7483c927fe3a7b017b39351610265308f5267ac6d4c2b64cc1d8d25", - "sha256:a4a3907011d39dbc3e37bdc5df0a8c93853c369039b59efa33a7b6669de04c60", - "sha256:aeb2c2688ed93b027eb0d26aa188ada34acb22dceea256d76390eea135083950", - "sha256:b094116f0b6155e36a304ff912f89bbb5067157aff5f94060ff20bbabdc8da06", - "sha256:b8ffb498a83d7e0305968289441914154fb0ef5d8b3157df02a90c6695978295", - "sha256:b9bb62fac84d5f2ff523304e59e5c439955fb3b7f44e3d7b2085184db74d733b", - "sha256:c61f66d93d712f6e03369b6a7769233bfda880b12f417eefdd4f16d1deb2fc4c", - "sha256:ca6e61dc52f601d1d224526360cdeab0d0712ec104a2ce6cc5ccef6ed9a233bc", - "sha256:ca7b26a5e456a843b9b6683eada193fc1f65c761b3a473941efe5a291f604c74", - "sha256:d12c923757de24e4e2110cf8832d83a886a4cf215c6e61ed506006872b43a6d1", - "sha256:d17bbc946f52ca67adf72a5ee783cd7cd3477f8f8796f59b4974a9b59cacc9ee", - "sha256:dfd1e1b9f0898817babf840b77ce9fe655ecbe8b1b327983df485b30df8cc011", - "sha256:e0860a348bf7004c812c8368d1fc7f77fe8e4c095d661a579196a9533778e156", - "sha256:f2f5968608b1fe2a1d00d01ad1017ee27efd99b3437e08b83ded9b7af3f6f766", - "sha256:f3771b23bb3675a06f5d885c3630b1d01ea6cac9e84a01aaf5508706dba546c5", - "sha256:f68ef3660677e6624c8cace943e4765545f8191313a07288a53d3da188bd8581", - "sha256:f86f368e1c7ce897bf2457b9eb61169a44e2ef797099fb5728482b8d69f3f016", - "sha256:f90515974b39f4dea2f27c0959688621b46d96d5a626cf9c53dbc653a895c05c", - "sha256:fe558371c1bdf3b8fa03e097c523fb9645b8730399c14fe7721ee9c9e2a545d3" + "sha256:00838a35b882694afda09f85e469c96367daa3f3f2b097d846a7216993d37f4c", + "sha256:0513b9508b93da4e1716744ef6ebc507aff016ba115ffe8ecff744d1322a7b63", + "sha256:09c3255458533cb76ef55da8cc49ffab9e33f083739c8bd4f58e79fecfe288f7", + "sha256:09ef9199ed6653989ebbcaacc9b62b514bb63ea2f90256e71fea3ed74bd8ff6f", + "sha256:09fa497a8ab37784fbb20ab699c246053ac294d13fc7eb40ec007a5043ec91f8", + "sha256:0f9f50e7ef2a71e2fae92774c99170eb8304e3fdf9c8c3c7ae9bab3e7229c5cf", + "sha256:137eb07173141545e07403cca94ab625cc1cc6bc4c1e97b6e3846270e7e1fea0", + "sha256:1f384c3cc76aeedce208643697fb3e8437604b512255de6d18dae3f27655a384", + "sha256:201bef2eea65e0e9c56343115ba3814e896afe6d36ffd37bab783261db430f76", + "sha256:38dd60d7bf242c4ed5b38e094baf6401faa114fc09e9e6632374388a404f98e7", + "sha256:3b799445b9f7ee8bf299cfaed6f5b226c0037b74886a4e11515e569b36fe310d", + "sha256:3ea79bb50e805cd6ac058dfa3b5c8f6c040cb87fe83de10845857f5535d1db70", + "sha256:40209e141059b9370a2657c9b15607815359ab3ef9918f0196b6fccce8d3230f", + "sha256:41c9c5f3de16b903b610d09650e5e27adbfa7f500302718c9ffd1c12cf9d6818", + "sha256:54eb8d1bf7cacfbf2a3186019bcf01d11c666bd495ed18717162f7eb1e9dd00b", + "sha256:598825b51b81c808cb6f078dcb972f96af96b078faa47af7dfcdf282835baa8d", + "sha256:5fc1de20b2d4a061b3df27ab9b7c7111e9a710f10dc2b84d33a4ab25065994ec", + "sha256:623512f8ba53c422fcfb2ce68362c97945095b864cda94a92edbaf5994201083", + "sha256:690db6517f09336559dc0b5f55342df62370a48f5469fabf502db2c6d1cffcd2", + "sha256:69eb372f7e2ece89f14751fbcbe470295d73ed41ecd37ca36ed2eb47512a6ab9", + "sha256:73bfb9c09951125d06ee473bed216e2c3742f530fc5acc1383883125de76d9cd", + "sha256:742a76a12aa45b44d236815d282b03cfb1de3b4323f3e4ec933acfae08e54ade", + "sha256:7c95949560050d04d46b919301826525597f07b33beba6187d04fa64d47ac82e", + "sha256:8130a2aa2acb8788e0b56938786c33c7c98562697bf9f4c7d6e8e5e3a0501e4a", + "sha256:8a2b2b78c78293782fd3767d53e6474582f62443d0504b1554370bde86cc8227", + "sha256:8ce1415194b4a6bd0cdcc3a1dfbf58b63f910dcb7330fe15bdff542c56949f87", + "sha256:9ca28a302acb19b6af89e90f33ee3e1906961f94b54ea37de6737b7ca9d8827c", + "sha256:a4cdc86d54b5da0df6d3d3a2f0b710949286094c3a6700c21e9015932b81447e", + "sha256:aa5b1c1bfc28384f1f53b69a023d789f72b2e0ab1b3787aae16992a7ca21056c", + "sha256:aadacf9a2f407a4688d700e4ebab33a7e2e408f2ca04dbf4aef17585389eff3e", + "sha256:ae71e7ddb7a413dd60052e90528f2f65270aad4b509563af6d03d53e979feafd", + "sha256:b14706df8b2de49869ae03a5ccbc211f4041750cd4a66f698df89d44f4bd30ec", + "sha256:b1a93009cb80730c9bca5d6d4665494b725b6e8e157c1cb7f2db5b4b122ea562", + "sha256:b2991665420a803495e0b90a79233c1433d6ed77ef282e8e152a324bbbc5e0c8", + "sha256:b2c5edc4ac10a7ef6605a966c58929ec6c1bd0917fb8c15cb3363f65aa40e677", + "sha256:b4d33f418f46362995f1e9d4f3a35a1b6322cb959c31d88ae56b0298e1c22357", + "sha256:b91cbc4b195444e7e258ba27ac33769c41b94967919f10037e6355e998af255c", + "sha256:c74880fc64d4958159fbd537a091d2a585448a8f8508bf248d72112723974cbd", + "sha256:c901df83d097649e257e803be22592aedfd5182f07b3cc87d640bbb9afd50f49", + "sha256:cac99918c7bba15302a2d81f0312c08054a3359eaa1929c7e4b26ebe41e9b286", + "sha256:cc4f1358cb0c78edef3ed237ef2c86056206bb8d9140e73b6b89fbcfcbdd40e1", + "sha256:ccd341521be3d1b3daeb41960ae94a5e87abe2f46f17224ba5d6f2b8398016cf", + "sha256:ce4b94265ca988c3f8e479e741693d143026632672e3ff924f25fab50518dd51", + "sha256:cf271892d13e43bc2b51e6908ec9a6a5094a4df1d8af0bfc360088ee6c684409", + "sha256:d5ae728ff3b5401cc320d792866987e7e7e880e6ebd24433b70a33b643bb0384", + "sha256:d71eec7d83298f1af3326ce0ff1d0ea83c7cb98f72b577097f9083b20bdaf05e", + "sha256:d898fe162d26929b5960e4e138651f7427048e72c853607f2b200909794ed978", + "sha256:d89d7b2974cae412400e88f35d86af72208e1ede1a541954af5d944a8ba46c57", + "sha256:dfa8fe35a0bb90382837b238fff375de15f0dcdb9ae68ff85f7a63649c98527e", + "sha256:e0be5efd5127542ef31f165de269f77560d6cdef525fffa446de6f7e9186cfb2", + "sha256:fdfafb32984684eb03c2d83e1e51f64f0906b11e64482df3c5db936ce3839d48", + "sha256:ff7687ca3d7028d8a5f0ebae95a6e4827c5616b31a4ee1192bdfde697db110d4" ], "markers": "python_version >= '3.8'", - "version": "==7.4.1" + "version": "==7.4.4" }, "decoy": { "hashes": [ @@ -1081,11 +1147,11 @@ }, "execnet": { "hashes": [ - "sha256:88256416ae766bc9e8895c76a87928c0012183da3cc4fc18016e6f050e025f41", - "sha256:cc59bc4423742fd71ad227122eb0dd44db51efb3dc4095b45ac9a08c770096af" + "sha256:26dee51f1b80cebd6d0ca8e74dd8745419761d3bef34163928cbebbdc4749fdc", + "sha256:5189b52c6121c24feae288166ab41b32549c7e2348652736540b9e6e7d4e72e3" ], - "markers": "python_version >= '3.7'", - "version": "==2.0.2" + "markers": "python_version >= '3.8'", + "version": "==2.1.1" }, "flake8": { "hashes": [ @@ -1142,11 +1208,11 @@ }, "httpcore": { "hashes": [ - "sha256:096cc05bca73b8e459a1fc3dcf585148f63e534eae4339559c9b8a8d6399acc7", - "sha256:9fc092e4799b26174648e54b74ed5f683132a464e95643b226e00c2ed2fa6535" + "sha256:34a38e2f9291467ee3b44e89dd52615370e152954ba21721378a87b2960f7a61", + "sha256:421f18bac248b25d310f3cacd198d55b8e6125c107797b609ff9b7a6ba7991b5" ], "markers": "python_version >= '3.8'", - "version": "==1.0.2" + "version": "==1.0.5" }, "httpx": { "hashes": [ @@ -1190,14 +1256,6 @@ "markers": "python_version >= '3.7'", "version": "==4.17.3" }, - "jsonschema-specifications": { - "hashes": [ - "sha256:48a76787b3e70f5ed53f1160d2b81f586e4ca6d1548c5de7085d1682674764cc", - "sha256:87e4fdf3a94858b8a2ba2778d9ba57d8a9cafca7c7489c46ba0d30a8bc6a9c3c" - ], - "markers": "python_version >= '3.8'", - "version": "==2023.12.1" - }, "mccabe": { "hashes": [ "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", @@ -1257,13 +1315,18 @@ "markers": "python_version >= '3.5'", "version": "==1.0.0" }, + "opentrons-shared-data": { + "editable": true, + "markers": "python_version >= '3.8'", + "path": "../shared-data/python" + }, "packaging": { "hashes": [ - "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5", - "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7" + "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5", + "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9" ], "markers": "python_version >= '3.7'", - "version": "==23.2" + "version": "==24.0" }, "paho-mqtt": { "hashes": [ @@ -1287,6 +1350,10 @@ "markers": "python_version >= '2.6'", "version": "==6.0.0" }, + "performance-metrics": { + "editable": true, + "file": "../performance-metrics" + }, "platformdirs": { "hashes": [ "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068", @@ -1319,6 +1386,49 @@ "markers": "python_version >= '3.8'", "version": "==2.11.1" }, + "pydantic": { + "hashes": [ + "sha256:0fe8a415cea8f340e7a9af9c54fc71a649b43e8ca3cc732986116b3cb135d303", + "sha256:1289c180abd4bd4555bb927c42ee42abc3aee02b0fb2d1223fb7c6e5bef87dbe", + "sha256:1eb2085c13bce1612da8537b2d90f549c8cbb05c67e8f22854e201bde5d98a47", + "sha256:2031de0967c279df0d8a1c72b4ffc411ecd06bac607a212892757db7462fc494", + "sha256:2a7bac939fa326db1ab741c9d7f44c565a1d1e80908b3797f7f81a4f86bc8d33", + "sha256:2d5a58feb9a39f481eda4d5ca220aa8b9d4f21a41274760b9bc66bfd72595b86", + "sha256:2f9a6fab5f82ada41d56b0602606a5506aab165ca54e52bc4545028382ef1c5d", + "sha256:2fcfb5296d7877af406ba1547dfde9943b1256d8928732267e2653c26938cd9c", + "sha256:549a8e3d81df0a85226963611950b12d2d334f214436a19537b2efed61b7639a", + "sha256:598da88dfa127b666852bef6d0d796573a8cf5009ffd62104094a4fe39599565", + "sha256:5d1197e462e0364906cbc19681605cb7c036f2475c899b6f296104ad42b9f5fb", + "sha256:69328e15cfda2c392da4e713443c7dbffa1505bc9d566e71e55abe14c97ddc62", + "sha256:6a9dfa722316f4acf4460afdf5d41d5246a80e249c7ff475c43a3a1e9d75cf62", + "sha256:6b30bcb8cbfccfcf02acb8f1a261143fab622831d9c0989707e0e659f77a18e0", + "sha256:6c076be61cd0177a8433c0adcb03475baf4ee91edf5a4e550161ad57fc90f523", + "sha256:771735dc43cf8383959dc9b90aa281f0b6092321ca98677c5fb6125a6f56d58d", + "sha256:795e34e6cc065f8f498c89b894a3c6da294a936ee71e644e4bd44de048af1405", + "sha256:87afda5539d5140cb8ba9e8b8c8865cb5b1463924d38490d73d3ccfd80896b3f", + "sha256:8fb2aa3ab3728d950bcc885a2e9eff6c8fc40bc0b7bb434e555c215491bcf48b", + "sha256:a1fcb59f2f355ec350073af41d927bf83a63b50e640f4dbaa01053a28b7a7718", + "sha256:a5e7add47a5b5a40c49b3036d464e3c7802f8ae0d1e66035ea16aa5b7a3923ed", + "sha256:a73f489aebd0c2121ed974054cb2759af8a9f747de120acd2c3394cf84176ccb", + "sha256:ab26038b8375581dc832a63c948f261ae0aa21f1d34c1293469f135fa92972a5", + "sha256:b0d191db0f92dfcb1dec210ca244fdae5cbe918c6050b342d619c09d31eea0cc", + "sha256:b749a43aa51e32839c9d71dc67eb1e4221bb04af1033a32e3923d46f9effa942", + "sha256:b7ccf02d7eb340b216ec33e53a3a629856afe1c6e0ef91d84a4e6f2fb2ca70fe", + "sha256:ba5b2e6fe6ca2b7e013398bc7d7b170e21cce322d266ffcd57cca313e54fb246", + "sha256:ba5c4a8552bff16c61882db58544116d021d0b31ee7c66958d14cf386a5b5350", + "sha256:c79e6a11a07da7374f46970410b41d5e266f7f38f6a17a9c4823db80dadf4303", + "sha256:ca48477862372ac3770969b9d75f1bf66131d386dba79506c46d75e6b48c1e09", + "sha256:dea7adcc33d5d105896401a1f37d56b47d443a2b2605ff8a969a0ed5543f7e33", + "sha256:e0a16d274b588767602b7646fa05af2782576a6cf1022f4ba74cbb4db66f6ca8", + "sha256:e4129b528c6baa99a429f97ce733fff478ec955513630e61b49804b6cf9b224a", + "sha256:e5f805d2d5d0a41633651a73fa4ecdd0b3d7a49de4ec3fadf062fe16501ddbf1", + "sha256:ef6c96b2baa2100ec91a4b428f80d8f28a3c9e53568219b6c298c1125572ebc6", + "sha256:fdbdd1d630195689f325c9ef1a12900524dceb503b00a987663ff4f58669b93d" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==1.10.12" + }, "pydocstyle": { "hashes": [ "sha256:118762d452a49d6b05e194ef344a55822987a462831ade91ec5c06fd2169d019", @@ -1350,6 +1460,44 @@ ], "version": "==1.8.0" }, + "pyrsistent": { + "hashes": [ + "sha256:0724c506cd8b63c69c7f883cc233aac948c1ea946ea95996ad8b1380c25e1d3f", + "sha256:09848306523a3aba463c4b49493a760e7a6ca52e4826aa100ee99d8d39b7ad1e", + "sha256:0f3b1bcaa1f0629c978b355a7c37acd58907390149b7311b5db1b37648eb6958", + "sha256:21cc459636983764e692b9eba7144cdd54fdec23ccdb1e8ba392a63666c60c34", + "sha256:2e14c95c16211d166f59c6611533d0dacce2e25de0f76e4c140fde250997b3ca", + "sha256:2e2c116cc804d9b09ce9814d17df5edf1df0c624aba3b43bc1ad90411487036d", + "sha256:4021a7f963d88ccd15b523787d18ed5e5269ce57aa4037146a2377ff607ae87d", + "sha256:4c48f78f62ab596c679086084d0dd13254ae4f3d6c72a83ffdf5ebdef8f265a4", + "sha256:4f5c2d012671b7391803263419e31b5c7c21e7c95c8760d7fc35602353dee714", + "sha256:58b8f6366e152092194ae68fefe18b9f0b4f89227dfd86a07770c3d86097aebf", + "sha256:59a89bccd615551391f3237e00006a26bcf98a4d18623a19909a2c48b8e986ee", + "sha256:5cdd7ef1ea7a491ae70d826b6cc64868de09a1d5ff9ef8d574250d0940e275b8", + "sha256:6288b3fa6622ad8a91e6eb759cfc48ff3089e7c17fb1d4c59a919769314af224", + "sha256:6d270ec9dd33cdb13f4d62c95c1a5a50e6b7cdd86302b494217137f760495b9d", + "sha256:79ed12ba79935adaac1664fd7e0e585a22caa539dfc9b7c7c6d5ebf91fb89054", + "sha256:7d29c23bdf6e5438c755b941cef867ec2a4a172ceb9f50553b6ed70d50dfd656", + "sha256:8441cf9616d642c475684d6cf2520dd24812e996ba9af15e606df5f6fd9d04a7", + "sha256:881bbea27bbd32d37eb24dd320a5e745a2a5b092a17f6debc1349252fac85423", + "sha256:8c3aba3e01235221e5b229a6c05f585f344734bd1ad42a8ac51493d74722bbce", + "sha256:a14798c3005ec892bbada26485c2eea3b54109cb2533713e355c806891f63c5e", + "sha256:b14decb628fac50db5e02ee5a35a9c0772d20277824cfe845c8a8b717c15daa3", + "sha256:b318ca24db0f0518630e8b6f3831e9cba78f099ed5c1d65ffe3e023003043ba0", + "sha256:c1beb78af5423b879edaf23c5591ff292cf7c33979734c99aa66d5914ead880f", + "sha256:c55acc4733aad6560a7f5f818466631f07efc001fd023f34a6c203f8b6df0f0b", + "sha256:ca52d1ceae015859d16aded12584c59eb3825f7b50c6cfd621d4231a6cc624ce", + "sha256:cae40a9e3ce178415040a0383f00e8d68b569e97f31928a3a8ad37e3fde6df6a", + "sha256:e78d0c7c1e99a4a45c99143900ea0546025e41bb59ebc10182e947cf1ece9174", + "sha256:ef3992833fbd686ee783590639f4b8343a57f1f75de8633749d984dc0eb16c86", + "sha256:f058a615031eea4ef94ead6456f5ec2026c19fb5bd6bfe86e9665c4158cf802f", + "sha256:f5ac696f02b3fc01a710427585c855f65cd9c640e14f52abe52020722bb4906b", + "sha256:f920385a11207dc372a028b3f1e1038bb244b3ec38d448e6d8e43c6b3ba20e98", + "sha256:fed2c3216a605dc9a6ea50c7e84c82906e3684c4e80d2908208f662a6cbf9022" + ], + "markers": "python_version >= '3.8'", + "version": "==0.20.0" + }, "pytest": { "hashes": [ "sha256:130328f552dcfac0b1cec75c12e3f005619dc5f874f0a06e8ff7263f0ee6225e", @@ -1361,12 +1509,12 @@ }, "pytest-asyncio": { "hashes": [ - "sha256:2143d9d9375bf372a73260e4114541485e84fca350b0b6b92674ca56ff5f7ea2", - "sha256:b0079dfac14b60cd1ce4691fbfb1748fe939db7d0234b5aba97197d10fbe0fef" + "sha256:68516fdd1018ac57b846c9846b954f0393b26f094764a28c955eabb0536a4e8a", + "sha256:ffe523a89c1c222598c76856e76852b787504ddb72dd5d9b6617ffa8aa2cde5f" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==0.23.4" + "version": "==0.23.6" }, "pytest-cov": { "hashes": [ @@ -1428,11 +1576,11 @@ }, "python-dateutil": { "hashes": [ - "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", - "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" + "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", + "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.8.2" + "version": "==2.9.0.post0" }, "pyyaml": { "hashes": [ @@ -1491,14 +1639,6 @@ "markers": "python_version >= '3.6'", "version": "==6.0.1" }, - "referencing": { - "hashes": [ - "sha256:39240f2ecc770258f28b642dd47fd74bc8b02484de54e1882b74b35ebd779bd5", - "sha256:c775fedf74bc0f9189c2a3be1c12fd03e8c23f4d371dce795df44e06c5b412f7" - ], - "markers": "python_version >= '3.8'", - "version": "==0.33.0" - }, "requests": { "hashes": [ "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61", @@ -1508,118 +1648,13 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", "version": "==2.27.1" }, - "rpds-py": { - "hashes": [ - "sha256:01f58a7306b64e0a4fe042047dd2b7d411ee82e54240284bab63e325762c1147", - "sha256:0210b2668f24c078307260bf88bdac9d6f1093635df5123789bfee4d8d7fc8e7", - "sha256:02866e060219514940342a1f84303a1ef7a1dad0ac311792fbbe19b521b489d2", - "sha256:0387ce69ba06e43df54e43968090f3626e231e4bc9150e4c3246947567695f68", - "sha256:060f412230d5f19fc8c8b75f315931b408d8ebf56aec33ef4168d1b9e54200b1", - "sha256:071bc28c589b86bc6351a339114fb7a029f5cddbaca34103aa573eba7b482382", - "sha256:0bfb09bf41fe7c51413f563373e5f537eaa653d7adc4830399d4e9bdc199959d", - "sha256:10162fe3f5f47c37ebf6d8ff5a2368508fe22007e3077bf25b9c7d803454d921", - "sha256:149c5cd24f729e3567b56e1795f74577aa3126c14c11e457bec1b1c90d212e38", - "sha256:1701fc54460ae2e5efc1dd6350eafd7a760f516df8dbe51d4a1c79d69472fbd4", - "sha256:1957a2ab607f9added64478a6982742eb29f109d89d065fa44e01691a20fc20a", - "sha256:1a746a6d49665058a5896000e8d9d2f1a6acba8a03b389c1e4c06e11e0b7f40d", - "sha256:1bfcad3109c1e5ba3cbe2f421614e70439f72897515a96c462ea657261b96518", - "sha256:1d36b2b59e8cc6e576f8f7b671e32f2ff43153f0ad6d0201250a7c07f25d570e", - "sha256:1db228102ab9d1ff4c64148c96320d0be7044fa28bd865a9ce628ce98da5973d", - "sha256:1dc29db3900cb1bb40353772417800f29c3d078dbc8024fd64655a04ee3c4bdf", - "sha256:1e626b365293a2142a62b9a614e1f8e331b28f3ca57b9f05ebbf4cf2a0f0bdc5", - "sha256:1f3c3461ebb4c4f1bbc70b15d20b565759f97a5aaf13af811fcefc892e9197ba", - "sha256:20de7b7179e2031a04042e85dc463a93a82bc177eeba5ddd13ff746325558aa6", - "sha256:24e4900a6643f87058a27320f81336d527ccfe503984528edde4bb660c8c8d59", - "sha256:2528ff96d09f12e638695f3a2e0c609c7b84c6df7c5ae9bfeb9252b6fa686253", - "sha256:25f071737dae674ca8937a73d0f43f5a52e92c2d178330b4c0bb6ab05586ffa6", - "sha256:270987bc22e7e5a962b1094953ae901395e8c1e1e83ad016c5cfcfff75a15a3f", - "sha256:292f7344a3301802e7c25c53792fae7d1593cb0e50964e7bcdcc5cf533d634e3", - "sha256:2953937f83820376b5979318840f3ee47477d94c17b940fe31d9458d79ae7eea", - "sha256:2a792b2e1d3038daa83fa474d559acfd6dc1e3650ee93b2662ddc17dbff20ad1", - "sha256:2a7b2f2f56a16a6d62e55354dd329d929560442bd92e87397b7a9586a32e3e76", - "sha256:2f4eb548daf4836e3b2c662033bfbfc551db58d30fd8fe660314f86bf8510b93", - "sha256:3664d126d3388a887db44c2e293f87d500c4184ec43d5d14d2d2babdb4c64cad", - "sha256:3677fcca7fb728c86a78660c7fb1b07b69b281964673f486ae72860e13f512ad", - "sha256:380e0df2e9d5d5d339803cfc6d183a5442ad7ab3c63c2a0982e8c824566c5ccc", - "sha256:3ac732390d529d8469b831949c78085b034bff67f584559340008d0f6041a049", - "sha256:4128980a14ed805e1b91a7ed551250282a8ddf8201a4e9f8f5b7e6225f54170d", - "sha256:4341bd7579611cf50e7b20bb8c2e23512a3dc79de987a1f411cb458ab670eb90", - "sha256:436474f17733c7dca0fbf096d36ae65277e8645039df12a0fa52445ca494729d", - "sha256:4dc889a9d8a34758d0fcc9ac86adb97bab3fb7f0c4d29794357eb147536483fd", - "sha256:4e21b76075c01d65d0f0f34302b5a7457d95721d5e0667aea65e5bb3ab415c25", - "sha256:516fb8c77805159e97a689e2f1c80655c7658f5af601c34ffdb916605598cda2", - "sha256:5576ee2f3a309d2bb403ec292d5958ce03953b0e57a11d224c1f134feaf8c40f", - "sha256:5a024fa96d541fd7edaa0e9d904601c6445e95a729a2900c5aec6555fe921ed6", - "sha256:5d0e8a6434a3fbf77d11448c9c25b2f25244226cfbec1a5159947cac5b8c5fa4", - "sha256:5e7d63ec01fe7c76c2dbb7e972fece45acbb8836e72682bde138e7e039906e2c", - "sha256:60e820ee1004327609b28db8307acc27f5f2e9a0b185b2064c5f23e815f248f8", - "sha256:637b802f3f069a64436d432117a7e58fab414b4e27a7e81049817ae94de45d8d", - "sha256:65dcf105c1943cba45d19207ef51b8bc46d232a381e94dd38719d52d3980015b", - "sha256:698ea95a60c8b16b58be9d854c9f993c639f5c214cf9ba782eca53a8789d6b19", - "sha256:70fcc6c2906cfa5c6a552ba7ae2ce64b6c32f437d8f3f8eea49925b278a61453", - "sha256:720215373a280f78a1814becb1312d4e4d1077b1202a56d2b0815e95ccb99ce9", - "sha256:7450dbd659fed6dd41d1a7d47ed767e893ba402af8ae664c157c255ec6067fde", - "sha256:7b7d9ca34542099b4e185b3c2a2b2eda2e318a7dbde0b0d83357a6d4421b5296", - "sha256:7fbd70cb8b54fe745301921b0816c08b6d917593429dfc437fd024b5ba713c58", - "sha256:81038ff87a4e04c22e1d81f947c6ac46f122e0c80460b9006e6517c4d842a6ec", - "sha256:810685321f4a304b2b55577c915bece4c4a06dfe38f6e62d9cc1d6ca8ee86b99", - "sha256:82ada4a8ed9e82e443fcef87e22a3eed3654dd3adf6e3b3a0deb70f03e86142a", - "sha256:841320e1841bb53fada91c9725e766bb25009cfd4144e92298db296fb6c894fb", - "sha256:8587fd64c2a91c33cdc39d0cebdaf30e79491cc029a37fcd458ba863f8815383", - "sha256:8ffe53e1d8ef2520ebcf0c9fec15bb721da59e8ef283b6ff3079613b1e30513d", - "sha256:9051e3d2af8f55b42061603e29e744724cb5f65b128a491446cc029b3e2ea896", - "sha256:91e5a8200e65aaac342a791272c564dffcf1281abd635d304d6c4e6b495f29dc", - "sha256:93432e747fb07fa567ad9cc7aaadd6e29710e515aabf939dfbed8046041346c6", - "sha256:938eab7323a736533f015e6069a7d53ef2dcc841e4e533b782c2bfb9fb12d84b", - "sha256:9584f8f52010295a4a417221861df9bea4c72d9632562b6e59b3c7b87a1522b7", - "sha256:9737bdaa0ad33d34c0efc718741abaafce62fadae72c8b251df9b0c823c63b22", - "sha256:99da0a4686ada4ed0f778120a0ea8d066de1a0a92ab0d13ae68492a437db78bf", - "sha256:99f567dae93e10be2daaa896e07513dd4bf9c2ecf0576e0533ac36ba3b1d5394", - "sha256:9bdf1303df671179eaf2cb41e8515a07fc78d9d00f111eadbe3e14262f59c3d0", - "sha256:9f0e4dc0f17dcea4ab9d13ac5c666b6b5337042b4d8f27e01b70fae41dd65c57", - "sha256:a000133a90eea274a6f28adc3084643263b1e7c1a5a66eb0a0a7a36aa757ed74", - "sha256:a3264e3e858de4fc601741498215835ff324ff2482fd4e4af61b46512dd7fc83", - "sha256:a71169d505af63bb4d20d23a8fbd4c6ce272e7bce6cc31f617152aa784436f29", - "sha256:a967dd6afda7715d911c25a6ba1517975acd8d1092b2f326718725461a3d33f9", - "sha256:aa5bfb13f1e89151ade0eb812f7b0d7a4d643406caaad65ce1cbabe0a66d695f", - "sha256:ae35e8e6801c5ab071b992cb2da958eee76340e6926ec693b5ff7d6381441745", - "sha256:b686f25377f9c006acbac63f61614416a6317133ab7fafe5de5f7dc8a06d42eb", - "sha256:b760a56e080a826c2e5af09002c1a037382ed21d03134eb6294812dda268c811", - "sha256:b86b21b348f7e5485fae740d845c65a880f5d1eda1e063bc59bef92d1f7d0c55", - "sha256:b9412abdf0ba70faa6e2ee6c0cc62a8defb772e78860cef419865917d86c7342", - "sha256:bd345a13ce06e94c753dab52f8e71e5252aec1e4f8022d24d56decd31e1b9b23", - "sha256:be22ae34d68544df293152b7e50895ba70d2a833ad9566932d750d3625918b82", - "sha256:bf046179d011e6114daf12a534d874958b039342b347348a78b7cdf0dd9d6041", - "sha256:c3d2010656999b63e628a3c694f23020322b4178c450dc478558a2b6ef3cb9bb", - "sha256:c64602e8be701c6cfe42064b71c84ce62ce66ddc6422c15463fd8127db3d8066", - "sha256:d65e6b4f1443048eb7e833c2accb4fa7ee67cc7d54f31b4f0555b474758bee55", - "sha256:d8bbd8e56f3ba25a7d0cf980fc42b34028848a53a0e36c9918550e0280b9d0b6", - "sha256:da1ead63368c04a9bded7904757dfcae01eba0e0f9bc41d3d7f57ebf1c04015a", - "sha256:dbbb95e6fc91ea3102505d111b327004d1c4ce98d56a4a02e82cd451f9f57140", - "sha256:dbc56680ecf585a384fbd93cd42bc82668b77cb525343170a2d86dafaed2a84b", - "sha256:df3b6f45ba4515632c5064e35ca7f31d51d13d1479673185ba8f9fefbbed58b9", - "sha256:dfe07308b311a8293a0d5ef4e61411c5c20f682db6b5e73de6c7c8824272c256", - "sha256:e796051f2070f47230c745d0a77a91088fbee2cc0502e9b796b9c6471983718c", - "sha256:efa767c220d94aa4ac3a6dd3aeb986e9f229eaf5bce92d8b1b3018d06bed3772", - "sha256:f0b8bf5b8db49d8fd40f54772a1dcf262e8be0ad2ab0206b5a2ec109c176c0a4", - "sha256:f175e95a197f6a4059b50757a3dca33b32b61691bdbd22c29e8a8d21d3914cae", - "sha256:f2f3b28b40fddcb6c1f1f6c88c6f3769cd933fa493ceb79da45968a21dccc920", - "sha256:f6c43b6f97209e370124baf2bf40bb1e8edc25311a158867eb1c3a5d449ebc7a", - "sha256:f7f4cb1f173385e8a39c29510dd11a78bf44e360fb75610594973f5ea141028b", - "sha256:fad059a4bd14c45776600d223ec194e77db6c20255578bb5bcdd7c18fd169361", - "sha256:ff1dcb8e8bc2261a088821b2595ef031c91d499a0c1b031c152d43fe0a6ecec8", - "sha256:ffee088ea9b593cc6160518ba9bd319b5475e5f3e578e4552d63818773c6f56a" - ], - "markers": "python_version >= '3.8'", - "version": "==0.17.1" - }, "ruamel.yaml": { "hashes": [ - "sha256:61917e3a35a569c1133a8f772e1226961bf5a1198bea7e23f06a0841dea1ab0e", - "sha256:a013ac02f99a69cdd6277d9664689eb1acba07069f912823177c5eced21a6ada" + "sha256:57b53ba33def16c4f3d807c0ccbc00f8a6081827e81ba2491691b76882d0c636", + "sha256:8b27e6a217e786c6fbe5634d8f3f11bc63e0f80f6a5890f28863d9c45aac311b" ], "markers": "python_version >= '3.7'", - "version": "==0.18.5" + "version": "==0.18.6" }, "ruamel.yaml.clib": { "hashes": [ @@ -1687,11 +1722,11 @@ }, "sniffio": { "hashes": [ - "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101", - "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384" + "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", + "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc" ], "markers": "python_version >= '3.7'", - "version": "==1.3.0" + "version": "==1.3.1" }, "snowballstemmer": { "hashes": [ @@ -1736,12 +1771,12 @@ }, "types-mock": { "hashes": [ - "sha256:13ca379d5710ccb3f18f69ade5b08881874cb83383d8fb49b1d4dac9d5c5d090", - "sha256:3d116955495935b0bcba14954b38d97e507cd43eca3e3700fc1b8e4f5c6bf2c7" + "sha256:0769cb376dfc75b45215619f17a9fd6333d771cc29ce4a38937f060b1e45530f", + "sha256:7472797986d83016f96fde7f73577d129b0cd8a8d0b783487a7be330d57ba431" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==5.1.0.20240106" + "version": "==5.1.0.20240311" }, "types-paho-mqtt": { "hashes": [ @@ -1769,12 +1804,12 @@ }, "typing-extensions": { "hashes": [ - "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783", - "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd" + "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0", + "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==4.9.0" + "version": "==4.11.0" }, "urllib3": { "hashes": [ diff --git a/shared-data/python/opentrons_shared_data/performance/dev_types.py b/shared-data/python/opentrons_shared_data/performance/dev_types.py new file mode 100644 index 00000000000..842399f2c3b --- /dev/null +++ b/shared-data/python/opentrons_shared_data/performance/dev_types.py @@ -0,0 +1,56 @@ +"""Type definitions for performance tracking.""" + +from typing import Protocol, TypeVar, Callable, Any +from pathlib import Path +from enum import Enum + +F = TypeVar("F", bound=Callable[..., Any]) + + +class SupportsTracking(Protocol): + """Protocol for classes that support tracking of robot context.""" + + def __init__(self, storage_location: Path, should_track: bool) -> None: + """Initialize the tracker.""" + ... + + def track(self, state: "RobotContextState") -> Callable[[F], F]: + """Decorator to track the given state for the decorated function.""" + ... + + def store(self) -> None: + """Store the tracked data.""" + ... + + +class RobotContextState(Enum): + """Enum representing different states of a robot's operation context.""" + + STARTING_UP = 0, "STARTING_UP" + CALIBRATING = 1, "CALIBRATING" + ANALYZING_PROTOCOL = 2, "ANALYZING_PROTOCOL" + RUNNING_PROTOCOL = 3, "RUNNING_PROTOCOL" + SHUTTING_DOWN = 4, "SHUTTING_DOWN" + + def __init__(self, state_id: int, state_name: str) -> None: + """Initialize the enum member.""" + self.state_id = state_id + self.state_name = state_name + + @classmethod + def from_id(cls, state_id: int) -> "RobotContextState": + """Returns the enum member matching the given state ID. + + Args: + state_id: The ID of the state to retrieve. + + Returns: + RobotContextStates: The enum member corresponding to the given ID. + + Raises: + ValueError: If no matching state is found. + """ + for state in RobotContextState: + if state.state_id == state_id: + return state + raise ValueError(f"Invalid state id: {state_id}") From 4931d03591c14da57be8ab6861d2827ad236fa60 Mon Sep 17 00:00:00 2001 From: koji Date: Wed, 17 Apr 2024 15:29:47 -0400 Subject: [PATCH 155/194] fix(opentrons-ai-client): apply GlobalStyle to the application (#14936) * fix(opentrons-ai-client): apply GlobalStyle to the application --- opentrons-ai-client/package.json | 1 - opentrons-ai-client/src/atoms/GlobalStyle/index.ts | 7 ++----- opentrons-ai-client/src/main.tsx | 2 ++ 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/opentrons-ai-client/package.json b/opentrons-ai-client/package.json index e3c056e8bfe..f3dd1d2f2a1 100644 --- a/opentrons-ai-client/package.json +++ b/opentrons-ai-client/package.json @@ -19,7 +19,6 @@ }, "homepage": "https://github.com/Opentrons/opentrons", "dependencies": { - "@fontsource/dejavu-sans": "5.0.3", "@fontsource/public-sans": "5.0.3", "@opentrons/components": "link:../components", "i18next": "^19.8.3", diff --git a/opentrons-ai-client/src/atoms/GlobalStyle/index.ts b/opentrons-ai-client/src/atoms/GlobalStyle/index.ts index 1319d297779..782a2a0b91b 100644 --- a/opentrons-ai-client/src/atoms/GlobalStyle/index.ts +++ b/opentrons-ai-client/src/atoms/GlobalStyle/index.ts @@ -4,15 +4,12 @@ import '@fontsource/public-sans' import '@fontsource/public-sans/600.css' import '@fontsource/public-sans/700.css' -export const GlobalStyle = createGlobalStyle<{ isOnDevice?: boolean }>` +export const GlobalStyle = createGlobalStyle` * { box-sizing: border-box; margin: 0; padding: 0; - font-family: ${props => - props.isOnDevice ?? false - ? 'Public Sans, DejaVu Sans' - : 'Open Sans'}, sans-serif; + font-family: 'Public Sans', 'sans-serif'; } html, diff --git a/opentrons-ai-client/src/main.tsx b/opentrons-ai-client/src/main.tsx index bf46623695e..a2f1338bd7b 100644 --- a/opentrons-ai-client/src/main.tsx +++ b/opentrons-ai-client/src/main.tsx @@ -1,6 +1,7 @@ import React from 'react' import ReactDOM from 'react-dom/client' import { I18nextProvider } from 'react-i18next' +import { GlobalStyle } from './atoms/GlobalStyle' import { i18n } from './i18n' import { App } from './App' @@ -9,6 +10,7 @@ const rootElement = document.getElementById('root') if (rootElement != null) { ReactDOM.createRoot(rootElement).render( + From 105e8bb70f670a223a87f1216026175d423b76ee Mon Sep 17 00:00:00 2001 From: Jethary Rader <66035149+jerader@users.noreply.github.com> Date: Wed, 17 Apr 2024 15:45:49 -0400 Subject: [PATCH 156/194] fix(app): add optional description icon to dropdownMenu (#14934) closes AUTH-311 --- app/src/atoms/MenuList/DropdownMenu.tsx | 33 +++++++++++++++---- .../organisms/ChooseRobotSlideout/index.tsx | 1 + 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/app/src/atoms/MenuList/DropdownMenu.tsx b/app/src/atoms/MenuList/DropdownMenu.tsx index 7eafba80ecb..ec383bf0ead 100644 --- a/app/src/atoms/MenuList/DropdownMenu.tsx +++ b/app/src/atoms/MenuList/DropdownMenu.tsx @@ -15,7 +15,9 @@ import { TYPOGRAPHY, useOnClickOutside, POSITION_RELATIVE, + useHoverTooltip, } from '@opentrons/components' +import { Tooltip } from '../Tooltip' import { MenuItem } from './MenuItem' export interface DropdownOption { @@ -33,6 +35,7 @@ export interface DropdownMenuProps { dropdownType?: DropdownBorder title?: string caption?: string | null + tooltipText?: string } // TODO: (smb: 4/15/22) refactor this to use html select for accessibility @@ -46,7 +49,9 @@ export function DropdownMenu(props: DropdownMenuProps): JSX.Element { dropdownType = 'rounded', title, caption, + tooltipText, } = props + const [targetProps, tooltipProps] = useHoverTooltip() const [showDropdownMenu, setShowDropdownMenu] = React.useState(false) const toggleSetShowDropdownMenu = (): void => { setShowDropdownMenu(!showDropdownMenu) @@ -96,13 +101,27 @@ export function DropdownMenu(props: DropdownMenuProps): JSX.Element { return ( {title !== null ? ( - - {title} - + + + {title} + + {tooltipText != null ? ( + <> + + + + {tooltipText} + + ) : null} + ) : null} ) } else if (runtimeParam.type === 'int' || runtimeParam.type === 'float') { From f3e966a136cdee4c503580093b93a114c34f4517 Mon Sep 17 00:00:00 2001 From: Nick Diehl <47604184+ncdiehl11@users.noreply.github.com> Date: Wed, 17 Apr 2024 16:40:44 -0400 Subject: [PATCH 157/194] fix(app): reset robot and protocol slideout states on close (#14939) closes RQA-2572 --- .../ChooseProtocolSlideout/index.tsx | 23 ++++++++++++------- .../organisms/ChooseRobotSlideout/index.tsx | 12 +++------- .../index.tsx | 21 ++++++++++++++--- 3 files changed, 36 insertions(+), 20 deletions(-) diff --git a/app/src/organisms/ChooseProtocolSlideout/index.tsx b/app/src/organisms/ChooseProtocolSlideout/index.tsx index 6a1c1a0aa8c..6f00082013a 100644 --- a/app/src/organisms/ChooseProtocolSlideout/index.tsx +++ b/app/src/organisms/ChooseProtocolSlideout/index.tsx @@ -331,6 +331,15 @@ export function ChooseProtocolSlideoutComponent( } }) ?? null + const resetRunTimeParameters = (): void => { + setRunTimeParametersOverrides( + runTimeParametersOverrides?.map(parameter => ({ + ...parameter, + value: parameter.default, + })) + ) + } + const pageTwoBody = ( @@ -339,13 +348,7 @@ export function ChooseProtocolSlideoutComponent( css={ isRestoreDefaultsLinkEnabled ? ENABLED_LINK_CSS : DISABLED_LINK_CSS } - onClick={() => { - const clone = runTimeParametersOverrides.map(parameter => ({ - ...parameter, - value: parameter.default, - })) - setRunTimeParametersOverrides(clone) - }} + onClick={resetRunTimeParameters} paddingBottom={SPACING.spacing10} {...targetProps} > @@ -408,7 +411,11 @@ export function ChooseProtocolSlideoutComponent( return ( { + onCloseClick() + setCurrentPage(1) + resetRunTimeParameters() + }} currentStep={currentPage} maxSteps={hasRunTimeParameters ? 2 : 1} title={t('choose_protocol_to_run', { name })} diff --git a/app/src/organisms/ChooseRobotSlideout/index.tsx b/app/src/organisms/ChooseRobotSlideout/index.tsx index 520cc0ed7c6..fd77056db76 100644 --- a/app/src/organisms/ChooseRobotSlideout/index.tsx +++ b/app/src/organisms/ChooseRobotSlideout/index.tsx @@ -113,6 +113,7 @@ interface ChooseRobotSlideoutProps showIdleOnly?: boolean multiSlideout?: { currentPage: number } | null setHasParamError?: (isError: boolean) => void + resetRunTimeParameters?: () => void } export function ChooseRobotSlideout( @@ -139,6 +140,7 @@ export function ChooseRobotSlideout( runTimeParametersOverrides, setRunTimeParametersOverrides, setHasParamError, + resetRunTimeParameters, } = props const dispatch = useDispatch() @@ -507,15 +509,7 @@ export function ChooseRobotSlideout( ? ENABLED_LINK_CSS : DISABLED_LINK_CSS } - onClick={() => { - const clone = runTimeParametersOverrides.map(parameter => ({ - ...parameter, - value: parameter.default, - })) - if (setRunTimeParametersOverrides != null) { - setRunTimeParametersOverrides(clone) - } - }} + onClick={() => resetRunTimeParameters?.()} paddingBottom={SPACING.spacing10} {...targetProps} > diff --git a/app/src/organisms/ChooseRobotToRunProtocolSlideout/index.tsx b/app/src/organisms/ChooseRobotToRunProtocolSlideout/index.tsx index 8ef332adaa3..17e64b4fb6b 100644 --- a/app/src/organisms/ChooseRobotToRunProtocolSlideout/index.tsx +++ b/app/src/organisms/ChooseRobotToRunProtocolSlideout/index.tsx @@ -132,6 +132,8 @@ export function ChooseRobotToRunProtocolSlideoutComponent( 'downgrade', ].includes(autoUpdateAction) + const hasRunTimeParameters = runTimeParameters.length > 0 + if ( protocolKey == null || srcFileNames == null || @@ -174,7 +176,14 @@ export function ChooseRobotToRunProtocolSlideoutComponent( ) - const hasRunTimeParameters = runTimeParameters.length > 0 + const resetRunTimeParameters = (): void => { + setRunTimeParametersOverrides( + runTimeParametersOverrides?.map(parameter => ({ + ...parameter, + value: parameter.default, + })) + ) + } return ( { + onCloseClick() + resetRunTimeParameters() + setCurrentPage(1) + setSelectedRobot(null) + }} title={ hasRunTimeParameters && currentPage === 2 ? t('select_parameters_for_robot', { @@ -250,8 +264,9 @@ export function ChooseRobotToRunProtocolSlideoutComponent( reset={resetCreateRun} runCreationError={runCreationError} runCreationErrorCode={runCreationErrorCode} - showIdleOnly={true} + showIdleOnly setHasParamError={setHasParamError} + resetRunTimeParameters={resetRunTimeParameters} /> ) } From 68aa208d18e09981c0cad961cfe89a2a4a0c37d7 Mon Sep 17 00:00:00 2001 From: Shlok Amin Date: Wed, 17 Apr 2024 14:47:51 -0600 Subject: [PATCH 158/194] ci(components): install udev before calling setup (#14937) # Overview Follow up to https://github.com/Opentrons/opentrons/pull/14935 This PR explicitly installs udev before calling make setup in the components CI workflow. This is required to do a full make setup because our usb bindings need it. Hopefully closes [AUTH-331](https://opentrons.atlassian.net/browse/AUTH-331) but I need to push another tag to verify # Risk assessment Low [AUTH-331]: https://opentrons.atlassian.net/browse/AUTH-331?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ --- .github/workflows/components-test-build-deploy.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/components-test-build-deploy.yaml b/.github/workflows/components-test-build-deploy.yaml index d1de9d2b619..6b39fb3b1c8 100644 --- a/.github/workflows/components-test-build-deploy.yaml +++ b/.github/workflows/components-test-build-deploy.yaml @@ -174,6 +174,8 @@ jobs: with: node-version: '18.19.0' registry-url: 'https://registry.npmjs.org' + - name: 'install udev for usb-detection' + run: sudo apt-get update && sudo apt-get install libudev-dev - name: 'setup-js' run: | npm config set cache ./.npm-cache From c09693124eaf76a3ac87741bc903b7a6bc182608 Mon Sep 17 00:00:00 2001 From: Jamey Huffnagle Date: Wed, 17 Apr 2024 18:23:18 -0400 Subject: [PATCH 159/194] fix(app): replace remark with react-markdown (#14942) Closes RQA-2587 and RQA-2566 After the vite migration, the app would crash any time the release notes markdown file needed parsing, because remark decides to invoke process.cwd() for some reason. Webpack can mock "process" out, but there's no 1:1 analogous solution with Vite. Although there's probably some hacky way do accomplish the same thing, remark-react has been deprecated for over three years, so that's more incentive to migrate. After testing a few options, react-markdown is relatively lightweight and properly overrides the markdown as desired (ie, no additional changes need to be made to the markdown for the refactor). It also doesn't have the same Vfile lib dependency that is the source of the "process" issue, so no more crashing when parsing release notes. It's also well-maintained, etc. --- app/package.json | 3 +- app/src/molecules/ReleaseNotes/index.tsx | 33 +- app/typings/remark.d.ts | 11 - yarn.lock | 776 +++++++++++++++++------ 4 files changed, 612 insertions(+), 211 deletions(-) delete mode 100644 app/typings/remark.d.ts diff --git a/app/package.json b/app/package.json index 5097851c9ff..30836e11b8e 100644 --- a/app/package.json +++ b/app/package.json @@ -49,6 +49,7 @@ "react-error-boundary": "^4.0.10", "react-i18next": "13.5.0", "react-intersection-observer": "^8.33.1", + "react-markdown": "9.0.1", "react-redux": "8.1.2", "react-router-dom": "5.3.4", "react-select": "5.4.0", @@ -57,8 +58,6 @@ "redux": "4.0.5", "redux-observable": "1.1.0", "redux-thunk": "2.3.0", - "remark": "9.0.0", - "remark-react": "4.0.3", "reselect": "4.0.0", "rxjs": "^6.5.1", "semver": "5.5.0", diff --git a/app/src/molecules/ReleaseNotes/index.tsx b/app/src/molecules/ReleaseNotes/index.tsx index 57eb22b04c6..38d88616143 100644 --- a/app/src/molecules/ReleaseNotes/index.tsx +++ b/app/src/molecules/ReleaseNotes/index.tsx @@ -1,26 +1,14 @@ import * as React from 'react' -import remark from 'remark' -import reactRenderer from 'remark-react' +import Markdown from 'react-markdown' + import { StyledText } from '@opentrons/components' + import styles from './styles.module.css' + export interface ReleaseNotesProps { source?: string | null } -// ToDo (kk:09/22/2023) This component should be updated in the future -// since the package we use hasn't been updated more than 2 years. -// Also the creator recommends users to replace remark-react with rehype-react. -const renderer = remark().use(reactRenderer, { - remarkReactComponents: { - div: React.Fragment, - h2: HeaderText, - ul: React.Fragment, - li: ParagraphText, - p: ParagraphText, - a: ExternalLink, - }, -}) - const DEFAULT_RELEASE_NOTES = 'We recommend upgrading to the latest version.' export function ReleaseNotes(props: ReleaseNotesProps): JSX.Element { @@ -29,7 +17,18 @@ export function ReleaseNotes(props: ReleaseNotesProps): JSX.Element { return (
    {source != null ? ( - renderer.processSync(source).contents + + {source} + ) : (

    {DEFAULT_RELEASE_NOTES}

    )} diff --git a/app/typings/remark.d.ts b/app/typings/remark.d.ts deleted file mode 100644 index 2eb55d6f77e..00000000000 --- a/app/typings/remark.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -declare module 'remark' { - const remark: any - // eslint-disable-next-line import/no-default-export - export default remark -} - -declare module 'remark-react' { - const reactRenderer: any - // eslint-disable-next-line import/no-default-export - export default reactRenderer -} diff --git a/yarn.lock b/yarn.lock index 9773a4fe6f3..9b1d8cc5d27 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2187,13 +2187,6 @@ lodash "^4.17.15" tmp-promise "^3.0.2" -"@mapbox/hast-util-table-cell-style@^0.1.3": - version "0.1.3" - resolved "https://registry.yarnpkg.com/@mapbox/hast-util-table-cell-style/-/hast-util-table-cell-style-0.1.3.tgz#5b7166ae01297d72216932b245e4b2f0b642dca6" - integrity sha512-QsEsh5YaDvHoMQ2YHdvZy2iDnU3GgKVBTcHf6cILyoWDZtPSdlG444pL/ioPYO/GpXSfODBb9sefEetfC4v9oA== - dependencies: - unist-util-visit "^1.3.0" - "@mdx-js/react@^2.1.5": version "2.3.0" resolved "https://registry.yarnpkg.com/@mdx-js/react/-/react-2.3.0.tgz#4208bd6d70f0d0831def28ef28c26149b03180b3" @@ -2413,6 +2406,7 @@ react-hook-form "7.50.1" react-i18next "13.5.0" react-intersection-observer "^8.33.1" + react-markdown "9.0.1" react-redux "8.1.2" react-router-dom "5.3.4" react-select "5.4.0" @@ -2421,8 +2415,6 @@ redux "4.0.5" redux-observable "1.1.0" redux-thunk "2.3.0" - remark "9.0.0" - remark-react "4.0.3" reselect "4.0.0" rxjs "^6.5.1" semver "5.5.0" @@ -3873,7 +3865,7 @@ resolved "https://registry.yarnpkg.com/@types/dateformat/-/dateformat-3.0.1.tgz#98d747a2e5e9a56070c6bf14e27bff56204e34cc" integrity sha512-KlPPdikagvL6ELjWsljbyDIPzNCeliYkqRpI+zea99vBBbCIA5JNshZAwQKTON139c87y9qvTFVgkFd14rtS4g== -"@types/debug@^4.1.6": +"@types/debug@^4.0.0", "@types/debug@^4.1.6": version "4.1.12" resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.12.tgz#a155f21690871953410df4b6b6f53187f0500917" integrity sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ== @@ -3910,6 +3902,13 @@ resolved "https://registry.yarnpkg.com/@types/escodegen/-/escodegen-0.0.6.tgz#5230a9ce796e042cda6f086dbf19f22ea330659c" integrity sha512-AjwI4MvWx3HAOaZqYsjKWyEObT9lcVV0Y0V8nXo6cXzN8ZiMxVhf6F3d/UNvXVGKrEzL/Dluc5p+y9GkzlTWig== +"@types/estree-jsx@^1.0.0": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@types/estree-jsx/-/estree-jsx-1.0.5.tgz#858a88ea20f34fe65111f005a689fa1ebf70dc18" + integrity sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg== + dependencies: + "@types/estree" "*" + "@types/estree@*", "@types/estree@1.0.5", "@types/estree@^1.0.0": version "1.0.5" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4" @@ -3985,6 +3984,13 @@ dependencies: "@types/node" "*" +"@types/hast@^3.0.0": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@types/hast/-/hast-3.0.4.tgz#1d6b39993b82cea6ad783945b0508c25903e15aa" + integrity sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ== + dependencies: + "@types/unist" "*" + "@types/history@^4.7.11": version "4.7.11" resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.11.tgz#56588b17ae8f50c53983a524fc3cc47437969d64" @@ -4056,6 +4062,13 @@ resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.202.tgz#f09dbd2fb082d507178b2f2a5c7e74bd72ff98f8" integrity sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ== +"@types/mdast@^4.0.0": + version "4.0.3" + resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-4.0.3.tgz#1e011ff013566e919a4232d1701ad30d70cab333" + integrity sha512-LsjtqsyF+d2/yFOYaN22dHZI1Cpwkrj+g06G8+qtUKlhovPW89YhqSnfKtMbkgmEtYpH2gydRNULd6y8mciAFg== + dependencies: + "@types/unist" "*" + "@types/mdx@^2.0.0": version "2.0.11" resolved "https://registry.yarnpkg.com/@types/mdx/-/mdx-2.0.11.tgz#21f4c166ed0e0a3a733869ba04cd8daea9834b8e" @@ -4595,7 +4608,7 @@ "@typescript-eslint/types" "6.21.0" eslint-visitor-keys "^3.4.1" -"@ungap/structured-clone@^1.2.0": +"@ungap/structured-clone@^1.0.0", "@ungap/structured-clone@^1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== @@ -5728,6 +5741,11 @@ bail@^1.0.0: resolved "https://registry.yarnpkg.com/bail/-/bail-1.0.5.tgz#b6fa133404a392cbc1f8c4bf63f5953351e7a776" integrity sha512-xFbRxM1tahm08yHBP16MMjVUAvDaBMD38zsM9EMAUN61omwLmKlOpB/Zku5QkjZ8TZ4vn53pj+t518cH0S03RQ== +bail@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/bail/-/bail-2.0.2.tgz#d26f5cd8fe5d6f832a31517b9f7c356040ba6d5d" + integrity sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw== + balanced-match@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" @@ -6481,6 +6499,11 @@ ccount@^1.0.0, ccount@^1.0.3: resolved "https://registry.yarnpkg.com/ccount/-/ccount-1.1.0.tgz#246687debb6014735131be8abab2d93898f8d043" integrity sha512-vlNK021QdI7PNeiUh/lKkC/mNHHfV0m/Ad5JoI0TYtlBnJAslM/JIkm/tGC88bkLIwO6OQ5uV6ztS6kVAtCDlg== +ccount@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/ccount/-/ccount-2.0.1.tgz#17a3bf82302e0870d6da43a01311a8bc02a3ecf5" + integrity sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg== + chai@^4.3.10: version "4.4.1" resolved "https://registry.yarnpkg.com/chai/-/chai-4.4.1.tgz#3603fa6eba35425b0f2ac91a009fe924106e50d1" @@ -6535,21 +6558,41 @@ character-entities-html4@^1.0.0: resolved "https://registry.yarnpkg.com/character-entities-html4/-/character-entities-html4-1.1.4.tgz#0e64b0a3753ddbf1fdc044c5fd01d0199a02e125" integrity sha512-HRcDxZuZqMx3/a+qrzxdBKBPUpxWEq9xw2OPZ3a/174ihfrQKVsFhqtthBInFy1zZ9GgZyFXOatNujm8M+El3g== +character-entities-html4@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/character-entities-html4/-/character-entities-html4-2.1.0.tgz#1f1adb940c971a4b22ba39ddca6b618dc6e56b2b" + integrity sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA== + character-entities-legacy@^1.0.0: version "1.1.4" resolved "https://registry.yarnpkg.com/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz#94bc1845dce70a5bb9d2ecc748725661293d8fc1" integrity sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA== +character-entities-legacy@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz#76bc83a90738901d7bc223a9e93759fdd560125b" + integrity sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ== + character-entities@^1.0.0: version "1.2.4" resolved "https://registry.yarnpkg.com/character-entities/-/character-entities-1.2.4.tgz#e12c3939b7eaf4e5b15e7ad4c5e28e1d48c5b16b" integrity sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw== +character-entities@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/character-entities/-/character-entities-2.0.2.tgz#2d09c2e72cd9523076ccb21157dff66ad43fcc22" + integrity sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ== + character-reference-invalid@^1.0.0: version "1.1.4" resolved "https://registry.yarnpkg.com/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz#083329cda0eae272ab3dbbf37e9a382c13af1560" integrity sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg== +character-reference-invalid@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz#85c66b041e43b47210faf401278abf808ac45cb9" + integrity sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw== + check-error@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.3.tgz#a6502e4312a7ee969f646e83bb3ddd56281bd694" @@ -6833,7 +6876,7 @@ code-point-at@^1.0.0: resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" integrity sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA== -collapse-white-space@^1.0.0, collapse-white-space@^1.0.2: +collapse-white-space@^1.0.2: version "1.0.6" resolved "https://registry.yarnpkg.com/collapse-white-space/-/collapse-white-space-1.0.6.tgz#e63629c0016665792060dbbeb79c42239d2c5287" integrity sha512-jEovNnrhMuqyCcjfEJA56v0Xq8SkIoPKDyaHahwo3POf4qcSXqMYuwNcOTzp74vTsR9Tn08z4MxWqAhcekogkQ== @@ -6921,6 +6964,11 @@ comma-separated-tokens@^1.0.0, comma-separated-tokens@^1.0.1: resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz#632b80b6117867a158f1080ad498b2fbe7e3f5ea" integrity sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw== +comma-separated-tokens@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz#4e89c9458acb61bc8fef19f4529973b2392839ee" + integrity sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg== + commander@2.17.x: version "2.17.1" resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf" @@ -7922,6 +7970,13 @@ decimal.js@^10.2.1: resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.4.3.tgz#1044092884d245d1b7f65725fa4ad4c6f781cc23" integrity sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA== +decode-named-character-reference@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz#daabac9690874c394c81e4162a0304b35d824f0e" + integrity sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg== + dependencies: + character-entities "^2.0.0" + decode-uri-component@^0.2.0: version "0.2.2" resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.2.tgz#e69dbe25d37941171dd540e024c444cd5188e1e9" @@ -8210,7 +8265,7 @@ deprecation@^2.0.0: resolved "https://registry.yarnpkg.com/deprecation/-/deprecation-2.3.1.tgz#6368cbdb40abf3373b525ac87e4a260c3a700919" integrity sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ== -dequal@^2.0.2, dequal@^2.0.3: +dequal@^2.0.0, dequal@^2.0.2, dequal@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== @@ -8233,13 +8288,6 @@ destroy@~1.0.4: resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" integrity sha512-3NdhDuEXnfun/z7x9GOElY49LoqVHoGScmOKwmxhsS8N5Y+Z8KyPPDnaSzqWgYt/ji4mqwfTS34Htrk0zPIXVg== -detab@^2.0.0: - version "2.0.4" - resolved "https://registry.yarnpkg.com/detab/-/detab-2.0.4.tgz#b927892069aff405fbb9a186fe97a44a92a94b43" - integrity sha512-8zdsQA5bIkoRECvCrNKPla84lyoR7DSAyf7p0YgXzBO9PDJx8KntPUay7NS6yp+KdxdVtiE5SpHKtbp2ZQyA9g== - dependencies: - repeat-string "^1.5.4" - detect-file@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/detect-file/-/detect-file-1.0.0.tgz#f0d66d03672a825cb1b73bdb3fe62310c8e552b7" @@ -8355,6 +8403,13 @@ detective-typescript@^5.8.0: node-source-walk "^4.2.0" typescript "^3.8.3" +devlop@^1.0.0, devlop@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/devlop/-/devlop-1.1.0.tgz#4db7c2ca4dc6e0e834c30be70c94bbc976dc7018" + integrity sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA== + dependencies: + dequal "^2.0.0" + diagnostics@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/diagnostics/-/diagnostics-1.1.1.tgz#cab6ac33df70c9d9a727490ae43ac995a769b22a" @@ -9531,6 +9586,11 @@ estraverse@^5.1.0, estraverse@^5.2.0, estraverse@^5.3.0: resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== +estree-util-is-identifier-name@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz#0b5ef4c4ff13508b34dcd01ecfa945f61fce5dbd" + integrity sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg== + estree-walker@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-1.0.1.tgz#31bc5d612c96b704106b477e6dd5d8aa138cb700" @@ -11208,19 +11268,6 @@ hasown@^2.0.0, hasown@^2.0.1: dependencies: function-bind "^1.1.2" -hast-to-hyperscript@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/hast-to-hyperscript/-/hast-to-hyperscript-4.0.0.tgz#3eb25483ec72a8e9a71e4b1ad7eb8f7c86f755db" - integrity sha512-4kOn4ihjDJTQg7B53ZcZ6NyExtTeG3hLNZv6rSKhq4haQvD52zCllE+49iLiC1VWuc4DbHmt96FHPGlHbslZqQ== - dependencies: - comma-separated-tokens "^1.0.0" - is-nan "^1.2.1" - kebab-case "^1.0.0" - property-information "^3.0.0" - space-separated-tokens "^1.0.0" - trim "0.0.1" - unist-util-is "^2.0.0" - hast-util-from-parse5@^5.0.0: version "5.0.3" resolved "https://registry.yarnpkg.com/hast-util-from-parse5/-/hast-util-from-parse5-5.0.3.tgz#3089dc0ee2ccf6ec8bc416919b51a54a589e097c" @@ -11247,13 +11294,6 @@ hast-util-parse-selector@^2.0.0: resolved "https://registry.yarnpkg.com/hast-util-parse-selector/-/hast-util-parse-selector-2.2.5.tgz#d57c23f4da16ae3c63b3b6ca4616683313499c3a" integrity sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ== -hast-util-sanitize@^1.0.0: - version "1.3.1" - resolved "https://registry.yarnpkg.com/hast-util-sanitize/-/hast-util-sanitize-1.3.1.tgz#4e60d66336bd67e52354d581967467029a933f2e" - integrity sha512-AIeKHuHx0Wk45nSkGVa2/ujQYTksnDl8gmmKo/mwQi7ag7IBZ8cM3nJ2G86SajbjGP/HRpud6kMkPtcM2i0Tlw== - dependencies: - xtend "^4.0.1" - hast-util-to-html@^6.0.0: version "6.1.0" resolved "https://registry.yarnpkg.com/hast-util-to-html/-/hast-util-to-html-6.1.0.tgz#86bcd19c3bd46af456984f8f34db16298c2b10b0" @@ -11270,11 +11310,39 @@ hast-util-to-html@^6.0.0: unist-util-is "^3.0.0" xtend "^4.0.1" +hast-util-to-jsx-runtime@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.0.tgz#3ed27caf8dc175080117706bf7269404a0aa4f7c" + integrity sha512-H/y0+IWPdsLLS738P8tDnrQ8Z+dj12zQQ6WC11TIM21C8WFVoIxcqWXf2H3hiTVZjF1AWqoimGwrTWecWrnmRQ== + dependencies: + "@types/estree" "^1.0.0" + "@types/hast" "^3.0.0" + "@types/unist" "^3.0.0" + comma-separated-tokens "^2.0.0" + devlop "^1.0.0" + estree-util-is-identifier-name "^3.0.0" + hast-util-whitespace "^3.0.0" + mdast-util-mdx-expression "^2.0.0" + mdast-util-mdx-jsx "^3.0.0" + mdast-util-mdxjs-esm "^2.0.0" + property-information "^6.0.0" + space-separated-tokens "^2.0.0" + style-to-object "^1.0.0" + unist-util-position "^5.0.0" + vfile-message "^4.0.0" + hast-util-whitespace@^1.0.0: version "1.0.4" resolved "https://registry.yarnpkg.com/hast-util-whitespace/-/hast-util-whitespace-1.0.4.tgz#e4fe77c4a9ae1cb2e6c25e02df0043d0164f6e41" integrity sha512-I5GTdSfhYfAPNztx2xJRQpG8cuDSNt599/7YUn7Gx/WxNMsG+a835k97TDkFgk123cwjfwINaZknkKkphx/f2A== +hast-util-whitespace@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz#7778ed9d3c92dd9e8c5c8f648a49c21fc51cb621" + integrity sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw== + dependencies: + "@types/hast" "^3.0.0" + hastscript@^5.0.0: version "5.1.2" resolved "https://registry.yarnpkg.com/hastscript/-/hastscript-5.1.2.tgz#bde2c2e56d04c62dd24e8c5df288d050a355fb8a" @@ -11435,6 +11503,11 @@ html-tags@^3.0.0, html-tags@^3.1.0: resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-3.3.1.tgz#a04026a18c882e4bba8a01a3d39cfe465d40b5ce" integrity sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ== +html-url-attributes@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/html-url-attributes/-/html-url-attributes-3.0.0.tgz#fc4abf0c3fb437e2329c678b80abb3c62cff6f08" + integrity sha512-/sXbVCWayk6GDVg3ctOX6nxaVj7So40FcFAnWlWGNAB1LpYKcV5Cd10APjPjW80O7zYW2MsjBV4zZ7IZO5fVow== + html-void-elements@^1.0.0: version "1.0.5" resolved "https://registry.yarnpkg.com/html-void-elements/-/html-void-elements-1.0.5.tgz#ce9159494e86d95e45795b166c2021c2cfca4483" @@ -11807,6 +11880,11 @@ ini@^1.3.2, ini@^1.3.4, ini@^1.3.5, ini@~1.3.0: resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== +inline-style-parser@0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/inline-style-parser/-/inline-style-parser-0.2.3.tgz#e35c5fb45f3a83ed7849fe487336eb7efa25971c" + integrity sha512-qlD8YNDqyTKTyuITrDOffsl6Tdhv+UC4hcdAVuQsK4IMQ99nSgd1MIA/Q+jQYoh9r3hVUXhYh7urSRmXPkW04g== + interactjs@^1.10.17: version "1.10.26" resolved "https://registry.yarnpkg.com/interactjs/-/interactjs-1.10.26.tgz#ad009a46ee3610cb75de6aec22ea6cc0b0e277e2" @@ -11906,6 +11984,11 @@ is-alphabetical@^1.0.0: resolved "https://registry.yarnpkg.com/is-alphabetical/-/is-alphabetical-1.0.4.tgz#9e7d6b94916be22153745d184c298cbf986a686d" integrity sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg== +is-alphabetical@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-alphabetical/-/is-alphabetical-2.0.1.tgz#01072053ea7c1036df3c7d19a6daaec7f19e789b" + integrity sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ== + is-alphanumeric@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-alphanumeric/-/is-alphanumeric-1.0.0.tgz#4a9cef71daf4c001c1d81d63d140cf53fd6889f4" @@ -11919,6 +12002,14 @@ is-alphanumerical@^1.0.0: is-alphabetical "^1.0.0" is-decimal "^1.0.0" +is-alphanumerical@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz#7c03fbe96e3e931113e57f964b0a368cc2dfd875" + integrity sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw== + dependencies: + is-alphabetical "^2.0.0" + is-decimal "^2.0.0" + is-arguments@^1.0.4, is-arguments@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b" @@ -11981,7 +12072,7 @@ is-boolean-object@^1.1.0: call-bind "^1.0.2" has-tostringtag "^1.0.0" -is-buffer@^1.1.4, is-buffer@^1.1.5: +is-buffer@^1.1.5: version "1.1.6" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== @@ -12062,6 +12153,11 @@ is-decimal@^1.0.0, is-decimal@^1.0.2: resolved "https://registry.yarnpkg.com/is-decimal/-/is-decimal-1.0.4.tgz#65a3a5958a1c5b63a706e1b333d7cd9f630d3fa5" integrity sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw== +is-decimal@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-decimal/-/is-decimal-2.0.1.tgz#9469d2dc190d0214fd87d78b78caecc0cc14eef7" + integrity sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A== + is-deflate@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-deflate/-/is-deflate-1.0.0.tgz#c862901c3c161fb09dac7cdc7e784f80e98f2f14" @@ -12165,6 +12261,11 @@ is-hexadecimal@^1.0.0: resolved "https://registry.yarnpkg.com/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz#cc35c97588da4bd49a8eedd6bc4082d44dcb23a7" integrity sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw== +is-hexadecimal@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz#86b5bf668fca307498d319dfc03289d781a90027" + integrity sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg== + is-installed-globally@^0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.3.2.tgz#fd3efa79ee670d1187233182d5b0a1dd00313141" @@ -12200,7 +12301,7 @@ is-module@^1.0.0: resolved "https://registry.yarnpkg.com/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591" integrity sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g== -is-nan@^1.2.1, is-nan@^1.3.2: +is-nan@^1.3.2: version "1.3.2" resolved "https://registry.yarnpkg.com/is-nan/-/is-nan-1.3.2.tgz#043a54adea31748b55b6cd4e09aadafa69bd9e1d" integrity sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w== @@ -12293,6 +12394,11 @@ is-plain-obj@^2.0.0: resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== +is-plain-obj@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-4.1.0.tgz#d65025edec3657ce032fd7db63c97883eaed71f0" + integrity sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg== + is-plain-object@5.0.0, is-plain-object@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344" @@ -12922,11 +13028,6 @@ jszip@^3.1.0: readable-stream "~2.3.6" setimmediate "^1.0.5" -kebab-case@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/kebab-case/-/kebab-case-1.0.2.tgz#5eac97d5d220acf606d40e3c0ecfea21f1f9e1eb" - integrity sha512-7n6wXq4gNgBELfDCpzKc+mRrZFs7D+wgfF5WRFLNAr4DA/qtr9Js8uOAVAfHhuLMfAcQ0pRKqbpjx+TcJVdE1Q== - keyboardevent-from-electron-accelerator@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/keyboardevent-from-electron-accelerator/-/keyboardevent-from-electron-accelerator-2.0.0.tgz#ace21b1aa4e47148815d160057f9edb66567c50c" @@ -13310,6 +13411,11 @@ longest-streak@^2.0.1: resolved "https://registry.yarnpkg.com/longest-streak/-/longest-streak-2.0.4.tgz#b8599957da5b5dab64dee3fe316fa774597d90e4" integrity sha512-vM6rUVCVUJJt33bnmHiZEvr7wPT78ztX7rojL+LW51bHtLh6HTjx84LA5W4+oa6aKEJA7jJu5LR6vQRBpA5DVg== +longest-streak@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/longest-streak/-/longest-streak-3.1.0.tgz#62fa67cd958742a1574af9f39866364102d90cd4" + integrity sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g== + longest@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097" @@ -13607,13 +13713,6 @@ mdast-util-compact@^1.0.0: dependencies: unist-util-visit "^1.1.0" -mdast-util-definitions@^1.2.0: - version "1.2.5" - resolved "https://registry.yarnpkg.com/mdast-util-definitions/-/mdast-util-definitions-1.2.5.tgz#3fe622a4171c774ebd06f11e9f8af7ec53ea5c74" - integrity sha512-CJXEdoLfiISCDc2JB6QLb79pYfI6+GcIH+W2ox9nMc7od0Pz+bovcHsiq29xAQY6ayqe/9CsK2VzkSJdg1pFYA== - dependencies: - unist-util-visit "^1.0.0" - mdast-util-definitions@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/mdast-util-definitions/-/mdast-util-definitions-4.0.0.tgz#c5c1a84db799173b4dcf7643cda999e440c24db2" @@ -13621,28 +13720,116 @@ mdast-util-definitions@^4.0.0: dependencies: unist-util-visit "^2.0.0" -mdast-util-to-hast@^3.0.0: - version "3.0.4" - resolved "https://registry.yarnpkg.com/mdast-util-to-hast/-/mdast-util-to-hast-3.0.4.tgz#132001b266031192348d3366a6b011f28e54dc40" - integrity sha512-/eIbly2YmyVgpJNo+bFLLMCI1XgolO/Ffowhf+pHDq3X4/V6FntC9sGQCDLM147eTS+uSXv5dRzJyFn+o0tazA== +mdast-util-from-markdown@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.0.tgz#52f14815ec291ed061f2922fd14d6689c810cb88" + integrity sha512-n7MTOr/z+8NAX/wmhhDji8O3bRvPTV/U0oTCaZJkjhPSKTPhS3xufVhKGF8s1pJ7Ox4QgoIU7KHseh09S+9rTA== dependencies: - collapse-white-space "^1.0.0" - detab "^2.0.0" - mdast-util-definitions "^1.2.0" - mdurl "^1.0.1" - trim "0.0.1" - trim-lines "^1.0.0" - unist-builder "^1.0.1" - unist-util-generated "^1.1.0" - unist-util-position "^3.0.0" - unist-util-visit "^1.1.0" - xtend "^4.0.1" + "@types/mdast" "^4.0.0" + "@types/unist" "^3.0.0" + decode-named-character-reference "^1.0.0" + devlop "^1.0.0" + mdast-util-to-string "^4.0.0" + micromark "^4.0.0" + micromark-util-decode-numeric-character-reference "^2.0.0" + micromark-util-decode-string "^2.0.0" + micromark-util-normalize-identifier "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + unist-util-stringify-position "^4.0.0" + +mdast-util-mdx-expression@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.0.tgz#4968b73724d320a379110d853e943a501bfd9d87" + integrity sha512-fGCu8eWdKUKNu5mohVGkhBXCXGnOTLuFqOvGMvdikr+J1w7lDJgxThOKpwRWzzbyXAU2hhSwsmssOY4yTokluw== + dependencies: + "@types/estree-jsx" "^1.0.0" + "@types/hast" "^3.0.0" + "@types/mdast" "^4.0.0" + devlop "^1.0.0" + mdast-util-from-markdown "^2.0.0" + mdast-util-to-markdown "^2.0.0" + +mdast-util-mdx-jsx@^3.0.0: + version "3.1.2" + resolved "https://registry.yarnpkg.com/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.1.2.tgz#daae777c72f9c4a106592e3025aa50fb26068e1b" + integrity sha512-eKMQDeywY2wlHc97k5eD8VC+9ASMjN8ItEZQNGwJ6E0XWKiW/Z0V5/H8pvoXUf+y+Mj0VIgeRRbujBmFn4FTyA== + dependencies: + "@types/estree-jsx" "^1.0.0" + "@types/hast" "^3.0.0" + "@types/mdast" "^4.0.0" + "@types/unist" "^3.0.0" + ccount "^2.0.0" + devlop "^1.1.0" + mdast-util-from-markdown "^2.0.0" + mdast-util-to-markdown "^2.0.0" + parse-entities "^4.0.0" + stringify-entities "^4.0.0" + unist-util-remove-position "^5.0.0" + unist-util-stringify-position "^4.0.0" + vfile-message "^4.0.0" + +mdast-util-mdxjs-esm@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz#019cfbe757ad62dd557db35a695e7314bcc9fa97" + integrity sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg== + dependencies: + "@types/estree-jsx" "^1.0.0" + "@types/hast" "^3.0.0" + "@types/mdast" "^4.0.0" + devlop "^1.0.0" + mdast-util-from-markdown "^2.0.0" + mdast-util-to-markdown "^2.0.0" + +mdast-util-phrasing@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz#7cc0a8dec30eaf04b7b1a9661a92adb3382aa6e3" + integrity sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w== + dependencies: + "@types/mdast" "^4.0.0" + unist-util-is "^6.0.0" + +mdast-util-to-hast@^13.0.0: + version "13.1.0" + resolved "https://registry.yarnpkg.com/mdast-util-to-hast/-/mdast-util-to-hast-13.1.0.tgz#1ae54d903150a10fe04d59f03b2b95fd210b2124" + integrity sha512-/e2l/6+OdGp/FB+ctrJ9Avz71AN/GRH3oi/3KAx/kMnoUsD6q0woXlDT8lLEeViVKE7oZxE7RXzvO3T8kF2/sA== + dependencies: + "@types/hast" "^3.0.0" + "@types/mdast" "^4.0.0" + "@ungap/structured-clone" "^1.0.0" + devlop "^1.0.0" + micromark-util-sanitize-uri "^2.0.0" + trim-lines "^3.0.0" + unist-util-position "^5.0.0" + unist-util-visit "^5.0.0" + vfile "^6.0.0" + +mdast-util-to-markdown@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.0.tgz#9813f1d6e0cdaac7c244ec8c6dabfdb2102ea2b4" + integrity sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ== + dependencies: + "@types/mdast" "^4.0.0" + "@types/unist" "^3.0.0" + longest-streak "^3.0.0" + mdast-util-phrasing "^4.0.0" + mdast-util-to-string "^4.0.0" + micromark-util-decode-string "^2.0.0" + unist-util-visit "^5.0.0" + zwitch "^2.0.0" mdast-util-to-string@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/mdast-util-to-string/-/mdast-util-to-string-1.1.0.tgz#27055500103f51637bd07d01da01eb1967a43527" integrity sha512-jVU0Nr2B9X3MU4tSK7JP1CMkSvOj7X5l/GboG1tKRw52lLF1x2Ju92Ms9tNetCcbfX3hzlM73zYo2NKkWSfF/A== +mdast-util-to-string@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz#7a5121475556a04e7eddeb67b264aae79d312814" + integrity sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg== + dependencies: + "@types/mdast" "^4.0.0" + mdn-data@2.0.14: version "2.0.14" resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50" @@ -13667,11 +13854,6 @@ mdns-js@1.0.1: dns-js "~0.2.1" semver "^5.4.1" -mdurl@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e" - integrity sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g== - media-typer@0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" @@ -13764,6 +13946,200 @@ methods@~1.1.2: resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== +micromark-core-commonmark@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-core-commonmark/-/micromark-core-commonmark-2.0.0.tgz#50740201f0ee78c12a675bf3e68ffebc0bf931a3" + integrity sha512-jThOz/pVmAYUtkroV3D5c1osFXAMv9e0ypGDOIZuCeAe91/sD6BoE2Sjzt30yuXtwOYUmySOhMas/PVyh02itA== + dependencies: + decode-named-character-reference "^1.0.0" + devlop "^1.0.0" + micromark-factory-destination "^2.0.0" + micromark-factory-label "^2.0.0" + micromark-factory-space "^2.0.0" + micromark-factory-title "^2.0.0" + micromark-factory-whitespace "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-chunked "^2.0.0" + micromark-util-classify-character "^2.0.0" + micromark-util-html-tag-name "^2.0.0" + micromark-util-normalize-identifier "^2.0.0" + micromark-util-resolve-all "^2.0.0" + micromark-util-subtokenize "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-factory-destination@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-factory-destination/-/micromark-factory-destination-2.0.0.tgz#857c94debd2c873cba34e0445ab26b74f6a6ec07" + integrity sha512-j9DGrQLm/Uhl2tCzcbLhy5kXsgkHUrjJHg4fFAeoMRwJmJerT9aw4FEhIbZStWN8A3qMwOp1uzHr4UL8AInxtA== + dependencies: + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-factory-label@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-factory-label/-/micromark-factory-label-2.0.0.tgz#17c5c2e66ce39ad6f4fc4cbf40d972f9096f726a" + integrity sha512-RR3i96ohZGde//4WSe/dJsxOX6vxIg9TimLAS3i4EhBAFx8Sm5SmqVfR8E87DPSR31nEAjZfbt91OMZWcNgdZw== + dependencies: + devlop "^1.0.0" + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-factory-space@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-factory-space/-/micromark-factory-space-2.0.0.tgz#5e7afd5929c23b96566d0e1ae018ae4fcf81d030" + integrity sha512-TKr+LIDX2pkBJXFLzpyPyljzYK3MtmllMUMODTQJIUfDGncESaqB90db9IAUcz4AZAJFdd8U9zOp9ty1458rxg== + dependencies: + micromark-util-character "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-factory-title@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-factory-title/-/micromark-factory-title-2.0.0.tgz#726140fc77892af524705d689e1cf06c8a83ea95" + integrity sha512-jY8CSxmpWLOxS+t8W+FG3Xigc0RDQA9bKMY/EwILvsesiRniiVMejYTE4wumNc2f4UbAa4WsHqe3J1QS1sli+A== + dependencies: + micromark-factory-space "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-factory-whitespace@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.0.tgz#9e92eb0f5468083381f923d9653632b3cfb5f763" + integrity sha512-28kbwaBjc5yAI1XadbdPYHX/eDnqaUFVikLwrO7FDnKG7lpgxnvk/XGRhX/PN0mOZ+dBSZ+LgunHS+6tYQAzhA== + dependencies: + micromark-factory-space "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-util-character@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/micromark-util-character/-/micromark-util-character-2.1.0.tgz#31320ace16b4644316f6bf057531689c71e2aee1" + integrity sha512-KvOVV+X1yLBfs9dCBSopq/+G1PcgT3lAK07mC4BzXi5E7ahzMAF8oIupDDJ6mievI6F+lAATkbQQlQixJfT3aQ== + dependencies: + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-util-chunked@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-util-chunked/-/micromark-util-chunked-2.0.0.tgz#e51f4db85fb203a79dbfef23fd41b2f03dc2ef89" + integrity sha512-anK8SWmNphkXdaKgz5hJvGa7l00qmcaUQoMYsBwDlSKFKjc6gjGXPDw3FNL3Nbwq5L8gE+RCbGqTw49FK5Qyvg== + dependencies: + micromark-util-symbol "^2.0.0" + +micromark-util-classify-character@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-util-classify-character/-/micromark-util-classify-character-2.0.0.tgz#8c7537c20d0750b12df31f86e976d1d951165f34" + integrity sha512-S0ze2R9GH+fu41FA7pbSqNWObo/kzwf8rN/+IGlW/4tC6oACOs8B++bh+i9bVyNnwCcuksbFwsBme5OCKXCwIw== + dependencies: + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-util-combine-extensions@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.0.tgz#75d6ab65c58b7403616db8d6b31315013bfb7ee5" + integrity sha512-vZZio48k7ON0fVS3CUgFatWHoKbbLTK/rT7pzpJ4Bjp5JjkZeasRfrS9wsBdDJK2cJLHMckXZdzPSSr1B8a4oQ== + dependencies: + micromark-util-chunked "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-util-decode-numeric-character-reference@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.1.tgz#2698bbb38f2a9ba6310e359f99fcb2b35a0d2bd5" + integrity sha512-bmkNc7z8Wn6kgjZmVHOX3SowGmVdhYS7yBpMnuMnPzDq/6xwVA604DuOXMZTO1lvq01g+Adfa0pE2UKGlxL1XQ== + dependencies: + micromark-util-symbol "^2.0.0" + +micromark-util-decode-string@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-util-decode-string/-/micromark-util-decode-string-2.0.0.tgz#7dfa3a63c45aecaa17824e656bcdb01f9737154a" + integrity sha512-r4Sc6leeUTn3P6gk20aFMj2ntPwn6qpDZqWvYmAG6NgvFTIlj4WtrAudLi65qYoaGdXYViXYw2pkmn7QnIFasA== + dependencies: + decode-named-character-reference "^1.0.0" + micromark-util-character "^2.0.0" + micromark-util-decode-numeric-character-reference "^2.0.0" + micromark-util-symbol "^2.0.0" + +micromark-util-encode@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-util-encode/-/micromark-util-encode-2.0.0.tgz#0921ac7953dc3f1fd281e3d1932decfdb9382ab1" + integrity sha512-pS+ROfCXAGLWCOc8egcBvT0kf27GoWMqtdarNfDcjb6YLuV5cM3ioG45Ys2qOVqeqSbjaKg72vU+Wby3eddPsA== + +micromark-util-html-tag-name@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.0.tgz#ae34b01cbe063363847670284c6255bb12138ec4" + integrity sha512-xNn4Pqkj2puRhKdKTm8t1YHC/BAjx6CEwRFXntTaRf/x16aqka6ouVoutm+QdkISTlT7e2zU7U4ZdlDLJd2Mcw== + +micromark-util-normalize-identifier@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.0.tgz#91f9a4e65fe66cc80c53b35b0254ad67aa431d8b" + integrity sha512-2xhYT0sfo85FMrUPtHcPo2rrp1lwbDEEzpx7jiH2xXJLqBuy4H0GgXk5ToU8IEwoROtXuL8ND0ttVa4rNqYK3w== + dependencies: + micromark-util-symbol "^2.0.0" + +micromark-util-resolve-all@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.0.tgz#189656e7e1a53d0c86a38a652b284a252389f364" + integrity sha512-6KU6qO7DZ7GJkaCgwBNtplXCvGkJToU86ybBAUdavvgsCiG8lSSvYxr9MhwmQ+udpzywHsl4RpGJsYWG1pDOcA== + dependencies: + micromark-util-types "^2.0.0" + +micromark-util-sanitize-uri@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.0.tgz#ec8fbf0258e9e6d8f13d9e4770f9be64342673de" + integrity sha512-WhYv5UEcZrbAtlsnPuChHUAsu/iBPOVaEVsntLBIdpibO0ddy8OzavZz3iL2xVvBZOpolujSliP65Kq0/7KIYw== + dependencies: + micromark-util-character "^2.0.0" + micromark-util-encode "^2.0.0" + micromark-util-symbol "^2.0.0" + +micromark-util-subtokenize@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-util-subtokenize/-/micromark-util-subtokenize-2.0.1.tgz#76129c49ac65da6e479c09d0ec4b5f29ec6eace5" + integrity sha512-jZNtiFl/1aY73yS3UGQkutD0UbhTt68qnRpw2Pifmz5wV9h8gOVsN70v+Lq/f1rKaU/W8pxRe8y8Q9FX1AOe1Q== + dependencies: + devlop "^1.0.0" + micromark-util-chunked "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-util-symbol@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-util-symbol/-/micromark-util-symbol-2.0.0.tgz#12225c8f95edf8b17254e47080ce0862d5db8044" + integrity sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw== + +micromark-util-types@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-util-types/-/micromark-util-types-2.0.0.tgz#63b4b7ffeb35d3ecf50d1ca20e68fc7caa36d95e" + integrity sha512-oNh6S2WMHWRZrmutsRmDDfkzKtxF+bc2VxLC9dvtrDIRFln627VsFP6fLMgTryGDljgLPjkrzQSDcPrjPyDJ5w== + +micromark@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/micromark/-/micromark-4.0.0.tgz#84746a249ebd904d9658cfabc1e8e5f32cbc6249" + integrity sha512-o/sd0nMof8kYff+TqcDx3VSrgBTcZpSvYcAHIfHhv5VAuNmisCxjhx6YmxS8PFEpb9z5WKWKPdzf0jM23ro3RQ== + dependencies: + "@types/debug" "^4.0.0" + debug "^4.0.0" + decode-named-character-reference "^1.0.0" + devlop "^1.0.0" + micromark-core-commonmark "^2.0.0" + micromark-factory-space "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-chunked "^2.0.0" + micromark-util-combine-extensions "^2.0.0" + micromark-util-decode-numeric-character-reference "^2.0.0" + micromark-util-encode "^2.0.0" + micromark-util-normalize-identifier "^2.0.0" + micromark-util-resolve-all "^2.0.0" + micromark-util-sanitize-uri "^2.0.0" + micromark-util-subtokenize "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + micromatch@^3.0.4, micromatch@^3.1.10, micromatch@^3.1.4: version "3.1.10" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" @@ -15040,6 +15416,20 @@ parse-entities@^1.0.2, parse-entities@^1.1.0: is-decimal "^1.0.0" is-hexadecimal "^1.0.0" +parse-entities@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/parse-entities/-/parse-entities-4.0.1.tgz#4e2a01111fb1c986549b944af39eeda258fc9e4e" + integrity sha512-SWzvYcSJh4d/SGLIOQfZ/CoNv6BTlI6YEQ7Nj82oDVnRpwe/Z/F1EMx42x3JAOwGBlCjeCH0BRJQbQ/opHL17w== + dependencies: + "@types/unist" "^2.0.0" + character-entities "^2.0.0" + character-entities-legacy "^3.0.0" + character-reference-invalid "^2.0.0" + decode-named-character-reference "^1.0.0" + is-alphanumerical "^2.0.0" + is-decimal "^2.0.0" + is-hexadecimal "^2.0.0" + parse-json@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0" @@ -16222,11 +16612,6 @@ property-expr@^2.0.4, property-expr@^2.0.5: resolved "https://registry.yarnpkg.com/property-expr/-/property-expr-2.0.6.tgz#f77bc00d5928a6c748414ad12882e83f24aec1e8" integrity sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA== -property-information@^3.0.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/property-information/-/property-information-3.2.0.tgz#fd1483c8fbac61808f5fe359e7693a1f48a58331" - integrity sha512-BKU45RMZAA+3npkQ/VxEH7EeZImQcfV6rfKH0O4HkkDz3uqqz+689dbkjiWia00vK390MY6EARPS6TzNS4tXPg== - property-information@^5.0.0, property-information@^5.2.0: version "5.6.0" resolved "https://registry.yarnpkg.com/property-information/-/property-information-5.6.0.tgz#61675545fb23002f245c6540ec46077d4da3ed69" @@ -16234,6 +16619,11 @@ property-information@^5.0.0, property-information@^5.2.0: dependencies: xtend "^4.0.0" +property-information@^6.0.0: + version "6.5.0" + resolved "https://registry.yarnpkg.com/property-information/-/property-information-6.5.0.tgz#6212fbb52ba757e92ef4fb9d657563b933b7ffec" + integrity sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig== + proxy-addr@~2.0.4, proxy-addr@~2.0.7: version "2.0.7" resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" @@ -16654,6 +17044,22 @@ react-is@^18.0.0: resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b" integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== +react-markdown@9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/react-markdown/-/react-markdown-9.0.1.tgz#c05ddbff67fd3b3f839f8c648e6fb35d022397d1" + integrity sha512-186Gw/vF1uRkydbsOIkcGXw7aHq0sZOCRFFjGrr7b9+nVZg4UfA4enXCaxm4fUzecU38sWfrNDitGhshuU7rdg== + dependencies: + "@types/hast" "^3.0.0" + devlop "^1.0.0" + hast-util-to-jsx-runtime "^2.0.0" + html-url-attributes "^3.0.0" + mdast-util-to-hast "^13.0.0" + remark-parse "^11.0.0" + remark-rehype "^11.0.0" + unified "^11.0.0" + unist-util-visit "^5.0.0" + vfile "^6.0.0" + react-popper@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-1.0.0.tgz#b99452144e8fe4acc77fa3d959a8c79e07a65084" @@ -17148,26 +17554,15 @@ remark-external-links@^8.0.0: space-separated-tokens "^1.0.0" unist-util-visit "^2.0.0" -remark-parse@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/remark-parse/-/remark-parse-5.0.0.tgz#4c077f9e499044d1d5c13f80d7a98cf7b9285d95" - integrity sha512-b3iXszZLH1TLoyUzrATcTQUZrwNl1rE70rVdSruJFlDaJ9z5aMkhrG43Pp68OgfHndL/ADz6V69Zow8cTQu+JA== +remark-parse@^11.0.0: + version "11.0.0" + resolved "https://registry.yarnpkg.com/remark-parse/-/remark-parse-11.0.0.tgz#aa60743fcb37ebf6b069204eb4da304e40db45a1" + integrity sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA== dependencies: - collapse-white-space "^1.0.2" - is-alphabetical "^1.0.0" - is-decimal "^1.0.0" - is-whitespace-character "^1.0.0" - is-word-character "^1.0.0" - markdown-escapes "^1.0.0" - parse-entities "^1.1.0" - repeat-string "^1.5.4" - state-toggle "^1.0.0" - trim "0.0.1" - trim-trailing-lines "^1.0.0" - unherit "^1.0.4" - unist-util-remove-position "^1.0.0" - vfile-location "^2.0.0" - xtend "^4.0.1" + "@types/mdast" "^4.0.0" + mdast-util-from-markdown "^2.0.0" + micromark-util-types "^2.0.0" + unified "^11.0.0" remark-parse@^6.0.0: version "6.0.3" @@ -17190,15 +17585,16 @@ remark-parse@^6.0.0: vfile-location "^2.0.0" xtend "^4.0.1" -remark-react@4.0.3: - version "4.0.3" - resolved "https://registry.yarnpkg.com/remark-react/-/remark-react-4.0.3.tgz#980938f3bcc93bef220215b26b0b0a80f3158c7d" - integrity sha512-M2DxXfX8/GK0hV84PUcsvkvb+8yGLdV+krb8mW28eoa9ZgTrhC5rk01EPRMXRNGCAEl3JMDFs+VKdT/FbsN9vg== +remark-rehype@^11.0.0: + version "11.1.0" + resolved "https://registry.yarnpkg.com/remark-rehype/-/remark-rehype-11.1.0.tgz#d5f264f42bcbd4d300f030975609d01a1697ccdc" + integrity sha512-z3tJrAs2kIs1AqIIy6pzHmAHlF1hWQ+OdY4/hv+Wxe35EhyLKcajL33iUEn3ScxtFox9nUvRufR/Zre8Q08H/g== dependencies: - "@mapbox/hast-util-table-cell-style" "^0.1.3" - hast-to-hyperscript "^4.0.0" - hast-util-sanitize "^1.0.0" - mdast-util-to-hast "^3.0.0" + "@types/hast" "^3.0.0" + "@types/mdast" "^4.0.0" + mdast-util-to-hast "^13.0.0" + unified "^11.0.0" + vfile "^6.0.0" remark-slug@^6.0.0: version "6.1.0" @@ -17209,26 +17605,6 @@ remark-slug@^6.0.0: mdast-util-to-string "^1.0.0" unist-util-visit "^2.0.0" -remark-stringify@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/remark-stringify/-/remark-stringify-5.0.0.tgz#336d3a4d4a6a3390d933eeba62e8de4bd280afba" - integrity sha512-Ws5MdA69ftqQ/yhRF9XhVV29mhxbfGhbz0Rx5bQH+oJcNhhSM6nCu1EpLod+DjrFGrU0BMPs+czVmJZU7xiS7w== - dependencies: - ccount "^1.0.0" - is-alphanumeric "^1.0.0" - is-decimal "^1.0.0" - is-whitespace-character "^1.0.0" - longest-streak "^2.0.1" - markdown-escapes "^1.0.0" - markdown-table "^1.1.0" - mdast-util-compact "^1.0.0" - parse-entities "^1.0.2" - repeat-string "^1.5.4" - state-toggle "^1.0.0" - stringify-entities "^1.0.1" - unherit "^1.0.4" - xtend "^4.0.1" - remark-stringify@^6.0.0: version "6.0.4" resolved "https://registry.yarnpkg.com/remark-stringify/-/remark-stringify-6.0.4.tgz#16ac229d4d1593249018663c7bddf28aafc4e088" @@ -17249,15 +17625,6 @@ remark-stringify@^6.0.0: unherit "^1.0.4" xtend "^4.0.1" -remark@9.0.0: - version "9.0.0" - resolved "https://registry.yarnpkg.com/remark/-/remark-9.0.0.tgz#c5cfa8ec535c73a67c4b0f12bfdbd3a67d8b2f60" - integrity sha512-amw8rGdD5lHbMEakiEsllmkdBP+/KpjW/PRK6NSGPZKCQowh0BT4IWXDAkRMyG3SB9dKPXWMviFjNusXzXNn3A== - dependencies: - remark-parse "^5.0.0" - remark-stringify "^5.0.0" - unified "^6.0.0" - remark@^10.0.1: version "10.0.1" resolved "https://registry.yarnpkg.com/remark/-/remark-10.0.1.tgz#3058076dc41781bf505d8978c291485fe47667df" @@ -18327,6 +18694,11 @@ space-separated-tokens@^1.0.0: resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz#85f32c3d10d9682007e917414ddc5c26d1aa6899" integrity sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA== +space-separated-tokens@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz#1ecd9d2350a3844572c3f4a312bceb018348859f" + integrity sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q== + spawn-command@0.0.2: version "0.0.2" resolved "https://registry.yarnpkg.com/spawn-command/-/spawn-command-0.0.2.tgz#9544e1a43ca045f8531aac1a48cb29bdae62338e" @@ -18721,6 +19093,14 @@ stringify-entities@^2.0.0: is-decimal "^1.0.2" is-hexadecimal "^1.0.0" +stringify-entities@^4.0.0: + version "4.0.4" + resolved "https://registry.yarnpkg.com/stringify-entities/-/stringify-entities-4.0.4.tgz#b3b79ef5f277cc4ac73caeb0236c5ba939b3a4f3" + integrity sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg== + dependencies: + character-entities-html4 "^2.0.0" + character-entities-legacy "^3.0.0" + stringify-object@^3.2.1: version "3.3.0" resolved "https://registry.yarnpkg.com/stringify-object/-/stringify-object-3.3.0.tgz#703065aefca19300d3ce88af4f5b3956d7556629" @@ -18848,6 +19228,13 @@ style-search@^0.1.0: resolved "https://registry.yarnpkg.com/style-search/-/style-search-0.1.0.tgz#7958c793e47e32e07d2b5cafe5c0bf8e12e77902" integrity sha512-Dj1Okke1C3uKKwQcetra4jSuk0DqbzbYtXipzFlFMZtowbF1x7BKJwB9AayVMyFARvU8EDrZdcax4At/452cAg== +style-to-object@^1.0.0: + version "1.0.6" + resolved "https://registry.yarnpkg.com/style-to-object/-/style-to-object-1.0.6.tgz#0c28aed8be1813d166c60d962719b2907c26547b" + integrity sha512-khxq+Qm3xEyZfKd/y9L3oIWQimxuc4STrQKtQn8aSDRHb8mFgpukgX1hdzfrMEW6JCjyJ8p89x+IUMVnCBI1PA== + dependencies: + inline-style-parser "0.2.3" + styled-components@5.3.6: version "5.3.6" resolved "https://registry.yarnpkg.com/styled-components/-/styled-components-5.3.6.tgz#27753c8c27c650bee9358e343fc927966bfd00d1" @@ -19441,10 +19828,10 @@ tree-kill@^1.2.2: resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc" integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A== -trim-lines@^1.0.0: - version "1.1.3" - resolved "https://registry.yarnpkg.com/trim-lines/-/trim-lines-1.1.3.tgz#839514be82428fd9e7ec89e35081afe8f6f93115" - integrity sha512-E0ZosSWYK2mkSu+KEtQ9/KqarVjA9HztOSX+9FDdNacRAq29RRV6ZQNgob3iuW8Htar9vAfEa6yyt5qBAHZDBA== +trim-lines@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/trim-lines/-/trim-lines-3.0.1.tgz#d802e332a07df861c48802c04321017b1bd87338" + integrity sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg== trim-newlines@^2.0.0: version "2.0.0" @@ -19483,6 +19870,11 @@ trough@^1.0.0: resolved "https://registry.yarnpkg.com/trough/-/trough-1.0.5.tgz#b8b639cefad7d0bb2abd37d433ff8293efa5f406" integrity sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA== +trough@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/trough/-/trough-2.2.0.tgz#94a60bd6bd375c152c1df911a4b11d5b0256f50f" + integrity sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw== + truncate-utf8-bytes@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz#405923909592d56f78a5818434b0b78489ca5f2b" @@ -19762,17 +20154,18 @@ unicode-property-aliases-ecmascript@^2.0.0: resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz#43d41e3be698bd493ef911077c9b131f827e8ccd" integrity sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w== -unified@^6.0.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/unified/-/unified-6.2.0.tgz#7fbd630f719126d67d40c644b7e3f617035f6dba" - integrity sha512-1k+KPhlVtqmG99RaTbAv/usu85fcSRu3wY8X+vnsEhIxNP5VbVIDiXnLqyKIG+UMdyTg0ZX9EI6k2AfjJkHPtA== +unified@^11.0.0: + version "11.0.4" + resolved "https://registry.yarnpkg.com/unified/-/unified-11.0.4.tgz#f4be0ac0fe4c88cb873687c07c64c49ed5969015" + integrity sha512-apMPnyLjAX+ty4OrNap7yumyVAMlKx5IWU2wlzzUdYJO9A8f1p9m/gywF/GM2ZDFcjQPrx59Mc90KwmxsoklxQ== dependencies: - bail "^1.0.0" + "@types/unist" "^3.0.0" + bail "^2.0.0" + devlop "^1.0.0" extend "^3.0.0" - is-plain-obj "^1.1.0" - trough "^1.0.0" - vfile "^2.0.0" - x-is-string "^0.1.0" + is-plain-obj "^4.0.0" + trough "^2.0.0" + vfile "^6.0.0" unified@^7.0.0: version "7.1.0" @@ -19854,13 +20247,6 @@ unique-string@^2.0.0: dependencies: crypto-random-string "^2.0.0" -unist-builder@^1.0.1: - version "1.0.4" - resolved "https://registry.yarnpkg.com/unist-builder/-/unist-builder-1.0.4.tgz#e1808aed30bd72adc3607f25afecebef4dd59e17" - integrity sha512-v6xbUPP7ILrT15fHGrNyHc1Xda8H3xVhP7/HAIotHOhVPjH5dCXA097C3Rry1Q2O+HbOLCao4hfPB+EYEjHgVg== - dependencies: - object-assign "^4.1.0" - unist-util-find-all-after@^1.0.2: version "1.0.5" resolved "https://registry.yarnpkg.com/unist-util-find-all-after/-/unist-util-find-all-after-1.0.5.tgz#5751a8608834f41d117ad9c577770c5f2f1b2899" @@ -19868,16 +20254,6 @@ unist-util-find-all-after@^1.0.2: dependencies: unist-util-is "^3.0.0" -unist-util-generated@^1.1.0: - version "1.1.6" - resolved "https://registry.yarnpkg.com/unist-util-generated/-/unist-util-generated-1.1.6.tgz#5ab51f689e2992a472beb1b35f2ce7ff2f324d4b" - integrity sha512-cln2Mm1/CZzN5ttGK7vkoGw+RZ8VcUH6BtGbq98DDtRGquAAOXig1mrBQYelOwMXYS8rK+vZDyyojSjp7JX+Lg== - -unist-util-is@^2.0.0: - version "2.1.3" - resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-2.1.3.tgz#459182db31f4742fceaea88d429693cbf0043d20" - integrity sha512-4WbQX2iwfr/+PfM4U3zd2VNXY+dWtZsN1fLnWEi2QQXA4qyDYAZcDMfXUX0Cu6XZUHHAO9q4nyxxLT4Awk1qUA== - unist-util-is@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-3.0.0.tgz#d9e84381c2468e82629e4a5be9d7d05a2dd324cd" @@ -19888,10 +20264,19 @@ unist-util-is@^4.0.0: resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-4.1.0.tgz#976e5f462a7a5de73d94b706bac1b90671b57797" integrity sha512-ZOQSsnce92GrxSqlnEEseX0gi7GH9zTJZ0p9dtu87WRb/37mMPO2Ilx1s/t9vBHrFhbgweUwb+t7cIn5dxPhZg== -unist-util-position@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/unist-util-position/-/unist-util-position-3.1.0.tgz#1c42ee6301f8d52f47d14f62bbdb796571fa2d47" - integrity sha512-w+PkwCbYSFw8vpgWD0v7zRCl1FpY3fjDSQ3/N/wNd9Ffa4gPi8+4keqt99N3XW6F99t/mUzp2xAhNmfKWp95QA== +unist-util-is@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-6.0.0.tgz#b775956486aff107a9ded971d996c173374be424" + integrity sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw== + dependencies: + "@types/unist" "^3.0.0" + +unist-util-position@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/unist-util-position/-/unist-util-position-5.0.0.tgz#678f20ab5ca1207a97d7ea8a388373c9cf896be4" + integrity sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA== + dependencies: + "@types/unist" "^3.0.0" unist-util-remove-position@^1.0.0: version "1.1.4" @@ -19900,6 +20285,14 @@ unist-util-remove-position@^1.0.0: dependencies: unist-util-visit "^1.1.0" +unist-util-remove-position@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/unist-util-remove-position/-/unist-util-remove-position-5.0.0.tgz#fea68a25658409c9460408bc6b4991b965b52163" + integrity sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q== + dependencies: + "@types/unist" "^3.0.0" + unist-util-visit "^5.0.0" + unist-util-stringify-position@^1.0.0, unist-util-stringify-position@^1.1.1: version "1.1.2" resolved "https://registry.yarnpkg.com/unist-util-stringify-position/-/unist-util-stringify-position-1.1.2.tgz#3f37fcf351279dcbca7480ab5889bb8a832ee1c6" @@ -19934,7 +20327,15 @@ unist-util-visit-parents@^3.0.0: "@types/unist" "^2.0.0" unist-util-is "^4.0.0" -unist-util-visit@^1.0.0, unist-util-visit@^1.1.0, unist-util-visit@^1.3.0, unist-util-visit@^1.4.0: +unist-util-visit-parents@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz#4d5f85755c3b8f0dc69e21eca5d6d82d22162815" + integrity sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw== + dependencies: + "@types/unist" "^3.0.0" + unist-util-is "^6.0.0" + +unist-util-visit@^1.1.0, unist-util-visit@^1.4.0: version "1.4.1" resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-1.4.1.tgz#4724aaa8486e6ee6e26d7ff3c8685960d560b1e3" integrity sha512-AvGNk7Bb//EmJZyhtRUnNMEpId/AZ5Ph/KUpTI09WHQuDZHKovQ1oEv3mfmKpWKtoMzyMC4GLBm1Zy5k12fjIw== @@ -19950,6 +20351,15 @@ unist-util-visit@^2.0.0: unist-util-is "^4.0.0" unist-util-visit-parents "^3.0.0" +unist-util-visit@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-5.0.0.tgz#a7de1f31f72ffd3519ea71814cccf5fd6a9217d6" + integrity sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg== + dependencies: + "@types/unist" "^3.0.0" + unist-util-is "^6.0.0" + unist-util-visit-parents "^6.0.0" + universal-user-agent@^6.0.0: version "6.0.1" resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-6.0.1.tgz#15f20f55da3c930c57bddbf1734c6654d5fd35aa" @@ -20302,7 +20712,7 @@ vfile-location@^2.0.0: resolved "https://registry.yarnpkg.com/vfile-location/-/vfile-location-2.0.6.tgz#8a274f39411b8719ea5728802e10d9e0dff1519e" integrity sha512-sSFdyCP3G6Ka0CEmN83A2YCMKIieHx0EDaj5IDP4g1pa5ZJ4FJDvpO0WODLxo4LUX4oe52gmSCK7Jw4SBghqxA== -vfile-message@*: +vfile-message@*, vfile-message@^4.0.0: version "4.0.2" resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-4.0.2.tgz#c883c9f677c72c166362fd635f21fc165a7d1181" integrity sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw== @@ -20325,16 +20735,6 @@ vfile-message@^2.0.0: "@types/unist" "^2.0.0" unist-util-stringify-position "^2.0.0" -vfile@^2.0.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/vfile/-/vfile-2.3.0.tgz#e62d8e72b20e83c324bc6c67278ee272488bf84a" - integrity sha512-ASt4mBUHcTpMKD/l5Q+WJXNtshlWxOogYyGYYrg4lt/vuRjC1EFQtlAofL5VmtVNIZJzWYFJjzGWZ0Gw8pzW1w== - dependencies: - is-buffer "^1.1.4" - replace-ext "1.0.0" - unist-util-stringify-position "^1.0.0" - vfile-message "^1.0.0" - vfile@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/vfile/-/vfile-3.0.1.tgz#47331d2abe3282424f4a4bb6acd20a44c4121803" @@ -20355,6 +20755,15 @@ vfile@^4.0.0: unist-util-stringify-position "^2.0.0" vfile-message "^2.0.0" +vfile@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/vfile/-/vfile-6.0.1.tgz#1e8327f41eac91947d4fe9d237a2dd9209762536" + integrity sha512-1bYqc7pt6NIADBJ98UiG0Bn/CHIVOoZ/IyEkqIruLg0mE1BKzkOXY2D6CSqQIcKqgadppE5lrxgWXJmXd7zZJw== + dependencies: + "@types/unist" "^3.0.0" + unist-util-stringify-position "^4.0.0" + vfile-message "^4.0.0" + vite-node@1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-1.2.2.tgz#f6d329b06f9032130ae6eac1dc773f3663903c25" @@ -21217,3 +21626,8 @@ yup@1.3.3: tiny-case "^1.0.3" toposort "^2.0.2" type-fest "^2.19.0" + +zwitch@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-2.0.4.tgz#c827d4b0acb76fc3e685a4c6ec2902d51070e9d7" + integrity sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A== From 0e79a4086a1176b58b9bc728d32c5284fdb3d970 Mon Sep 17 00:00:00 2001 From: koji Date: Thu, 18 Apr 2024 08:55:36 -0400 Subject: [PATCH 160/194] feat(opentrons-ai-client): add ChatContainer component (#14921) * feat(opentrons-ai-client): add ChatContainer component --- opentrons-ai-client/src/App.test.tsx | 15 ++- opentrons-ai-client/src/App.tsx | 11 ++- .../src/molecules/PromptGuide/index.tsx | 98 ++++++++++--------- .../ChatContainer/ChatContainer.stories.tsx | 21 ++++ .../__tests__/ChatContainer.test.tsx | 28 ++++++ .../src/organisms/ChatContainer/index.tsx | 35 +++++++ 6 files changed, 156 insertions(+), 52 deletions(-) create mode 100644 opentrons-ai-client/src/organisms/ChatContainer/ChatContainer.stories.tsx create mode 100644 opentrons-ai-client/src/organisms/ChatContainer/__tests__/ChatContainer.test.tsx create mode 100644 opentrons-ai-client/src/organisms/ChatContainer/index.tsx diff --git a/opentrons-ai-client/src/App.test.tsx b/opentrons-ai-client/src/App.test.tsx index 03b731311c0..4ae3494a53c 100644 --- a/opentrons-ai-client/src/App.test.tsx +++ b/opentrons-ai-client/src/App.test.tsx @@ -1,18 +1,29 @@ import React from 'react' import { screen } from '@testing-library/react' -import { describe, it } from 'vitest' +import { describe, it, vi, beforeEach } from 'vitest' import { renderWithProviders } from './__testing-utils__' +import { SidePanel } from './molecules/SidePanel' +import { ChatContainer } from './organisms/ChatContainer' import { App } from './App' +vi.mock('./molecules/SidePanel') +vi.mock('./organisms/ChatContainer') + const render = (): ReturnType => { return renderWithProviders() } describe('App', () => { + beforeEach(() => { + vi.mocked(SidePanel).mockReturnValue(
    mock SidePanel
    ) + vi.mocked(ChatContainer).mockReturnValue(
    mock ChatContainer
    ) + }) + it('should render text', () => { render() - screen.getByText('Opentrons AI') + screen.getByText('mock SidePanel') + screen.getByText('mock ChatContainer') }) }) diff --git a/opentrons-ai-client/src/App.tsx b/opentrons-ai-client/src/App.tsx index f31fbd35940..268a61b2e7f 100644 --- a/opentrons-ai-client/src/App.tsx +++ b/opentrons-ai-client/src/App.tsx @@ -1,9 +1,14 @@ import React from 'react' -import { Flex, StyledText } from '@opentrons/components' +import { DIRECTION_ROW, Flex } from '@opentrons/components' + +import { SidePanel } from './molecules/SidePanel' +import { ChatContainer } from './organisms/ChatContainer' + export function App(): JSX.Element { return ( - - Opentrons AI + + + ) } diff --git a/opentrons-ai-client/src/molecules/PromptGuide/index.tsx b/opentrons-ai-client/src/molecules/PromptGuide/index.tsx index fb65c615ea3..16d995d5cfa 100644 --- a/opentrons-ai-client/src/molecules/PromptGuide/index.tsx +++ b/opentrons-ai-client/src/molecules/PromptGuide/index.tsx @@ -29,52 +29,55 @@ export function PromptGuide(): JSX.Element { {t('what_typeof_protocol')} - - {t('make_sure_your_prompt')} - - -
      -
    • - {t('metadata')} -
        -
      • - {t('application')} -
      • -
      • - {t('robot')} -
      • -
      • - {t('api')} -
      • -
      -
    • -
    • - {t('ot2_pipettes')} -
    • -
    • - {t('modules')} -
    • -
    • - {t('well_allocations')} -
    • -
    • - , - span: , - }} - /> -
    • -
    • - {t('commands')} -
    • -
    + + + + {t('make_sure_your_prompt')} + + +
      +
    • + {t('metadata')} + +
    • + {t('application')} +
    • +
    • + {t('robot')} +
    • +
    • + {t('api')} +
    • + + +
    • + {t('ot2_pipettes')} +
    • +
    • + {t('modules')} +
    • +
    • + {t('well_allocations')} +
    • +
    • + , + span: , + }} + /> +
    • +
    • + {t('commands')} +
    • +
    +
    = { + title: 'AI/organisms/ChatContainer', + component: ChatContainerComponent, + decorators: [ + Story => ( + + + + ), + ], +} +export default meta +type Story = StoryObj +export const ChatContainer: Story = {} diff --git a/opentrons-ai-client/src/organisms/ChatContainer/__tests__/ChatContainer.test.tsx b/opentrons-ai-client/src/organisms/ChatContainer/__tests__/ChatContainer.test.tsx new file mode 100644 index 00000000000..26eb7b0a2b5 --- /dev/null +++ b/opentrons-ai-client/src/organisms/ChatContainer/__tests__/ChatContainer.test.tsx @@ -0,0 +1,28 @@ +import React from 'react' +import { screen } from '@testing-library/react' +import { describe, it, vi, beforeEach } from 'vitest' +import { renderWithProviders } from '../../../__testing-utils__' +import { i18n } from '../../../i18n' +import { PromptGuide } from '../../../molecules/PromptGuide' +import { ChatContainer } from '../index' + +vi.mock('../../../molecules/PromptGuide') + +const render = (): ReturnType => { + return renderWithProviders(, { + i18nInstance: i18n, + }) +} + +describe('ChatContainer', () => { + beforeEach(() => { + vi.mocked(PromptGuide).mockReturnValue(
    mock PromptGuide
    ) + }) + it('should render prompt guide and text', () => { + render() + screen.getByText('OpentronsAI') + screen.getByText('mock PromptGuide') + }) + + // ToDo (kk:04/16/2024) Add more test cases +}) diff --git a/opentrons-ai-client/src/organisms/ChatContainer/index.tsx b/opentrons-ai-client/src/organisms/ChatContainer/index.tsx new file mode 100644 index 00000000000..2a6542c8e68 --- /dev/null +++ b/opentrons-ai-client/src/organisms/ChatContainer/index.tsx @@ -0,0 +1,35 @@ +import React from 'react' +import { useTranslation } from 'react-i18next' +import { + COLORS, + DIRECTION_COLUMN, + FLEX_MAX_CONTENT, + Flex, + SPACING, + StyledText, +} from '@opentrons/components' +import { PromptGuide } from '../../molecules/PromptGuide' + +export function ChatContainer(): JSX.Element { + const { t } = useTranslation('protocol_generator') + const isDummyInitial = true + return ( + + {/* This will be updated when input textbox and function are implemented */} + {isDummyInitial ? ( + + {t('opentronsai')} + + + ) : null} + + ) +} From 7d6100d97df33399adc448c621b1d75e63b7da18 Mon Sep 17 00:00:00 2001 From: Alise Au <20424172+ahiuchingau@users.noreply.github.com> Date: Thu, 18 Apr 2024 11:37:18 -0400 Subject: [PATCH 161/194] fix(hardware): remove can messenger `motor_enabled` listener (#14943) # Overview When #14479 was added, the motor enable message listener was never cleaned up properly and thus causing the robot to slow down significantly as it runs more and more move commands. --- .../hardware_control/motor_enable_disable.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/hardware/opentrons_hardware/hardware_control/motor_enable_disable.py b/hardware/opentrons_hardware/hardware_control/motor_enable_disable.py index 9928b841da9..32897d16679 100644 --- a/hardware/opentrons_hardware/hardware_control/motor_enable_disable.py +++ b/hardware/opentrons_hardware/hardware_control/motor_enable_disable.py @@ -122,7 +122,9 @@ def _listener(message: MessageDefinition, arb_id: ArbitrationId) -> None: ) else: log.debug("Read motor status terminated, no missing nodes.") - return reported + finally: + can_messenger.remove_listener(_listener) + return reported async def get_tip_motor_enabled( From 58973c63ca5e98c0fb9ff42f77cf7be8d83aa6f7 Mon Sep 17 00:00:00 2001 From: Alise Au <20424172+ahiuchingau@users.noreply.github.com> Date: Thu, 18 Apr 2024 11:38:10 -0400 Subject: [PATCH 162/194] fix(api): retract function should acquire motion lock (#14944) # Overview This PR wraps the hardware controller retract function in the motion lock. # Test Plan # Changelog # Review requests # Risk assessment --- api/src/opentrons/hardware_control/ot3api.py | 21 ++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/api/src/opentrons/hardware_control/ot3api.py b/api/src/opentrons/hardware_control/ot3api.py index 0d97855045f..5edc327ced1 100644 --- a/api/src/opentrons/hardware_control/ot3api.py +++ b/api/src/opentrons/hardware_control/ot3api.py @@ -1655,16 +1655,17 @@ async def retract_axis(self, axis: Axis) -> None: motor_ok = self._backend.check_motor_status([axis]) encoder_ok = self._backend.check_encoder_status([axis]) - if motor_ok and encoder_ok: - # we can move to the home position without checking the limit switch - origin = await self._backend.update_position() - target_pos = {axis: self._backend.home_position()[axis]} - await self._backend.move(origin, target_pos, 400, HWStopCondition.none) - else: - # home the axis - await self._home_axis(axis) - await self._cache_current_position() - await self._cache_encoder_position() + async with self._motion_lock: + if motor_ok and encoder_ok: + # we can move to the home position without checking the limit switch + origin = await self._backend.update_position() + target_pos = {axis: self._backend.home_position()[axis]} + await self._backend.move(origin, target_pos, 400, HWStopCondition.none) + else: + # home the axis + await self._home_axis(axis) + await self._cache_current_position() + await self._cache_encoder_position() # Gantry/frame (i.e. not pipette) config API @property From 585f69e03907cc549d0ff07474c2f3dced8893a8 Mon Sep 17 00:00:00 2001 From: Jethary Rader <66035149+jerader@users.noreply.github.com> Date: Thu, 18 Apr 2024 12:01:07 -0400 Subject: [PATCH 163/194] feat(protocol-designer): edit multiple modules modal + row (#14933) closes AUTH-16 --- .../src/components/EditModules.tsx | 41 ++- .../components/__tests__/EditModules.test.tsx | 25 +- .../EditMultipleModulesModal.tsx | 274 ++++++++++++++++++ .../EditMultipleModulesModal.test.tsx | 106 +++++++ .../components/modules/EditModulesCard.tsx | 30 +- .../components/modules/MultipleModuleRow.tsx | 121 ++++++++ .../__tests__/MultipleModuleRow.test.tsx | 67 +++++ .../src/localization/en/alert.json | 5 + .../src/localization/en/modules.json | 1 + .../src/step-forms/selectors/index.ts | 5 +- protocol-designer/src/step-forms/types.ts | 2 +- 11 files changed, 655 insertions(+), 22 deletions(-) create mode 100644 protocol-designer/src/components/modals/EditModulesModal/EditMultipleModulesModal.tsx create mode 100644 protocol-designer/src/components/modals/EditModulesModal/__tests__/EditMultipleModulesModal.test.tsx create mode 100644 protocol-designer/src/components/modules/MultipleModuleRow.tsx create mode 100644 protocol-designer/src/components/modules/__tests__/MultipleModuleRow.test.tsx diff --git a/protocol-designer/src/components/EditModules.tsx b/protocol-designer/src/components/EditModules.tsx index 9df9defbdd9..7a4ef5b48c7 100644 --- a/protocol-designer/src/components/EditModules.tsx +++ b/protocol-designer/src/components/EditModules.tsx @@ -1,14 +1,21 @@ import * as React from 'react' import { useSelector, useDispatch } from 'react-redux' +import { + FLEX_ROBOT_TYPE, + TEMPERATURE_MODULE_TYPE, +} from '@opentrons/shared-data' import { selectors as stepFormSelectors, actions as stepFormActions, } from '../step-forms' import { moveDeckItem } from '../labware-ingred/actions/actions' +import { getRobotType } from '../file-data/selectors' +import { getEnableMoam } from '../feature-flags/selectors' +import { EditMultipleModulesModal } from './modals/EditModulesModal/EditMultipleModulesModal' import { useBlockingHint } from './Hints/useBlockingHint' import { MagneticModuleWarningModalContent } from './modals/EditModulesModal/MagneticModuleWarningModalContent' import { EditModulesModal } from './modals/EditModulesModal' -import { ModuleModel, ModuleType } from '@opentrons/shared-data' +import type { ModuleModel, ModuleType } from '@opentrons/shared-data' export interface EditModulesProps { moduleToEdit: { @@ -27,6 +34,12 @@ export const EditModules = (props: EditModulesProps): JSX.Element => { const { onCloseClick, moduleToEdit } = props const { moduleId, moduleType } = moduleToEdit const _initialDeckSetup = useSelector(stepFormSelectors.getInitialDeckSetup) + const robotType = useSelector(getRobotType) + const moamFf = useSelector(getEnableMoam) + const showMultipleModuleModal = + robotType === FLEX_ROBOT_TYPE && + moduleType === TEMPERATURE_MODULE_TYPE && + moamFf const moduleOnDeck = moduleId ? _initialDeckSetup.modules[moduleId] : null const [ @@ -74,16 +87,24 @@ export const EditModules = (props: EditModulesProps): JSX.Element => { enabled: changeModuleWarningInfo !== null, }) - return ( - changeModuleWarning ?? ( - + ) + if (showMultipleModuleModal) { + modal = ( + ) - ) + } + return changeModuleWarning ?? modal } diff --git a/protocol-designer/src/components/__tests__/EditModules.test.tsx b/protocol-designer/src/components/__tests__/EditModules.test.tsx index 2cb2ed8c55f..fb183a3e9e6 100644 --- a/protocol-designer/src/components/__tests__/EditModules.test.tsx +++ b/protocol-designer/src/components/__tests__/EditModules.test.tsx @@ -1,19 +1,29 @@ import * as React from 'react' import { screen } from '@testing-library/react' import { vi, beforeEach, describe, it } from 'vitest' +import { + FLEX_ROBOT_TYPE, + OT2_ROBOT_TYPE, + TEMPERATURE_MODULE_TYPE, +} from '@opentrons/shared-data' import { i18n } from '../../localization' import { getInitialDeckSetup } from '../../step-forms/selectors' import { getDismissedHints } from '../../tutorial/selectors' import { EditModules } from '../EditModules' import { EditModulesModal } from '../modals/EditModulesModal' import { renderWithProviders } from '../../__testing-utils__' +import { getEnableMoam } from '../../feature-flags/selectors' +import { getRobotType } from '../../file-data/selectors' +import { EditMultipleModulesModal } from '../modals/EditModulesModal/EditMultipleModulesModal' import type { HintKey } from '../../tutorial' vi.mock('../../step-forms/selectors') +vi.mock('../modals/EditModulesModal/EditMultipleModulesModal') vi.mock('../modals/EditModulesModal') vi.mock('../../tutorial/selectors') - +vi.mock('../../file-data/selectors') +vi.mock('../../feature-flags/selectors') const render = (props: React.ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, @@ -51,11 +61,22 @@ describe('EditModules', () => { vi.mocked(EditModulesModal).mockReturnValue(
    mock EditModulesModal
    ) + vi.mocked(EditMultipleModulesModal).mockReturnValue( +
    mock EditMultipleModulesModal
    + ) vi.mocked(getDismissedHints).mockReturnValue([hintKey]) + vi.mocked(getRobotType).mockReturnValue(OT2_ROBOT_TYPE) + vi.mocked(getEnableMoam).mockReturnValue(true) }) - it('renders the edit modules modal', () => { + it('renders the edit modules modal for single modules', () => { render(props) screen.getByText('mock EditModulesModal') }) + it('renders multiple edit modules modal', () => { + props.moduleToEdit.moduleType = TEMPERATURE_MODULE_TYPE + vi.mocked(getRobotType).mockReturnValue(FLEX_ROBOT_TYPE) + render(props) + screen.getByText('mock EditMultipleModulesModal') + }) }) diff --git a/protocol-designer/src/components/modals/EditModulesModal/EditMultipleModulesModal.tsx b/protocol-designer/src/components/modals/EditModulesModal/EditMultipleModulesModal.tsx new file mode 100644 index 00000000000..cc31c4eb071 --- /dev/null +++ b/protocol-designer/src/components/modals/EditModulesModal/EditMultipleModulesModal.tsx @@ -0,0 +1,274 @@ +import * as React from 'react' +import { useTranslation } from 'react-i18next' +import { useSelector, useDispatch } from 'react-redux' +import { Controller, useForm, useWatch } from 'react-hook-form' +import { + BUTTON_TYPE_SUBMIT, + OutlineButton, + ModalShell, + Flex, + SPACING, + DIRECTION_ROW, + Box, + Text, + ALIGN_CENTER, + JUSTIFY_FLEX_END, + JUSTIFY_END, + DeckConfigurator, + DIRECTION_COLUMN, +} from '@opentrons/components' +import { + DeckConfiguration, + SINGLE_RIGHT_SLOT_FIXTURE, + TEMPERATURE_MODULE_CUTOUTS, + TEMPERATURE_MODULE_TYPE, + TEMPERATURE_MODULE_V2, + TEMPERATURE_MODULE_V2_FIXTURE, +} from '@opentrons/shared-data' +import { createModule, deleteModule } from '../../../step-forms/actions' +import { getLabwareOnSlot, getSlotIsEmpty } from '../../../step-forms' +import { getInitialDeckSetup } from '../../../step-forms/selectors' +import { getLabwareIsCompatible } from '../../../utils/labwareModuleCompatibility' +import { PDAlert } from '../../alerts/PDAlert' +import type { Control, ControllerRenderProps } from 'react-hook-form' +import type { CutoutId, ModuleType } from '@opentrons/shared-data' +import type { ModuleOnDeck } from '../../../step-forms' + +export interface EditMultipleModulesModalValues { + selectedAddressableAreas: string[] +} + +interface EditMultipleModulesModalComponentProps + extends EditMultipleModulesModalProps { + control: Control + moduleLocations: string[] | null +} + +const EditMultipleModulesModalComponent = ( + props: EditMultipleModulesModalComponentProps +): JSX.Element => { + const { t } = useTranslation(['button', 'alert']) + const { + onCloseClick, + allModulesOnDeck, + control, + moduleLocations, + moduleType, + } = props + const initialDeckSetup = useSelector(getInitialDeckSetup) + + const selectedSlots = useWatch({ + control, + name: 'selectedAddressableAreas', + defaultValue: moduleLocations ?? [], + }) + const occupiedCutoutIds = selectedSlots + .map(slot => { + const hasModSlot = + allModulesOnDeck.find( + module => + module.type === moduleType && slot === `cutout${module.slot}` + ) != null + const labwareOnSlot = getLabwareOnSlot(initialDeckSetup, slot) + const isLabwareCompatible = + (labwareOnSlot && + getLabwareIsCompatible(labwareOnSlot.def, moduleType)) ?? + true + const isEmpty = + (getSlotIsEmpty(initialDeckSetup, slot, true) || hasModSlot) && + isLabwareCompatible + + return { slot, isEmpty } + }) + .filter(slot => !slot.isEmpty) + const hasConflictedSlot = occupiedCutoutIds.length > 0 + const mappedModules: DeckConfiguration = + moduleLocations != null + ? moduleLocations.flatMap(location => { + return [ + { + cutoutId: location as CutoutId, + cutoutFixtureId: TEMPERATURE_MODULE_V2_FIXTURE, + }, + ] + }) + : [] + const STANDARD_EMPTY_SLOTS: DeckConfiguration = TEMPERATURE_MODULE_CUTOUTS.map( + cutoutId => ({ + cutoutId, + cutoutFixtureId: SINGLE_RIGHT_SLOT_FIXTURE, + }) + ) + + STANDARD_EMPTY_SLOTS.forEach(emptySlot => { + if ( + !mappedModules.some(({ cutoutId }) => cutoutId === emptySlot.cutoutId) + ) { + mappedModules.push(emptySlot) + } + }) + + const selectableSlots = + mappedModules.length > 0 ? mappedModules : STANDARD_EMPTY_SLOTS + const [updatedSlots, setUpdatedSlots] = React.useState( + selectableSlots + ) + const handleClickAdd = ( + cutoutId: string, + field: ControllerRenderProps< + EditMultipleModulesModalValues, + 'selectedAddressableAreas' + > + ): void => { + const modifiedSlots: DeckConfiguration = updatedSlots.map(slot => { + if (slot.cutoutId === cutoutId) { + return { + ...slot, + cutoutFixtureId: TEMPERATURE_MODULE_V2_FIXTURE, + } + } + return slot + }) + setUpdatedSlots(modifiedSlots) + const updatedSelectedSlots = [...selectedSlots, cutoutId] + field.onChange(updatedSelectedSlots) + } + + const handleClickRemove = ( + cutoutId: string, + field: ControllerRenderProps< + EditMultipleModulesModalValues, + 'selectedAddressableAreas' + > + ): void => { + const modifiedSlots: DeckConfiguration = updatedSlots.map(slot => { + if (slot.cutoutId === cutoutId) { + return { ...slot, cutoutFixtureId: SINGLE_RIGHT_SLOT_FIXTURE } + } + return slot + }) + setUpdatedSlots(modifiedSlots) + + field.onChange(selectedSlots.filter(item => item !== cutoutId)) + } + const occupiedSlots = occupiedCutoutIds.map( + occupiedCutout => occupiedCutout.slot.split('cutout')[1] + ) + const alertDescription = t( + `alert:module_placement.SLOTS_OCCUPIED.${ + occupiedSlots.length === 1 ? 'single' : 'multi' + }`, + { + slotName: occupiedSlots, + } + ) + + return ( + <> + + + + {hasConflictedSlot ? ( + + ) : null} + + + ( + handleClickAdd(cutoutId, field)} + handleClickRemove={cutoutId => handleClickRemove(cutoutId, field)} + showExpansion={false} + /> + )} + /> + + + {t('cancel')} + + {t('save')} + + + + ) +} + +export interface EditMultipleModulesModalProps { + onCloseClick: () => void + allModulesOnDeck: ModuleOnDeck[] + moduleType: ModuleType +} +export function EditMultipleModulesModal( + props: EditMultipleModulesModalProps +): JSX.Element { + const { onCloseClick, allModulesOnDeck, moduleType } = props + const { t } = useTranslation('modules') + const dispatch = useDispatch() + const { control, handleSubmit } = useForm() + const moduleLocations = Object.values(allModulesOnDeck) + .filter(module => module.type === moduleType) + .map(temp => `cutout${temp.slot}`) + + const onSaveClick = (data: EditMultipleModulesModalValues): void => { + onCloseClick() + + data.selectedAddressableAreas.forEach(aa => { + const moduleInSlot = Object.values(allModulesOnDeck).find(module => + aa.includes(module.slot) + ) + if (!moduleInSlot) { + dispatch( + createModule({ + slot: aa.split('cutout')[1], + type: TEMPERATURE_MODULE_TYPE, + model: TEMPERATURE_MODULE_V2, + }) + ) + } + }) + Object.values(allModulesOnDeck).forEach(module => { + const moduleCutout = `cutout${module.slot}` + if (!data.selectedAddressableAreas.includes(moduleCutout)) { + dispatch(deleteModule(module.id)) + } + }) + } + + return ( +
    + + + + {t('module_display_names.multipleTemperatureModuleTypes')} + + + + +
    + ) +} diff --git a/protocol-designer/src/components/modals/EditModulesModal/__tests__/EditMultipleModulesModal.test.tsx b/protocol-designer/src/components/modals/EditModulesModal/__tests__/EditMultipleModulesModal.test.tsx new file mode 100644 index 00000000000..fa01bd44ecf --- /dev/null +++ b/protocol-designer/src/components/modals/EditModulesModal/__tests__/EditMultipleModulesModal.test.tsx @@ -0,0 +1,106 @@ +import * as React from 'react' +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' +import { fireEvent, screen, cleanup } from '@testing-library/react' +import { renderWithProviders } from '../../../../__testing-utils__' +import { i18n } from '../../../../localization' +import { getInitialDeckSetup } from '../../../../step-forms/selectors' +import { getLabwareIsCompatible } from '../../../../utils/labwareModuleCompatibility' +import { + getLabwareOnSlot, + getSlotIsEmpty, + ModuleOnDeck, +} from '../../../../step-forms' +import { EditMultipleModulesModal } from '../EditMultipleModulesModal' +import type * as Components from '@opentrons/components' + +vi.mock('../../../../step-forms/selectors') +vi.mock('../../../../utils/labwareModuleCompatibility') +vi.mock('../../../../step-forms') +vi.mock('@opentrons/components', async importOriginal => { + const actual = await importOriginal() + return { + ...actual, + DeckConfigurator: vi.fn(() =>
    mock deck config
    ), + } +}) + +const render = ( + props: React.ComponentProps +) => { + return renderWithProviders(, { + i18nInstance: i18n, + })[0] +} + +const mockTemp: ModuleOnDeck = { + id: 'temperatureId', + type: 'temperatureModuleType', + model: 'temperatureModuleV2', + slot: 'C3', + moduleState: {} as any, +} +const mockTemp2: ModuleOnDeck = { + id: 'temperatureId', + type: 'temperatureModuleType', + model: 'temperatureModuleV2', + slot: 'A1', + moduleState: {} as any, +} +const mockHS: ModuleOnDeck = { + id: 'heaterShakerId', + type: 'heaterShakerModuleType', + model: 'heaterShakerModuleV1', + moduleState: {} as any, + slot: 'A1', +} +describe('EditMultipleModulesModal', () => { + let props: React.ComponentProps + beforeEach(() => { + props = { + moduleType: 'temperatureModuleType', + onCloseClick: vi.fn(), + allModulesOnDeck: [mockTemp, mockTemp2], + } + vi.mocked(getInitialDeckSetup).mockReturnValue({ + modules: { + temperatureId: mockTemp, + temperatureId2: mockTemp2, + }, + labware: {}, + additionalEquipmentOnDeck: {}, + pipettes: {}, + }) + vi.mocked(getLabwareOnSlot).mockReturnValue(null) + vi.mocked(getSlotIsEmpty).mockReturnValue(true) + }) + afterEach(() => { + cleanup() + }) + it('renders modal and buttons with no error', () => { + vi.mocked(getLabwareIsCompatible).mockReturnValue(true) + render(props) + screen.getByText('mock deck config') + screen.getByText('Multiple Temperatures') + fireEvent.click(screen.getByRole('button', { name: 'cancel' })) + expect(props.onCloseClick).toHaveBeenCalled() + screen.getByRole('button', { name: 'save' }) + }) + it('renders modal with a cannot place module error', () => { + vi.mocked(getLabwareOnSlot).mockReturnValue({ slot: 'A1' } as any) + vi.mocked(getLabwareIsCompatible).mockReturnValue(false) + vi.mocked(getSlotIsEmpty).mockReturnValue(false) + props.allModulesOnDeck = [mockTemp, mockTemp2, mockHS] + vi.mocked(getInitialDeckSetup).mockReturnValue({ + modules: { + heaterShakerId: mockHS, + }, + labware: {}, + additionalEquipmentOnDeck: {}, + pipettes: {}, + }) + render(props) + screen.getByText('warning') + screen.getByText('Cannot place module') + screen.getByText('Multiple slots are occupied') + }) +}) diff --git a/protocol-designer/src/components/modules/EditModulesCard.tsx b/protocol-designer/src/components/modules/EditModulesCard.tsx index 40df5ef14a2..896463c295c 100644 --- a/protocol-designer/src/components/modules/EditModulesCard.tsx +++ b/protocol-designer/src/components/modules/EditModulesCard.tsx @@ -27,10 +27,12 @@ import { CrashInfoBox } from './CrashInfoBox' import { ModuleRow } from './ModuleRow' import { AdditionalItemsRow } from './AdditionalItemsRow' import { isModuleWithCollisionIssue } from './utils' -import styles from './styles.module.css' -import { AdditionalEquipmentEntity } from '@opentrons/step-generation' import { StagingAreasRow } from './StagingAreasRow' +import { MultipleModuleRow } from './MultipleModuleRow' + +import type { AdditionalEquipmentEntity } from '@opentrons/step-generation' +import styles from './styles.module.css' export interface Props { modules: ModulesForEditModulesCard openEditModuleModal: (moduleType: ModuleType, moduleId?: string) => void @@ -38,6 +40,7 @@ export interface Props { export function EditModulesCard(props: Props): JSX.Element { const { modules, openEditModuleModal } = props + const pipettesByMount = useSelector( stepFormSelectors.getPipettesForEditPipetteForm ) @@ -67,10 +70,10 @@ export function EditModulesCard(props: Props): JSX.Element { ) const hasCrashableMagneticModule = magneticModuleOnDeck && - isModuleWithCollisionIssue(magneticModuleOnDeck.model) + isModuleWithCollisionIssue(magneticModuleOnDeck[0].model) const hasCrashableTempModule = temperatureModuleOnDeck && - isModuleWithCollisionIssue(temperatureModuleOnDeck.model) + isModuleWithCollisionIssue(temperatureModuleOnDeck[0].model) const isHeaterShakerOnDeck = Boolean(heaterShakerOnDeck) const showTempPipetteCollisons = @@ -130,22 +133,33 @@ export function EditModulesCard(props: Props): JSX.Element { ) : null} {SUPPORTED_MODULE_TYPES_FILTERED.map((moduleType, i) => { const moduleData = modules[moduleType] - if (moduleData) { + if (moduleData != null && moduleData.length === 1) { return ( ) + } else if (moduleData != null && moduleData.length > 1) { + return ( + + ) } else { return ( ) diff --git a/protocol-designer/src/components/modules/MultipleModuleRow.tsx b/protocol-designer/src/components/modules/MultipleModuleRow.tsx new file mode 100644 index 00000000000..b38978d7dfc --- /dev/null +++ b/protocol-designer/src/components/modules/MultipleModuleRow.tsx @@ -0,0 +1,121 @@ +import * as React from 'react' +import { useTranslation } from 'react-i18next' +import { useDispatch } from 'react-redux' +import { + LabeledValue, + OutlineButton, + ModuleIcon, + C_DARK_GRAY, + SPACING, +} from '@opentrons/components' +import { actions as stepFormActions } from '../../step-forms' +import { DEFAULT_MODEL_FOR_MODULE_TYPE } from '../../constants' +import { ModuleDiagram } from './ModuleDiagram' +import { FlexSlotMap } from './FlexSlotMap' +import type { ModuleModel, ModuleType } from '@opentrons/shared-data' +import type { ModuleOnDeck } from '../../step-forms' + +import styles from './styles.module.css' + +interface MultipleModulesRowProps { + moduleType: ModuleType + openEditModuleModal: (moduleType: ModuleType, moduleId?: string) => void + moduleOnDeckType?: ModuleType + moduleOnDeckModel?: ModuleModel + moduleOnDeck?: ModuleOnDeck[] +} + +export function MultipleModuleRow(props: MultipleModulesRowProps): JSX.Element { + const { + moduleOnDeck, + openEditModuleModal, + moduleOnDeckModel, + moduleOnDeckType, + moduleType, + } = props + const { t } = useTranslation(['modules', 'shared']) + const dispatch = useDispatch() + + const type: ModuleType = moduleOnDeckType ?? moduleType + const occupiedSlots = moduleOnDeck?.map(module => module.slot) ?? [] + const occupiedSlotsDisplayName = ( + moduleOnDeck?.map(module => module.slot) ?? [] + ).join(', ') + + const setCurrentModule = (moduleType: ModuleType, moduleId?: string) => () => + openEditModuleModal(moduleType, moduleId) + + const addRemoveText = moduleOnDeck ? t('shared:remove') : t('shared:add') + + const handleAddOrRemove = (): void => { + if (moduleOnDeck != null) { + moduleOnDeck.forEach(module => { + dispatch(stepFormActions.deleteModule(module.id)) + }) + } else { + setCurrentModule(type) + } + } + const handleEditModule = + moduleOnDeck && setCurrentModule(type, moduleOnDeck[0].id) + + return ( +
    +

    + + {t( + `module_display_names.${ + occupiedSlots.length > 1 ? 'multipleTemperatureModuleTypes' : type + }` + )} +

    +
    +
    + +
    +
    + {moduleOnDeckModel && ( + + )} +
    +
    + {occupiedSlots.length > 0 ? ( + + ) : null} +
    +
    + {occupiedSlots.length > 0 ? ( + + ) : null} +
    +
    + {moduleOnDeck != null ? ( + + {t('shared:edit')} + + ) : null} + + {addRemoveText} + +
    +
    +
    + ) +} diff --git a/protocol-designer/src/components/modules/__tests__/MultipleModuleRow.test.tsx b/protocol-designer/src/components/modules/__tests__/MultipleModuleRow.test.tsx new file mode 100644 index 00000000000..5d5d90794d5 --- /dev/null +++ b/protocol-designer/src/components/modules/__tests__/MultipleModuleRow.test.tsx @@ -0,0 +1,67 @@ +import * as React from 'react' +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { fireEvent, screen } from '@testing-library/react' + +import { i18n } from '../../../localization' +import { renderWithProviders } from '../../../__testing-utils__' +import { MultipleModuleRow } from '../MultipleModuleRow' +import { + TEMPERATURE_MODULE_TYPE, + TEMPERATURE_MODULE_V2, +} from '@opentrons/shared-data' +import { FlexSlotMap } from '../FlexSlotMap' +import { deleteModule } from '../../../step-forms/actions' +import type { ModuleOnDeck } from '../../../step-forms' + +vi.mock('../../../step-forms/actions') +vi.mock('../FlexSlotMap') +const render = (props: React.ComponentProps) => { + return renderWithProviders(, { + i18nInstance: i18n, + })[0] +} + +const mockTemp: ModuleOnDeck = { + id: 'temperatureId', + type: 'temperatureModuleType', + model: 'temperatureModuleV2', + slot: 'C3', + moduleState: {} as any, +} +const mockTemp2: ModuleOnDeck = { + id: 'temperatureId', + type: 'temperatureModuleType', + model: 'temperatureModuleV2', + slot: 'A1', + moduleState: {} as any, +} + +describe('MultipleModuleRow', () => { + let props: React.ComponentProps + beforeEach(() => { + props = { + moduleType: TEMPERATURE_MODULE_TYPE, + openEditModuleModal: vi.fn(), + moduleOnDeckType: TEMPERATURE_MODULE_TYPE, + moduleOnDeckModel: TEMPERATURE_MODULE_V2, + moduleOnDeck: [mockTemp, mockTemp2], + } + vi.mocked(FlexSlotMap).mockReturnValue(
    mock FlexSlotMap
    ) + }) + it('renders 2 modules in the row with text and buttons', () => { + render(props) + screen.getByText('Multiple Temperatures') + screen.getByText('Position:') + screen.getByText('C3, A1') + screen.getByText('mock FlexSlotMap') + fireEvent.click(screen.getByText('edit')) + expect(props.openEditModuleModal).toHaveBeenCalled() + fireEvent.click(screen.getByText('remove')) + expect(vi.mocked(deleteModule)).toHaveBeenCalled() + }) + it('renders no modules', () => { + props.moduleOnDeck = undefined + render(props) + screen.getByText('add') + }) +}) diff --git a/protocol-designer/src/localization/en/alert.json b/protocol-designer/src/localization/en/alert.json index b17e1028e3b..34ac8c33a02 100644 --- a/protocol-designer/src/localization/en/alert.json +++ b/protocol-designer/src/localization/en/alert.json @@ -218,6 +218,11 @@ "export": "Export", "import": "Import", "module_placement": { + "SLOTS_OCCUPIED": { + "title": "Cannot place module", + "single": "Slot {{slotName}} is occupied", + "multi": "Multiple slots are occupied" + }, "SLOT_OCCUPIED": { "title": "Cannot place module", "body": "Slot {{selectedSlot}} is occupied. Navigate to the design tab and remove the labware or remove the additional item to continue." diff --git a/protocol-designer/src/localization/en/modules.json b/protocol-designer/src/localization/en/modules.json index 10a50dc0775..5cad25ca050 100644 --- a/protocol-designer/src/localization/en/modules.json +++ b/protocol-designer/src/localization/en/modules.json @@ -6,6 +6,7 @@ "wasteChute": "Waste Chute" }, "module_display_names": { + "multipleTemperatureModuleTypes": "Multiple Temperatures", "temperatureModuleType": "Temperature", "magneticModuleType": "Magnetic", "thermocyclerModuleType": "Thermocycler", diff --git a/protocol-designer/src/step-forms/selectors/index.ts b/protocol-designer/src/step-forms/selectors/index.ts index 1c0be8ca60c..3e3cb161f81 100644 --- a/protocol-designer/src/step-forms/selectors/index.ts +++ b/protocol-designer/src/step-forms/selectors/index.ts @@ -454,7 +454,10 @@ export const getModulesForEditModulesCard: Selector< reduce( initialDeckSetup.modules, (acc, moduleOnDeck: ModuleOnDeck, id) => { - acc[moduleOnDeck.type] = moduleOnDeck + if (!acc[moduleOnDeck.type]) { + acc[moduleOnDeck.type] = [] + } + acc[moduleOnDeck.type]?.push(moduleOnDeck) return acc }, { diff --git a/protocol-designer/src/step-forms/types.ts b/protocol-designer/src/step-forms/types.ts index 81422cc985b..24dee9b0c46 100644 --- a/protocol-designer/src/step-forms/types.ts +++ b/protocol-designer/src/step-forms/types.ts @@ -72,7 +72,7 @@ export interface ModuleTemporalProperties { } export type ModuleOnDeck = ModuleEntity & ModuleTemporalProperties export type ModulesForEditModulesCard = Partial< - Record + Record > // =========== LABWARE ======== export type NormalizedLabwareById = Record< From 1a4492b59dea39cb335ae3100203dd96cf570741 Mon Sep 17 00:00:00 2001 From: Jeremy Leon Date: Thu, 18 Apr 2024 13:40:16 -0400 Subject: [PATCH 164/194] fix(api): better error message for non-string variable names and min/max validation adjustment (#14938) --- api/src/opentrons/protocols/parameters/validation.py | 4 ++-- api/tests/opentrons/protocols/parameters/test_validation.py | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/api/src/opentrons/protocols/parameters/validation.py b/api/src/opentrons/protocols/parameters/validation.py index 2db343c71b6..8e7a0bed8ad 100644 --- a/api/src/opentrons/protocols/parameters/validation.py +++ b/api/src/opentrons/protocols/parameters/validation.py @@ -20,7 +20,7 @@ def validate_variable_name_unique( variable_name: str, other_variable_names: Set[str] ) -> None: """Validate that the given variable name is unique.""" - if variable_name in other_variable_names: + if isinstance(variable_name, str) and variable_name in other_variable_names: raise ParameterNameError( f'"{variable_name}" is already defined as a variable name for another parameter.' f" All variable names must be unique." @@ -222,7 +222,7 @@ def _validate_min_and_max( # These asserts are for the type checker and should never actually be asserted false assert isinstance(minimum, (int, float)) assert isinstance(maximum, (int, float)) - if maximum <= minimum: + if maximum < minimum: raise ParameterDefinitionError( "Maximum must be greater than the minimum" ) diff --git a/api/tests/opentrons/protocols/parameters/test_validation.py b/api/tests/opentrons/protocols/parameters/test_validation.py index 4206d3d3cd4..0ff337eb91d 100644 --- a/api/tests/opentrons/protocols/parameters/test_validation.py +++ b/api/tests/opentrons/protocols/parameters/test_validation.py @@ -13,8 +13,9 @@ def test_validate_variable_name_unique() -> None: - """It should no-op if the name is unique and raise if it is not.""" + """It should no-op if the name is unique or if it's not a string, and raise if it is not.""" subject.validate_variable_name_unique("one of a kind", {"fee", "foo", "fum"}) + subject.validate_variable_name_unique({}, {"fee", "foo", "fum"}) # type: ignore[arg-type] with pytest.raises(ParameterNameError): subject.validate_variable_name_unique("copy", {"paste", "copy", "cut"}) @@ -103,10 +104,12 @@ def test_ensure_variable_name_raises_keyword(variable_name: str) -> None: def test_validate_options() -> None: """It should not raise when given valid constraints""" subject.validate_options(123, 1, 100, None, int) + subject.validate_options(123, 100, 100, None, int) subject.validate_options( 123, None, None, [{"display_name": "abc", "value": 456}], int ) subject.validate_options(12.3, 1.1, 100.9, None, float) + subject.validate_options(12.3, 1.1, 1.1, None, float) subject.validate_options( 12.3, None, None, [{"display_name": "abc", "value": 45.6}], float ) From 6d91a5607ece5cfea482d448d138801486f1261a Mon Sep 17 00:00:00 2001 From: Nick Diehl <47604184+ncdiehl11@users.noreply.github.com> Date: Thu, 18 Apr 2024 14:26:10 -0400 Subject: [PATCH 165/194] fix(app): ensure ApplyHistoricOffsets renders on non-RTP protocols (#14948) closes RQA-2606 --- .../ChooseRobotToRunProtocolSlideout.test.tsx | 14 ++++++++++ .../index.tsx | 27 ++++++++++++------- 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/app/src/organisms/ChooseRobotToRunProtocolSlideout/__tests__/ChooseRobotToRunProtocolSlideout.test.tsx b/app/src/organisms/ChooseRobotToRunProtocolSlideout/__tests__/ChooseRobotToRunProtocolSlideout.test.tsx index 5bd054d887f..9ac6e0232ea 100644 --- a/app/src/organisms/ChooseRobotToRunProtocolSlideout/__tests__/ChooseRobotToRunProtocolSlideout.test.tsx +++ b/app/src/organisms/ChooseRobotToRunProtocolSlideout/__tests__/ChooseRobotToRunProtocolSlideout.test.tsx @@ -403,4 +403,18 @@ describe('ChooseRobotToRunProtocolSlideout', () => { }) expect(proceedButton).toBeDisabled() }) + + it('renders labware offset data selection and learn more button launches help modal', () => { + render({ + storedProtocolData: storedProtocolDataFixture, + onCloseClick: vi.fn(), + showSlideout: true, + }) + screen.getByText('No offset data available') + const learnMoreLink = screen.getByText('Learn more') + fireEvent.click(learnMoreLink) + screen.getByText( + 'Labware offset data references previous protocol run labware locations to save you time. If all the labware in this protocol have been checked in previous runs, that data will be applied to this run.' + ) + }) }) diff --git a/app/src/organisms/ChooseRobotToRunProtocolSlideout/index.tsx b/app/src/organisms/ChooseRobotToRunProtocolSlideout/index.tsx index 17e64b4fb6b..5dd3278bdfe 100644 --- a/app/src/organisms/ChooseRobotToRunProtocolSlideout/index.tsx +++ b/app/src/organisms/ChooseRobotToRunProtocolSlideout/index.tsx @@ -158,7 +158,7 @@ export function ChooseRobotToRunProtocolSlideoutComponent( ? mostRecentAnalysis?.robotType ?? null : null - const SinglePageButtonWithoutFF = ( + const singlePageButton = ( ) + const offsetsComponent = ( + + ) + const resetRunTimeParameters = (): void => { setRunTimeParametersOverrides( runTimeParametersOverrides?.map(parameter => ({ @@ -214,14 +225,7 @@ export function ChooseRobotToRunProtocolSlideoutComponent( {hasRunTimeParameters ? ( currentPage === 1 ? ( <> - + {offsetsComponent} setCurrentPage(2)} width="100%" @@ -253,7 +257,10 @@ export function ChooseRobotToRunProtocolSlideoutComponent(
    ) ) : ( - SinglePageButtonWithoutFF + <> + {offsetsComponent} + {singlePageButton} + )}
    } From e877f3a8e2b8892f6ca63c6571ae6e012e11e75e Mon Sep 17 00:00:00 2001 From: Max Marrone Date: Fri, 19 Apr 2024 09:19:27 -0400 Subject: [PATCH 166/194] refactor(api): Delete unused action_dispatcher argument (#14954) --- .../execution/create_queue_worker.py | 1 - .../protocol_engine/execution/equipment.py | 3 --- .../execution/test_equipment_handler.py | 13 ------------- 3 files changed, 17 deletions(-) diff --git a/api/src/opentrons/protocol_engine/execution/create_queue_worker.py b/api/src/opentrons/protocol_engine/execution/create_queue_worker.py index 3323aab0aa3..8b59eda5ef2 100644 --- a/api/src/opentrons/protocol_engine/execution/create_queue_worker.py +++ b/api/src/opentrons/protocol_engine/execution/create_queue_worker.py @@ -39,7 +39,6 @@ def create_queue_worker( equipment_handler = EquipmentHandler( hardware_api=hardware_api, state_store=state_store, - action_dispatcher=action_dispatcher, ) movement_handler = MovementHandler( diff --git a/api/src/opentrons/protocol_engine/execution/equipment.py b/api/src/opentrons/protocol_engine/execution/equipment.py index cda39925945..ee04653bda2 100644 --- a/api/src/opentrons/protocol_engine/execution/equipment.py +++ b/api/src/opentrons/protocol_engine/execution/equipment.py @@ -22,7 +22,6 @@ TemperatureModuleId, ThermocyclerModuleId, ) -from ..actions import ActionDispatcher from ..errors import ( FailedToLoadPipetteError, LabwareDefinitionDoesNotExistError, @@ -99,7 +98,6 @@ def __init__( self, hardware_api: HardwareControlAPI, state_store: StateStore, - action_dispatcher: ActionDispatcher, labware_data_provider: Optional[LabwareDataProvider] = None, module_data_provider: Optional[ModuleDataProvider] = None, model_utils: Optional[ModelUtils] = None, @@ -110,7 +108,6 @@ def __init__( """Initialize an EquipmentHandler instance.""" self._hardware_api = hardware_api self._state_store = state_store - self._action_dispatcher = action_dispatcher self._labware_data_provider = labware_data_provider or LabwareDataProvider() self._module_data_provider = module_data_provider or ModuleDataProvider() self._model_utils = model_utils or ModelUtils() diff --git a/api/tests/opentrons/protocol_engine/execution/test_equipment_handler.py b/api/tests/opentrons/protocol_engine/execution/test_equipment_handler.py index b2d97aff7d5..1177894e977 100644 --- a/api/tests/opentrons/protocol_engine/execution/test_equipment_handler.py +++ b/api/tests/opentrons/protocol_engine/execution/test_equipment_handler.py @@ -22,7 +22,6 @@ from opentrons.protocols.models import LabwareDefinition from opentrons.protocol_engine import errors -from opentrons.protocol_engine.actions import ActionDispatcher from opentrons.protocol_engine.types import ( DeckSlotLocation, DeckType, @@ -84,12 +83,6 @@ def state_store(decoy: Decoy) -> StateStore: return decoy.mock(cls=StateStore) -@pytest.fixture -def action_dispatcher(decoy: Decoy) -> ActionDispatcher: - """Get a mocked out ActionDispatcher instance.""" - return decoy.mock(cls=ActionDispatcher) - - @pytest.fixture def model_utils(decoy: Decoy) -> ModelUtils: """Get a mocked out ModelUtils instance.""" @@ -166,7 +159,6 @@ def virtual_pipette_data_provider( def subject( hardware_api: HardwareControlAPI, state_store: StateStore, - action_dispatcher: ActionDispatcher, labware_data_provider: LabwareDataProvider, module_data_provider: ModuleDataProvider, model_utils: ModelUtils, @@ -176,7 +168,6 @@ def subject( return EquipmentHandler( hardware_api=hardware_api, state_store=state_store, - action_dispatcher=action_dispatcher, labware_data_provider=labware_data_provider, module_data_provider=module_data_provider, model_utils=model_utils, @@ -614,7 +605,6 @@ async def test_load_pipette( model_utils: ModelUtils, hardware_api: HardwareControlAPI, state_store: StateStore, - action_dispatcher: ActionDispatcher, loaded_static_pipette_data: LoadedStaticPipetteData, subject: EquipmentHandler, ) -> None: @@ -665,7 +655,6 @@ async def test_load_pipette_96_channels( model_utils: ModelUtils, hardware_api: HardwareControlAPI, state_store: StateStore, - action_dispatcher: ActionDispatcher, loaded_static_pipette_data: LoadedStaticPipetteData, subject: EquipmentHandler, ) -> None: @@ -702,7 +691,6 @@ async def test_load_pipette_uses_provided_id( decoy: Decoy, hardware_api: HardwareControlAPI, state_store: StateStore, - action_dispatcher: ActionDispatcher, loaded_static_pipette_data: LoadedStaticPipetteData, subject: EquipmentHandler, ) -> None: @@ -734,7 +722,6 @@ async def test_load_pipette_use_virtual( decoy: Decoy, model_utils: ModelUtils, state_store: StateStore, - action_dispatcher: ActionDispatcher, loaded_static_pipette_data: LoadedStaticPipetteData, subject: EquipmentHandler, virtual_pipette_data_provider: pipette_data_provider.VirtualPipetteDataProvider, From 8fa37b32000b8d12900f4528ad8050411e15451c Mon Sep 17 00:00:00 2001 From: Ed Cormany Date: Fri, 19 Apr 2024 15:15:07 -0400 Subject: [PATCH 167/194] docs(api): fix or remove broken links on Welcome page (#14957) # Overview Some links on the Welcome page weren't going where we intended. Addresses RTC-437, RTC-427 # Test Plan [Sandbox](http://sandbox.docs.opentrons.com/docs-welcome-fixes/v2/) # Changelog - remove one Help Center link entirely - change another to email support - fix services link and turnaround time # Review requests click on 'em. # Risk assessment none --- api/docs/v2/index.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/api/docs/v2/index.rst b/api/docs/v2/index.rst index 376d483f33b..5e29296241d 100644 --- a/api/docs/v2/index.rst +++ b/api/docs/v2/index.rst @@ -171,17 +171,17 @@ More Resources Opentrons App +++++++++++++ -The `Opentrons App `__ is the easiest way to run your Python protocols. The app `supports `_ the latest versions of macOS, Windows, and Ubuntu. +The `Opentrons App `__ is the easiest way to run your Python protocols. The app runs on the latest versions of macOS, Windows, and Ubuntu. Support +++++++ -Questions about setting up your robot, using Opentrons software, or troubleshooting? Check out our `support articles `_ or `get in touch directly `_ with Opentrons Support. +Questions about setting up your robot, using Opentrons software, or troubleshooting? Check out our `support articles `_ or `contact Opentrons Support directly `_. Custom Protocol Service +++++++++++++++++++++++ -Don't have the time or resources to write your own protocols? The `Opentrons Custom Protocols `_ service can get you set up in as little as a week. +Don't have the time or resources to write your own protocols? Our `custom protocol development service `_ can get you set up in two weeks. Contributing ++++++++++++ From 44181288a1f1803f6977cfb6e62676d37784f200 Mon Sep 17 00:00:00 2001 From: Max Marrone Date: Fri, 19 Apr 2024 15:41:28 -0400 Subject: [PATCH 168/194] fix(api): Various E-stop fixes (#14929) --- .../protocol_engine/errors/exceptions.py | 10 +- .../execution/command_executor.py | 2 +- .../protocol_engine/protocol_engine.py | 137 ++++++------------ .../protocol_engine/state/commands.py | 16 +- .../execution/test_command_executor.py | 4 +- .../state/test_command_state.py | 39 ++++- .../state/test_command_store_old.py | 11 +- .../protocol_engine/test_protocol_engine.py | 99 +++---------- .../maintenance_engine_store.py | 55 +++++-- .../robot_server/runs/engine_store.py | 45 +++++- .../maintenance_runs/test_engine_store.py | 35 +++-- robot-server/tests/runs/test_engine_store.py | 39 +++-- 12 files changed, 250 insertions(+), 242 deletions(-) diff --git a/api/src/opentrons/protocol_engine/errors/exceptions.py b/api/src/opentrons/protocol_engine/errors/exceptions.py index 9d9ff99b33e..0e27a270c94 100644 --- a/api/src/opentrons/protocol_engine/errors/exceptions.py +++ b/api/src/opentrons/protocol_engine/errors/exceptions.py @@ -951,16 +951,18 @@ def __init__( class EStopActivatedError(ProtocolEngineError): - """Raised when an operation's required pipette tip is not attached.""" + """Represents an E-stop event.""" def __init__( self, - message: Optional[str] = None, - details: Optional[Dict[str, Any]] = None, wrapping: Optional[Sequence[EnumeratedError]] = None, ) -> None: """Build an EStopActivatedError.""" - super().__init__(ErrorCodes.E_STOP_ACTIVATED, message, details, wrapping) + super().__init__( + code=ErrorCodes.E_STOP_ACTIVATED, + message="E-stop activated.", + wrapping=wrapping, + ) class NotSupportedOnRobotType(ProtocolEngineError): diff --git a/api/src/opentrons/protocol_engine/execution/command_executor.py b/api/src/opentrons/protocol_engine/execution/command_executor.py index 9488d1719e9..d00b5c0a96d 100644 --- a/api/src/opentrons/protocol_engine/execution/command_executor.py +++ b/api/src/opentrons/protocol_engine/execution/command_executor.py @@ -159,7 +159,7 @@ async def execute(self, command_id: str) -> None: if isinstance(error, asyncio.CancelledError): error = RunStoppedError("Run was cancelled") elif isinstance(error, EStopActivatedError): - error = PE_EStopActivatedError(message=str(error), wrapping=[error]) + error = PE_EStopActivatedError(wrapping=[error]) elif not isinstance(error, EnumeratedError): error = PythonException(error) diff --git a/api/src/opentrons/protocol_engine/protocol_engine.py b/api/src/opentrons/protocol_engine/protocol_engine.py index 7389078343d..8bb4c91dda3 100644 --- a/api/src/opentrons/protocol_engine/protocol_engine.py +++ b/api/src/opentrons/protocol_engine/protocol_engine.py @@ -5,7 +5,6 @@ from opentrons.protocol_engine.actions.actions import ResumeFromRecoveryAction from opentrons.protocol_engine.error_recovery_policy import ( ErrorRecoveryPolicy, - ErrorRecoveryType, error_recovery_by_ff, ) @@ -58,7 +57,6 @@ HardwareStoppedAction, ResetTipsAction, SetPipetteMovementSpeedAction, - FailCommandAction, ) @@ -277,14 +275,8 @@ async def add_and_execute_command_wait_for_recovery( ) return completed_command - def estop( - self, - # TODO(mm, 2024-03-26): Maintenance runs are a robot-server concept that - # ProtocolEngine should not have to know about. Can this be simplified or - # defined in other terms? - maintenance_run: bool, - ) -> None: - """Signal to the engine that an estop event occurred. + def estop(self) -> None: + """Signal to the engine that an E-stop event occurred. If an estop happens while the robot is moving, lower layers physically stop motion and raise the event as an exception, which fails the Protocol Engine @@ -292,88 +284,36 @@ def estop( However, if an estop happens in between commands, or in the middle of a command like `comment` or `waitForDuration` that doesn't access the hardware, - `ProtocolEngine` needs to be told about it so it can treat it as a fatal run - error and stop executing more commands. This method is how to do that. - - If there are any queued commands for the engine, they will be marked - as failed due to the estop event. If there aren't any queued commands - *and* this is a maintenance run (which has commands queued one-by-one), - a series of actions will mark the engine as Stopped. In either case the - queue worker will be deactivated; the primary difference is that the former - case will expect the protocol runner to `finish()` the engine, whereas the - maintenance run will be put into a state wherein the engine can be discarded. - """ - if self._state_store.commands.get_is_stopped(): - return - running_or_next_queued_id = ( - self._state_store.commands.get_running_command_id() - or self._state_store.commands.get_queue_ids().head(None) - # TODO(mm, 2024-04-02): This logic looks wrong whenever the next queued - # command is a setup command, which is the normal case in maintenance - # runs. Setup commands won't show up in commands.get_queue_ids(). - ) - running_or_next_queued = ( - self._state_store.commands.get(running_or_next_queued_id) - if running_or_next_queued_id is not None - else None - ) + `ProtocolEngine` needs to be told about it so it can interrupt the command + and stop executing any more. This method is how to do that. - if running_or_next_queued_id is not None: - assert running_or_next_queued is not None - - fail_action = FailCommandAction( - command_id=running_or_next_queued_id, - # FIXME(mm, 2024-04-02): As of https://github.com/Opentrons/opentrons/pull/14726, - # this action is only legal if the command is running, not queued. - running_command=running_or_next_queued, - error_id=self._model_utils.generate_id(), - failed_at=self._model_utils.get_timestamp(), - error=EStopActivatedError(message="Estop Activated"), - notes=[], - type=ErrorRecoveryType.FAIL_RUN, - ) - self._action_dispatcher.dispatch(fail_action) - - # The FailCommandAction above will have cleared all the queued protocol - # OR setup commands, depending on whether we gave it a protocol or setup - # command. We want both to be cleared in either case. So, do that here. - running_or_next_queued_id = self._state_store.commands.get_queue_ids().head( - None - ) - if running_or_next_queued_id is not None: - running_or_next_queued = self._state_store.commands.get( - running_or_next_queued_id - ) - fail_action = FailCommandAction( - command_id=running_or_next_queued_id, - # FIXME(mm, 2024-04-02): As of https://github.com/Opentrons/opentrons/pull/14726, - # this action is only legal if the command is running, not queued. - running_command=running_or_next_queued, - error_id=self._model_utils.generate_id(), - failed_at=self._model_utils.get_timestamp(), - error=EStopActivatedError(message="Estop Activated"), - notes=[], - type=ErrorRecoveryType.FAIL_RUN, - ) - self._action_dispatcher.dispatch(fail_action) - self._queue_worker.cancel() - elif maintenance_run: - stop_action = self._state_store.commands.validate_action_allowed( + This acts roughly like `request_stop()`. After calling this, you should call + `finish()` with an EStopActivatedError. + """ + try: + action = self._state_store.commands.validate_action_allowed( StopAction(from_estop=True) ) - self._action_dispatcher.dispatch(stop_action) - hardware_stop_action = HardwareStoppedAction( - completed_at=self._model_utils.get_timestamp(), - finish_error_details=FinishErrorDetails( - error=EStopActivatedError(message="Estop Activated"), - error_id=self._model_utils.generate_id(), - created_at=self._model_utils.get_timestamp(), - ), + except Exception: # todo(mm, 2024-04-16): Catch a more specific type. + # This is likely called from some hardware API callback that doesn't care + # about ProtocolEngine lifecycle or what methods are valid to call at what + # times. So it makes more sense for us to no-op here than to propagate this + # as an error. + _log.info( + "ProtocolEngine cannot handle E-stop event right now. Ignoring it.", + exc_info=True, ) - self._action_dispatcher.dispatch(hardware_stop_action) - self._queue_worker.cancel() - else: - _log.info("estop pressed before protocol was started, taking no action.") + return + self._action_dispatcher.dispatch(action) + # self._queue_worker.cancel() will try to interrupt any ongoing command. + # Unfortunately, if it's a hardware command, this interruption will race + # against the E-stop exception propagating up from lower layers. But we need to + # do this because we want to make sure non-hardware commands, like + # `waitForDuration`, are also interrupted. + self._queue_worker.cancel() + # Unlike self.request_stop(), we don't need to do + # self._hardware_api.cancel_execution_and_running_tasks(). Since this was an + # E-stop event, the hardware API already knows. async def request_stop(self) -> None: """Make command execution stop soon. @@ -418,14 +358,20 @@ async def finish( set_run_status: bool = True, post_run_hardware_state: PostRunHardwareState = PostRunHardwareState.HOME_AND_STAY_ENGAGED, ) -> None: - """Gracefully finish using the ProtocolEngine, waiting for it to become idle. + """Finish using the `ProtocolEngine`. + + This does a few things: + + 1. It may do post-run actions like homing and dropping tips. This depends on the + arguments passed as well as heuristics based on the history of the engine. + 2. It waits for the engine to be done controlling the robot's hardware. + 3. It releases internal resources, like background tasks. - The engine will finish executing its current command (if any), - and then shut down. After an engine has been `finished`'ed, it cannot - be restarted. + It's safe to call `finish()` multiple times. After you call `finish()`, + the engine can't be restarted. This method should not raise. If any exceptions happened during execution that were not - properly caught by the CommandExecutor, or if any exceptions happen during this + properly caught by `ProtocolEngine` internals, or if any exceptions happen during this `finish()` call, they should be saved as `.state_view.get_summary().errors`. Arguments: @@ -439,12 +385,11 @@ async def finish( if self._state_store.commands.state.stopped_by_estop: # This handles the case where the E-stop was pressed while we were *not* in the middle # of some hardware interaction that would raise it as an exception. For example, imagine - # we were paused between two commands, or imagine we were executing a very long run of - # comment commands. + # we were paused between two commands, or imagine we were executing a waitForDuration. drop_tips_after_run = False post_run_hardware_state = PostRunHardwareState.DISENGAGE_IN_PLACE if error is None: - error = EStopActivatedError(message="Estop was activated during a run") + error = EStopActivatedError() if error: # If the run had an error, check if that error indicates an E-stop. diff --git a/api/src/opentrons/protocol_engine/state/commands.py b/api/src/opentrons/protocol_engine/state/commands.py index 1ae0cb1ed68..b5805251046 100644 --- a/api/src/opentrons/protocol_engine/state/commands.py +++ b/api/src/opentrons/protocol_engine/state/commands.py @@ -345,9 +345,11 @@ def handle_action(self, action: Action) -> None: # noqa: C901 elif isinstance(action, StopAction): if not self._state.run_result: self._state.queue_status = QueueStatus.PAUSED - self._state.run_result = RunResult.STOPPED if action.from_estop: self._state.stopped_by_estop = True + self._state.run_result = RunResult.FAILED + else: + self._state.run_result = RunResult.STOPPED elif isinstance(action, FinishAction): if not self._state.run_result: @@ -361,12 +363,12 @@ def handle_action(self, action: Action) -> None: # noqa: C901 else: self._state.run_result = RunResult.STOPPED - if action.error_details: - self._state.run_error = self._map_run_exception_to_error_occurrence( - action.error_details.error_id, - action.error_details.created_at, - action.error_details.error, - ) + if not self._state.run_error and action.error_details: + self._state.run_error = self._map_run_exception_to_error_occurrence( + action.error_details.error_id, + action.error_details.created_at, + action.error_details.error, + ) elif isinstance(action, HardwareStoppedAction): self._state.queue_status = QueueStatus.PAUSED diff --git a/api/tests/opentrons/protocol_engine/execution/test_command_executor.py b/api/tests/opentrons/protocol_engine/execution/test_command_executor.py index 2cd753093f9..1cdb051164c 100644 --- a/api/tests/opentrons/protocol_engine/execution/test_command_executor.py +++ b/api/tests/opentrons/protocol_engine/execution/test_command_executor.py @@ -360,8 +360,8 @@ def _ImplementationCls(self) -> Type[_TestCommandImpl]: False, ), ( - EStopActivatedError("oh no"), - matchers.ErrorMatching(PE_EStopActivatedError, match="oh no"), + EStopActivatedError(), + matchers.ErrorMatching(PE_EStopActivatedError), True, ), ( diff --git a/api/tests/opentrons/protocol_engine/state/test_command_state.py b/api/tests/opentrons/protocol_engine/state/test_command_state.py index 8f1ea39fc00..742abf3e6e9 100644 --- a/api/tests/opentrons/protocol_engine/state/test_command_state.py +++ b/api/tests/opentrons/protocol_engine/state/test_command_state.py @@ -5,6 +5,7 @@ """ from datetime import datetime +from unittest.mock import sentinel import pytest @@ -13,10 +14,15 @@ from opentrons.ordered_set import OrderedSet from opentrons.protocol_engine import actions, commands, errors from opentrons.protocol_engine.error_recovery_policy import ErrorRecoveryType +from opentrons.protocol_engine.errors.error_occurrence import ErrorOccurrence +from opentrons.protocol_engine.errors.exceptions import EStopActivatedError from opentrons.protocol_engine.notes.notes import CommandNote -from opentrons.protocol_engine.state.commands import CommandStore, CommandView +from opentrons.protocol_engine.state.commands import ( + CommandStore, + CommandView, +) from opentrons.protocol_engine.state.config import Config -from opentrons.protocol_engine.types import DeckType +from opentrons.protocol_engine.types import DeckType, EngineStatus def _make_config() -> Config: @@ -434,3 +440,32 @@ def test_get_recovery_in_progress_for_command() -> None: # c3 failed, but not recoverably. assert not subject_view.get_recovery_in_progress_for_command("c2") + + +def test_final_state_after_estop() -> None: + """Test the final state of the run after it's E-stopped.""" + subject = CommandStore(config=_make_config(), is_door_open=False) + subject_view = CommandView(subject.state) + + error_details = actions.FinishErrorDetails( + error=EStopActivatedError(), error_id="error-id", created_at=datetime.now() + ) + expected_error_occurrence = ErrorOccurrence( + id=error_details.error_id, + createdAt=error_details.created_at, + errorCode=ErrorCodes.E_STOP_ACTIVATED.value.code, + errorType="EStopActivatedError", + detail="E-stop activated.", + ) + + subject.handle_action(actions.StopAction(from_estop=True)) + subject.handle_action(actions.FinishAction(error_details=error_details)) + subject.handle_action( + actions.HardwareStoppedAction( + completed_at=sentinel.hardware_stopped_action_completed_at, + finish_error_details=None, + ) + ) + + assert subject_view.get_status() == EngineStatus.FAILED + assert subject_view.get_error() == expected_error_occurrence diff --git a/api/tests/opentrons/protocol_engine/state/test_command_store_old.py b/api/tests/opentrons/protocol_engine/state/test_command_store_old.py index a859ae7573b..52d5aa961ce 100644 --- a/api/tests/opentrons/protocol_engine/state/test_command_store_old.py +++ b/api/tests/opentrons/protocol_engine/state/test_command_store_old.py @@ -600,8 +600,13 @@ def test_command_store_handles_finish_action_with_stopped() -> None: assert subject.state.run_result == RunResult.STOPPED -@pytest.mark.parametrize("from_estop", [True, False]) -def test_command_store_handles_stop_action(from_estop: bool) -> None: +@pytest.mark.parametrize( + ["from_estop", "expected_run_result"], + [(True, RunResult.FAILED), (False, RunResult.STOPPED)], +) +def test_command_store_handles_stop_action( + from_estop: bool, expected_run_result: RunResult +) -> None: """It should mark the engine as non-gracefully stopped on StopAction.""" subject = CommandStore(is_door_open=False, config=_make_config()) @@ -615,7 +620,7 @@ def test_command_store_handles_stop_action(from_estop: bool) -> None: assert subject.state == CommandState( command_history=CommandHistory(), queue_status=QueueStatus.PAUSED, - run_result=RunResult.STOPPED, + run_result=expected_run_result, run_completed_at=None, is_door_blocking=False, run_error=None, diff --git a/api/tests/opentrons/protocol_engine/test_protocol_engine.py b/api/tests/opentrons/protocol_engine/test_protocol_engine.py index 959c9172b9e..e3f7b315e4d 100644 --- a/api/tests/opentrons/protocol_engine/test_protocol_engine.py +++ b/api/tests/opentrons/protocol_engine/test_protocol_engine.py @@ -8,9 +8,7 @@ from decoy import Decoy from opentrons_shared_data.robot.dev_types import RobotType -from opentrons.ordered_set import OrderedSet from opentrons.protocol_engine.actions.actions import ResumeFromRecoveryAction -from opentrons.protocol_engine.error_recovery_policy import ErrorRecoveryType from opentrons.types import DeckSlotName from opentrons.hardware_control import HardwareControlAPI, OT2HardwareControlAPI @@ -19,7 +17,6 @@ from opentrons.protocols.models import LabwareDefinition from opentrons.protocol_engine import ProtocolEngine, commands, slot_standardization -from opentrons.protocol_engine.errors.exceptions import EStopActivatedError from opentrons.protocol_engine.types import ( DeckType, LabwareOffset, @@ -59,7 +56,6 @@ QueueCommandAction, HardwareStoppedAction, ResetTipsAction, - FailCommandAction, ) @@ -570,8 +566,8 @@ async def test_finish( """It should be able to gracefully tell the engine it's done.""" completed_at = datetime(2021, 1, 1, 0, 0) - decoy.when(model_utils.get_timestamp()).then_return(completed_at) decoy.when(state_store.commands.state.stopped_by_estop).then_return(False) + decoy.when(model_utils.get_timestamp()).then_return(completed_at) await subject.finish( drop_tips_after_run=drop_tips_after_run, @@ -845,106 +841,53 @@ async def test_stop_for_legacy_core_protocols( ) -@pytest.mark.parametrize("maintenance_run", [True, False]) -async def test_estop_during_command( +async def test_estop( decoy: Decoy, action_dispatcher: ActionDispatcher, queue_worker: QueueWorker, state_store: StateStore, subject: ProtocolEngine, - model_utils: ModelUtils, - maintenance_run: bool, ) -> None: """It should be able to stop the engine.""" - timestamp = datetime(2021, 1, 1, 0, 0) - command_id = "command_fake_id" - running_command = sentinel.running_command - queued_command = sentinel.queued_command - error_id = "fake_error_id" - fake_command_set = OrderedSet(["fake-id-1", "fake-id-1"]) - - decoy.when(model_utils.get_timestamp()).then_return(timestamp) - decoy.when(model_utils.generate_id()).then_return(error_id) - decoy.when(state_store.commands.get_is_stopped()).then_return(False) - decoy.when(state_store.commands.get_running_command_id()).then_return(command_id) - decoy.when(state_store.commands.get(command_id)).then_return(running_command) - decoy.when(state_store.commands.get_queue_ids()).then_return(fake_command_set) - decoy.when(state_store.commands.get(fake_command_set.head())).then_return( - queued_command - ) - - expected_action = FailCommandAction( - command_id=command_id, - running_command=running_command, - error_id=error_id, - failed_at=timestamp, - error=EStopActivatedError(message="Estop Activated"), - notes=[], - type=ErrorRecoveryType.FAIL_RUN, - ) - expected_action_2 = FailCommandAction( - command_id=fake_command_set.head(), - running_command=queued_command, - error_id=error_id, - failed_at=timestamp, - error=EStopActivatedError(message="Estop Activated"), - notes=[], - type=ErrorRecoveryType.FAIL_RUN, - ) + expected_action = StopAction(from_estop=True) + validated_action = sentinel.validated_action + decoy.when( + state_store.commands.validate_action_allowed(expected_action), + ).then_return(validated_action) - subject.estop(maintenance_run=maintenance_run) + subject.estop() decoy.verify( - action_dispatcher.dispatch(action=expected_action), - action_dispatcher.dispatch(action=expected_action_2), + action_dispatcher.dispatch(action=validated_action), queue_worker.cancel(), ) -@pytest.mark.parametrize("maintenance_run", [True, False]) -async def test_estop_without_command( +async def test_estop_noops_if_invalid( decoy: Decoy, action_dispatcher: ActionDispatcher, queue_worker: QueueWorker, state_store: StateStore, subject: ProtocolEngine, - model_utils: ModelUtils, - maintenance_run: bool, ) -> None: - """It should be able to stop the engine.""" - timestamp = datetime(2021, 1, 1, 0, 0) - error_id = "fake_error_id" - - decoy.when(model_utils.get_timestamp()).then_return(timestamp) - decoy.when(model_utils.generate_id()).then_return(error_id) - decoy.when(state_store.commands.get_is_stopped()).then_return(False) - decoy.when(state_store.commands.get_running_command_id()).then_return(None) - decoy.when(state_store.commands.get_queue_ids()).then_return(OrderedSet()) - - expected_stop = StopAction(from_estop=True) - expected_hardware_stop = HardwareStoppedAction( - completed_at=timestamp, - finish_error_details=FinishErrorDetails( - error=EStopActivatedError(message="Estop Activated"), - error_id=error_id, - created_at=timestamp, - ), - ) - + """It should no-op if a stop is invalid right now..""" + expected_action = StopAction(from_estop=True) decoy.when( - state_store.commands.validate_action_allowed(expected_stop), - ).then_return(expected_stop) + state_store.commands.validate_action_allowed(expected_action), + ).then_raise(RuntimeError("unable to stop; this machine craves flesh")) - subject.estop(maintenance_run=maintenance_run) + subject.estop() # Should not raise. decoy.verify( - action_dispatcher.dispatch(expected_stop), times=1 if maintenance_run else 0 + action_dispatcher.dispatch(), # type: ignore + ignore_extra_args=True, + times=0, ) decoy.verify( - action_dispatcher.dispatch(expected_hardware_stop), - times=1 if maintenance_run else 0, + queue_worker.cancel(), + ignore_extra_args=True, + times=0, ) - decoy.verify(queue_worker.cancel(), times=1 if maintenance_run else 0) def test_add_plugin( diff --git a/robot-server/robot_server/maintenance_runs/maintenance_engine_store.py b/robot-server/robot_server/maintenance_runs/maintenance_engine_store.py index 3b60f38f533..c70d2a1dd07 100644 --- a/robot-server/robot_server/maintenance_runs/maintenance_engine_store.py +++ b/robot-server/robot_server/maintenance_runs/maintenance_engine_store.py @@ -1,7 +1,10 @@ """In-memory storage of ProtocolEngine instances.""" +import asyncio +import logging from datetime import datetime from typing import List, NamedTuple, Optional, Callable +from opentrons.protocol_engine.errors.exceptions import EStopActivatedError from opentrons.protocol_engine.types import PostRunHardwareState from opentrons_shared_data.robot.dev_types import RobotType from opentrons_shared_data.robot.dev_types import RobotTypeEnum @@ -27,6 +30,9 @@ from opentrons.protocol_engine.types import DeckConfigurationType +_log = logging.getLogger(__name__) + + class EngineConflictError(RuntimeError): """An error raised if an active engine is already initialized. @@ -48,18 +54,47 @@ class RunnerEnginePair(NamedTuple): engine: ProtocolEngine -def get_estop_listener(engine_store: "MaintenanceEngineStore") -> HardwareEventHandler: - """Create a callback for estop events.""" +async def handle_estop_event( + engine_store: "MaintenanceEngineStore", event: HardwareEvent +) -> None: + """Handle an E-stop event from the hardware API. - def _callback(event: HardwareEvent) -> None: + This is meant to run in the engine's thread and asyncio event loop. + + This is a public function for unit-testing purposes, but it's an implementation + detail of the store. + """ + try: if isinstance(event, EstopStateNotification): if event.new_state is not EstopState.PHYSICALLY_ENGAGED: return if engine_store.current_run_id is None: return - engine_store.engine.estop(maintenance_run=True) + # todo(mm, 2024-04-17): This estop teardown sequencing belongs in the + # runner layer. + engine_store.engine.estop() + await engine_store.engine.finish(error=EStopActivatedError()) + except Exception: + # This is a background task kicked off by a hardware event, + # so there's no one to propagate this exception to. + _log.exception("Exception handling E-stop event.") + + +def _get_estop_listener(engine_store: "MaintenanceEngineStore") -> HardwareEventHandler: + """Create a callback for estop events. + + The returned callback is meant to run in the hardware API's thread. + """ + engine_loop = asyncio.get_running_loop() - return _callback + def run_handler_in_engine_thread_from_hardware_thread( + event: HardwareEvent, + ) -> None: + asyncio.run_coroutine_threadsafe( + handle_estop_event(engine_store, event), engine_loop + ) + + return run_handler_in_engine_thread_from_hardware_thread class MaintenanceEngineStore: @@ -83,15 +118,7 @@ def __init__( self._robot_type = robot_type self._deck_type = deck_type self._runner_engine_pair: Optional[RunnerEnginePair] = None - hardware_api.register_callback(get_estop_listener(self)) - - def _estop_listener(self, event: HardwareEvent) -> None: - if isinstance(event, EstopStateNotification): - if event.new_state is not EstopState.PHYSICALLY_ENGAGED: - return - if self._runner_engine_pair is None: - return - self._runner_engine_pair.engine.estop(maintenance_run=True) + hardware_api.register_callback(_get_estop_listener(self)) @property def engine(self) -> ProtocolEngine: diff --git a/robot-server/robot_server/runs/engine_store.py b/robot-server/robot_server/runs/engine_store.py index 8a35c20d92f..5b6d57520a7 100644 --- a/robot-server/robot_server/runs/engine_store.py +++ b/robot-server/robot_server/runs/engine_store.py @@ -1,6 +1,9 @@ """In-memory storage of ProtocolEngine instances.""" +import asyncio +import logging from typing import List, NamedTuple, Optional, Callable +from opentrons.protocol_engine.errors.exceptions import EStopActivatedError from opentrons.protocol_engine.types import PostRunHardwareState from opentrons_shared_data.robot.dev_types import RobotType from opentrons_shared_data.robot.dev_types import RobotTypeEnum @@ -38,6 +41,9 @@ ) +_log = logging.getLogger(__name__) + + class EngineConflictError(RuntimeError): """An error raised if an active engine is already initialized. @@ -58,18 +64,45 @@ class RunnerEnginePair(NamedTuple): engine: ProtocolEngine -def get_estop_listener(engine_store: "EngineStore") -> HardwareEventHandler: - """Create a callback for estop events.""" +async def handle_estop_event(engine_store: "EngineStore", event: HardwareEvent) -> None: + """Handle an E-stop event from the hardware API. + + This is meant to run in the engine's thread and asyncio event loop. - def _callback(event: HardwareEvent) -> None: + This is a public function for unit-testing purposes, but it's an implementation + detail of the store. + """ + try: if isinstance(event, EstopStateNotification): if event.new_state is not EstopState.PHYSICALLY_ENGAGED: return if engine_store.current_run_id is None: return - engine_store.engine.estop(maintenance_run=False) + # todo(mm, 2024-04-17): This estop teardown sequencing belongs in the + # runner layer. + engine_store.engine.estop() + await engine_store.engine.finish(error=EStopActivatedError()) + except Exception: + # This is a background task kicked off by a hardware event, + # so there's no one to propagate this exception to. + _log.exception("Exception handling E-stop event.") + + +def _get_estop_listener(engine_store: "EngineStore") -> HardwareEventHandler: + """Create a callback for estop events. + + The returned callback is meant to run in the hardware API's thread. + """ + engine_loop = asyncio.get_running_loop() + + def run_handler_in_engine_thread_from_hardware_thread( + event: HardwareEvent, + ) -> None: + asyncio.run_coroutine_threadsafe( + handle_estop_event(engine_store, event), engine_loop + ) - return _callback + return run_handler_in_engine_thread_from_hardware_thread class EngineStore: @@ -94,7 +127,7 @@ def __init__( self._deck_type = deck_type self._default_engine: Optional[ProtocolEngine] = None self._runner_engine_pair: Optional[RunnerEnginePair] = None - hardware_api.register_callback(get_estop_listener(self)) + hardware_api.register_callback(_get_estop_listener(self)) @property def engine(self) -> ProtocolEngine: diff --git a/robot-server/tests/maintenance_runs/test_engine_store.py b/robot-server/tests/maintenance_runs/test_engine_store.py index 15855ab48d1..948705572ce 100644 --- a/robot-server/tests/maintenance_runs/test_engine_store.py +++ b/robot-server/tests/maintenance_runs/test_engine_store.py @@ -6,6 +6,7 @@ from opentrons_shared_data.robot.dev_types import RobotType +from opentrons.protocol_engine.errors.exceptions import EStopActivatedError from opentrons.types import DeckSlotName from opentrons.hardware_control import API from opentrons.hardware_control.types import EstopStateNotification, EstopState @@ -20,7 +21,7 @@ MaintenanceEngineStore, EngineConflictError, NoRunnerEnginePairError, - get_estop_listener, + handle_estop_event, ) @@ -30,7 +31,7 @@ def mock_notify_publishers() -> None: @pytest.fixture -def subject(decoy: Decoy) -> MaintenanceEngineStore: +async def subject(decoy: Decoy) -> MaintenanceEngineStore: """Get a MaintenanceEngineStore test subject.""" # TODO(mc, 2021-06-11): to make these test more effective and valuable, we # should pass in some sort of actual, valid HardwareAPI instead of a mock @@ -176,22 +177,30 @@ async def test_estop_callback( """The callback should stop an active engine.""" engine_store = decoy.mock(cls=MaintenanceEngineStore) - subject = get_estop_listener(engine_store=engine_store) - - decoy.when(engine_store.current_run_id).then_return(None, "fake_run_id") - disengage_event = EstopStateNotification( old_state=EstopState.PHYSICALLY_ENGAGED, new_state=EstopState.LOGICALLY_ENGAGED ) - - subject(disengage_event) - engage_event = EstopStateNotification( old_state=EstopState.LOGICALLY_ENGAGED, new_state=EstopState.PHYSICALLY_ENGAGED ) - subject(engage_event) - - subject(engage_event) + decoy.when(engine_store.current_run_id).then_return(None) + await handle_estop_event(engine_store, disengage_event) + decoy.verify( + engine_store.engine.estop(), + ignore_extra_args=True, + times=0, + ) + decoy.verify( + await engine_store.engine.finish(), + ignore_extra_args=True, + times=0, + ) - decoy.verify(engine_store.engine.estop(maintenance_run=True), times=1) + decoy.when(engine_store.current_run_id).then_return("fake-run-id") + await handle_estop_event(engine_store, engage_event) + decoy.verify( + engine_store.engine.estop(), + await engine_store.engine.finish(error=matchers.IsA(EStopActivatedError)), + times=1, + ) diff --git a/robot-server/tests/runs/test_engine_store.py b/robot-server/tests/runs/test_engine_store.py index 7a1f79b903a..330e974be9c 100644 --- a/robot-server/tests/runs/test_engine_store.py +++ b/robot-server/tests/runs/test_engine_store.py @@ -1,12 +1,12 @@ """Tests for the EngineStore interface.""" from datetime import datetime -from pathlib import Path import pytest from decoy import Decoy, matchers from opentrons_shared_data import get_shared_data_root from opentrons_shared_data.robot.dev_types import RobotType +from opentrons.protocol_engine.errors.exceptions import EStopActivatedError from opentrons.types import DeckSlotName from opentrons.hardware_control import HardwareControlAPI, API from opentrons.hardware_control.types import EstopStateNotification, EstopState @@ -23,7 +23,7 @@ EngineStore, EngineConflictError, NoRunnerEnginePairError, - get_estop_listener, + handle_estop_event, ) @@ -33,7 +33,7 @@ def mock_notify_publishers() -> None: @pytest.fixture -def subject(decoy: Decoy, hardware_api: HardwareControlAPI) -> EngineStore: +async def subject(decoy: Decoy, hardware_api: HardwareControlAPI) -> EngineStore: """Get a EngineStore test subject.""" return EngineStore( hardware_api=hardware_api, @@ -45,7 +45,7 @@ def subject(decoy: Decoy, hardware_api: HardwareControlAPI) -> EngineStore: @pytest.fixture -async def json_protocol_source(tmp_path: Path) -> ProtocolSource: +async def json_protocol_source() -> ProtocolSource: """Get a protocol source fixture.""" simple_protocol = ( get_shared_data_root() / "protocol" / "fixtures" / "6" / "simpleV6.json" @@ -70,7 +70,6 @@ async def test_create_engine(subject: EngineStore) -> None: async def test_create_engine_with_protocol( - decoy: Decoy, subject: EngineStore, json_protocol_source: ProtocolSource, ) -> None: @@ -311,22 +310,30 @@ async def test_estop_callback( """The callback should stop an active engine.""" engine_store = decoy.mock(cls=EngineStore) - subject = get_estop_listener(engine_store=engine_store) - - decoy.when(engine_store.current_run_id).then_return(None, "fake_run_id") - disengage_event = EstopStateNotification( old_state=EstopState.PHYSICALLY_ENGAGED, new_state=EstopState.LOGICALLY_ENGAGED ) - - subject(disengage_event) - engage_event = EstopStateNotification( old_state=EstopState.LOGICALLY_ENGAGED, new_state=EstopState.PHYSICALLY_ENGAGED ) - subject(engage_event) - - subject(engage_event) + decoy.when(engine_store.current_run_id).then_return(None) + await handle_estop_event(engine_store, disengage_event) + decoy.verify( + engine_store.engine.estop(), + ignore_extra_args=True, + times=0, + ) + decoy.verify( + await engine_store.engine.finish(), + ignore_extra_args=True, + times=0, + ) - decoy.verify(engine_store.engine.estop(maintenance_run=False), times=1) + decoy.when(engine_store.current_run_id).then_return("fake-run-id") + await handle_estop_event(engine_store, engage_event) + decoy.verify( + engine_store.engine.estop(), + await engine_store.engine.finish(error=matchers.IsA(EStopActivatedError)), + times=1, + ) From 8e1794f2df938af0cf58953eb55d4f0530d66789 Mon Sep 17 00:00:00 2001 From: koji Date: Fri, 19 Apr 2024 15:42:08 -0400 Subject: [PATCH 169/194] chore: remove downgrade npm (#14898) * chore: remove downgrade npm --- .github/workflows/app-test-build-deploy.yaml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/app-test-build-deploy.yaml b/.github/workflows/app-test-build-deploy.yaml index 8d0658a930e..738fa369e58 100644 --- a/.github/workflows/app-test-build-deploy.yaml +++ b/.github/workflows/app-test-build-deploy.yaml @@ -112,8 +112,6 @@ jobs: - uses: actions/setup-python@v4 with: python-version: '3.10' - - name: 'downgrade npm version' - run: npm install -g npm@6 - name: check make version run: make --version - name: 'install libudev and libsystemd' @@ -245,8 +243,6 @@ jobs: - uses: actions/setup-python@v4 with: python-version: '3.10' - - name: 'downgrade npm version' - run: npm install -g npm@6 - name: check make version run: make --version - name: 'install libudev and libsystemd' From 15bfd98a24bdfef77ded2bca64817384b2ecc878 Mon Sep 17 00:00:00 2001 From: CaseyBatten Date: Fri, 19 Apr 2024 15:51:30 -0400 Subject: [PATCH 170/194] refactor(api): Relocate module location validation to engine (#14960) Prevents the engine from accepting load module commands in locations for modules that match the deck configuraiton but would not have passed protocol core validation. --- .../protocol_api/core/engine/protocol.py | 26 --- .../protocol_engine/commands/load_module.py | 31 +++ .../core/engine/test_protocol_core.py | 162 -------------- .../commands/test_load_module.py | 204 +++++++++++++++++- .../test_load_module_success.tavern.yaml | 2 +- 5 files changed, 227 insertions(+), 198 deletions(-) diff --git a/api/src/opentrons/protocol_api/core/engine/protocol.py b/api/src/opentrons/protocol_api/core/engine/protocol.py index 68b86cbfe34..4089dff4b4d 100644 --- a/api/src/opentrons/protocol_api/core/engine/protocol.py +++ b/api/src/opentrons/protocol_api/core/engine/protocol.py @@ -4,7 +4,6 @@ from opentrons.protocol_engine.commands import LoadModuleResult from opentrons_shared_data.deck.dev_types import DeckDefinitionV5, SlotDefV3 -from opentrons.protocol_engine.resources import deck_configuration_provider from opentrons_shared_data.labware.labware_definition import LabwareDefinition from opentrons_shared_data.labware.dev_types import LabwareDefinition as LabwareDefDict from opentrons_shared_data.pipette.dev_types import PipetteNameType @@ -410,7 +409,6 @@ def load_module( robot_type = self._engine_client.state.config.robot_type normalized_deck_slot = deck_slot.to_equivalent_for_robot_type(robot_type) - self._ensure_module_location(normalized_deck_slot, module_type) result = self._engine_client.load_module( model=EngineModuleModel(model), @@ -623,30 +621,6 @@ def get_staging_slot_definitions(self) -> Dict[str, SlotDefV3]: self._engine_client.state.addressable_areas.get_staging_slot_definitions() ) - def _ensure_module_location( - self, slot: DeckSlotName, module_type: ModuleType - ) -> None: - if self._engine_client.state.config.robot_type == "OT-2 Standard": - slot_def = self.get_slot_definition(slot) - compatible_modules = slot_def["compatibleModuleTypes"] - if module_type.value not in compatible_modules: - raise ValueError( - f"A {module_type.value} cannot be loaded into slot {slot}" - ) - else: - cutout_fixture_id = ModuleType.to_module_fixture_id(module_type) - module_fixture = deck_configuration_provider.get_cutout_fixture( - cutout_fixture_id, - self._engine_client.state.addressable_areas.state.deck_definition, - ) - cutout_id = self._engine_client.state.addressable_areas.get_cutout_id_by_deck_slot_name( - slot - ) - if cutout_id not in module_fixture["mayMountTo"]: - raise ValueError( - f"A {module_type.value} cannot be loaded into slot {slot}" - ) - def get_slot_item( self, slot_name: Union[DeckSlotName, StagingSlotName] ) -> Union[LabwareCore, ModuleCore, NonConnectedModuleCore, None]: diff --git a/api/src/opentrons/protocol_engine/commands/load_module.py b/api/src/opentrons/protocol_engine/commands/load_module.py index dcaa396a245..5c1d474be4d 100644 --- a/api/src/opentrons/protocol_engine/commands/load_module.py +++ b/api/src/opentrons/protocol_engine/commands/load_module.py @@ -7,9 +7,13 @@ from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate from ..types import ( DeckSlotLocation, + ModuleType, ModuleModel, ModuleDefinition, ) +from opentrons.types import DeckSlotName + +from opentrons.protocol_engine.resources import deck_configuration_provider if TYPE_CHECKING: from ..state import StateView @@ -108,6 +112,9 @@ def __init__( async def execute(self, params: LoadModuleParams) -> LoadModuleResult: """Check that the requested module is attached and assign its identifier.""" + module_type = params.model.as_type() + self._ensure_module_location(params.location.slotName, module_type) + if self._state_view.config.robot_type == "OT-2 Standard": self._state_view.addressable_areas.raise_if_area_not_in_deck_configuration( params.location.slotName.id @@ -146,6 +153,30 @@ async def execute(self, params: LoadModuleParams) -> LoadModuleResult: definition=loaded_module.definition, ) + def _ensure_module_location( + self, slot: DeckSlotName, module_type: ModuleType + ) -> None: + if self._state_view.config.robot_type == "OT-2 Standard": + slot_def = self._state_view.addressable_areas.get_slot_definition(slot.id) + compatible_modules = slot_def["compatibleModuleTypes"] + if module_type.value not in compatible_modules: + raise ValueError( + f"A {module_type.value} cannot be loaded into slot {slot}" + ) + else: + cutout_fixture_id = ModuleType.to_module_fixture_id(module_type) + module_fixture = deck_configuration_provider.get_cutout_fixture( + cutout_fixture_id, + self._state_view.addressable_areas.state.deck_definition, + ) + cutout_id = ( + self._state_view.addressable_areas.get_cutout_id_by_deck_slot_name(slot) + ) + if cutout_id not in module_fixture["mayMountTo"]: + raise ValueError( + f"A {module_type.value} cannot be loaded into slot {slot}" + ) + class LoadModule(BaseCommand[LoadModuleParams, LoadModuleResult]): """The model for a load module command.""" diff --git a/api/tests/opentrons/protocol_api/core/engine/test_protocol_core.py b/api/tests/opentrons/protocol_api/core/engine/test_protocol_core.py index d5e71f56f46..8f6589b1104 100644 --- a/api/tests/opentrons/protocol_api/core/engine/test_protocol_core.py +++ b/api/tests/opentrons/protocol_api/core/engine/test_protocol_core.py @@ -1264,168 +1264,6 @@ def test_load_module( assert subject.get_labware_on_module(result) is None -@pytest.mark.parametrize( - ( - "requested_model", - "engine_model", - "expected_core_cls", - "deck_def", - "slot_name", - "robot_type", - ), - [ - ( - TemperatureModuleModel.TEMPERATURE_V2, - EngineModuleModel.TEMPERATURE_MODULE_V2, - TemperatureModuleCore, - lazy_fixture("ot3_standard_deck_def"), - DeckSlotName.SLOT_D2, - "OT-3 Standard", - ), - ( - ThermocyclerModuleModel.THERMOCYCLER_V1, - EngineModuleModel.THERMOCYCLER_MODULE_V1, - ThermocyclerModuleCore, - lazy_fixture("ot2_standard_deck_def"), - DeckSlotName.SLOT_1, - "OT-2 Standard", - ), - ( - ThermocyclerModuleModel.THERMOCYCLER_V2, - EngineModuleModel.THERMOCYCLER_MODULE_V2, - ThermocyclerModuleCore, - lazy_fixture("ot3_standard_deck_def"), - DeckSlotName.SLOT_A2, - "OT-3 Standard", - ), - ( - HeaterShakerModuleModel.HEATER_SHAKER_V1, - EngineModuleModel.HEATER_SHAKER_MODULE_V1, - HeaterShakerModuleCore, - lazy_fixture("ot3_standard_deck_def"), - DeckSlotName.SLOT_A2, - "OT-3 Standard", - ), - ], -) -def test_load_module_raises_wrong_location( - decoy: Decoy, - mock_engine_client: EngineClient, - mock_sync_hardware_api: SyncHardwareAPI, - requested_model: ModuleModel, - engine_model: EngineModuleModel, - expected_core_cls: Type[ModuleCore], - subject: ProtocolCore, - deck_def: DeckDefinitionV5, - slot_name: DeckSlotName, - robot_type: RobotType, -) -> None: - """It should issue a load module engine command.""" - mock_hw_mod_1 = decoy.mock(cls=AbstractModule) - mock_hw_mod_2 = decoy.mock(cls=AbstractModule) - - decoy.when(mock_hw_mod_1.device_info).then_return({"serial": "abc123"}) - decoy.when(mock_hw_mod_2.device_info).then_return({"serial": "xyz789"}) - decoy.when(mock_sync_hardware_api.attached_modules).then_return( - [mock_hw_mod_1, mock_hw_mod_2] - ) - - decoy.when(mock_engine_client.state.config.robot_type).then_return(robot_type) - - if robot_type == "OT-2 Standard": - decoy.when(subject.get_slot_definition(slot_name)).then_return( - cast(SlotDefV3, {"compatibleModuleTypes": []}) - ) - else: - decoy.when( - mock_engine_client.state.addressable_areas.state.deck_definition - ).then_return(deck_def) - decoy.when( - mock_engine_client.state.addressable_areas.get_cutout_id_by_deck_slot_name( - slot_name - ) - ).then_return("cutout" + slot_name.value) - - with pytest.raises( - ValueError, - match=f"A {ModuleType.from_model(requested_model).value} cannot be loaded into slot {slot_name}", - ): - subject.load_module( - model=requested_model, - deck_slot=slot_name, - configuration=None, - ) - - -@pytest.mark.parametrize( - ( - "requested_model", - "engine_model", - "expected_core_cls", - "deck_def", - "slot_name", - "robot_type", - ), - [ - ( - MagneticModuleModel.MAGNETIC_V2, - EngineModuleModel.MAGNETIC_MODULE_V2, - MagneticModuleCore, - lazy_fixture("ot3_standard_deck_def"), - DeckSlotName.SLOT_A2, - "OT-3 Standard", - ), - ], -) -def test_load_module_raises_module_fixture_id_does_not_exist( - decoy: Decoy, - mock_engine_client: EngineClient, - mock_sync_hardware_api: SyncHardwareAPI, - requested_model: ModuleModel, - engine_model: EngineModuleModel, - expected_core_cls: Type[ModuleCore], - subject: ProtocolCore, - deck_def: DeckDefinitionV5, - slot_name: DeckSlotName, - robot_type: RobotType, -) -> None: - """It should issue a load module engine command and raise an error for unmatched fixtures.""" - mock_hw_mod_1 = decoy.mock(cls=AbstractModule) - mock_hw_mod_2 = decoy.mock(cls=AbstractModule) - - decoy.when(mock_hw_mod_1.device_info).then_return({"serial": "abc123"}) - decoy.when(mock_hw_mod_2.device_info).then_return({"serial": "xyz789"}) - decoy.when(mock_sync_hardware_api.attached_modules).then_return( - [mock_hw_mod_1, mock_hw_mod_2] - ) - - decoy.when(mock_engine_client.state.config.robot_type).then_return(robot_type) - - if robot_type == "OT-2 Standard": - decoy.when(subject.get_slot_definition(slot_name)).then_return( - cast(SlotDefV3, {"compatibleModuleTypes": []}) - ) - else: - decoy.when( - mock_engine_client.state.addressable_areas.state.deck_definition - ).then_return(deck_def) - decoy.when( - mock_engine_client.state.addressable_areas.get_cutout_id_by_deck_slot_name( - slot_name - ) - ).then_return("cutout" + slot_name.value) - - with pytest.raises( - ValueError, - match=f"Module Type {ModuleType.from_model(requested_model).value} does not have a related fixture ID.", - ): - subject.load_module( - model=requested_model, - deck_slot=slot_name, - configuration=None, - ) - - # APIv2.15 because we're expecting a fixed trash. @pytest.mark.parametrize("api_version", [APIVersion(2, 15)]) def test_load_mag_block( diff --git a/api/tests/opentrons/protocol_engine/commands/test_load_module.py b/api/tests/opentrons/protocol_engine/commands/test_load_module.py index 84be22d4661..65306f34adc 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_load_module.py +++ b/api/tests/opentrons/protocol_engine/commands/test_load_module.py @@ -1,9 +1,11 @@ """Test load module command.""" import pytest +from typing import cast from decoy import Decoy from opentrons.protocol_engine.errors import LocationIsOccupiedError from opentrons.protocol_engine.state import StateView +from opentrons_shared_data.robot.dev_types import RobotType from opentrons.types import DeckSlotName from opentrons.protocol_engine.types import ( DeckSlotLocation, @@ -11,12 +13,30 @@ ModuleDefinition, ) from opentrons.protocol_engine.execution import EquipmentHandler, LoadedModuleData +from opentrons.protocol_engine import ModuleModel as EngineModuleModel +from opentrons.hardware_control.modules import ModuleType from opentrons.protocol_engine.commands.load_module import ( LoadModuleParams, LoadModuleResult, LoadModuleImplementation, ) +from opentrons.hardware_control.modules.types import ( + ModuleModel as HardwareModuleModel, + TemperatureModuleModel, + MagneticModuleModel, + ThermocyclerModuleModel, + HeaterShakerModuleModel, +) +from opentrons_shared_data.deck.dev_types import ( + DeckDefinitionV5, + SlotDefV3, +) +from opentrons_shared_data.deck import load as load_deck +from opentrons.protocols.api_support.deck_type import ( + STANDARD_OT2_DECK, + STANDARD_OT3_DECK, +) async def test_load_module_implementation( @@ -29,19 +49,29 @@ async def test_load_module_implementation( subject = LoadModuleImplementation(equipment=equipment, state_view=state_view) data = LoadModuleParams( - model=ModuleModel.TEMPERATURE_MODULE_V1, - location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), + model=ModuleModel.TEMPERATURE_MODULE_V2, + location=DeckSlotLocation(slotName=DeckSlotName.SLOT_D1), moduleId="some-id", ) + + deck_def = load_deck(STANDARD_OT3_DECK, 5) + + decoy.when(state_view.addressable_areas.state.deck_definition).then_return(deck_def) + decoy.when( + state_view.addressable_areas.get_cutout_id_by_deck_slot_name( + DeckSlotName.SLOT_D1 + ) + ).then_return("cutout" + DeckSlotName.SLOT_D1.value) + decoy.when( state_view.geometry.ensure_location_not_occupied( - DeckSlotLocation(slotName=DeckSlotName.SLOT_1) + DeckSlotLocation(slotName=DeckSlotName.SLOT_D1) ) ).then_return(DeckSlotLocation(slotName=DeckSlotName.SLOT_2)) decoy.when( await equipment.load_module( - model=ModuleModel.TEMPERATURE_MODULE_V1, + model=ModuleModel.TEMPERATURE_MODULE_V2, location=DeckSlotLocation(slotName=DeckSlotName.SLOT_2), module_id="some-id", ) @@ -73,12 +103,22 @@ async def test_load_module_implementation_mag_block( data = LoadModuleParams( model=ModuleModel.MAGNETIC_BLOCK_V1, - location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), + location=DeckSlotLocation(slotName=DeckSlotName.SLOT_D1), moduleId="some-id", ) + + deck_def = load_deck(STANDARD_OT3_DECK, 5) + + decoy.when(state_view.addressable_areas.state.deck_definition).then_return(deck_def) + decoy.when( + state_view.addressable_areas.get_cutout_id_by_deck_slot_name( + DeckSlotName.SLOT_D1 + ) + ).then_return("cutout" + DeckSlotName.SLOT_D1.value) + decoy.when( state_view.geometry.ensure_location_not_occupied( - DeckSlotLocation(slotName=DeckSlotName.SLOT_1) + DeckSlotLocation(slotName=DeckSlotName.SLOT_D1) ) ).then_return(DeckSlotLocation(slotName=DeckSlotName.SLOT_2)) @@ -114,16 +154,162 @@ async def test_load_module_raises_if_location_occupied( subject = LoadModuleImplementation(equipment=equipment, state_view=state_view) data = LoadModuleParams( - model=ModuleModel.TEMPERATURE_MODULE_V1, - location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), + model=ModuleModel.TEMPERATURE_MODULE_V2, + location=DeckSlotLocation(slotName=DeckSlotName.SLOT_D1), moduleId="some-id", ) + deck_def = load_deck(STANDARD_OT3_DECK, 5) + + decoy.when(state_view.addressable_areas.state.deck_definition).then_return(deck_def) + decoy.when( + state_view.addressable_areas.get_cutout_id_by_deck_slot_name( + DeckSlotName.SLOT_D1 + ) + ).then_return("cutout" + DeckSlotName.SLOT_D1.value) + decoy.when( state_view.geometry.ensure_location_not_occupied( - DeckSlotLocation(slotName=DeckSlotName.SLOT_1) + DeckSlotLocation(slotName=DeckSlotName.SLOT_D1) ) ).then_raise(LocationIsOccupiedError("Get your own spot!")) with pytest.raises(LocationIsOccupiedError): await subject.execute(data) + + +@pytest.mark.parametrize( + ( + "requested_model", + "engine_model", + "deck_def", + "slot_name", + "robot_type", + ), + [ + ( + TemperatureModuleModel.TEMPERATURE_V2, + EngineModuleModel.TEMPERATURE_MODULE_V2, + load_deck(STANDARD_OT3_DECK, 5), + DeckSlotName.SLOT_D2, + "OT-3 Standard", + ), + ( + ThermocyclerModuleModel.THERMOCYCLER_V1, + EngineModuleModel.THERMOCYCLER_MODULE_V1, + load_deck(STANDARD_OT2_DECK, 5), + DeckSlotName.SLOT_1, + "OT-2 Standard", + ), + ( + ThermocyclerModuleModel.THERMOCYCLER_V2, + EngineModuleModel.THERMOCYCLER_MODULE_V2, + load_deck(STANDARD_OT3_DECK, 5), + DeckSlotName.SLOT_A2, + "OT-3 Standard", + ), + ( + HeaterShakerModuleModel.HEATER_SHAKER_V1, + EngineModuleModel.HEATER_SHAKER_MODULE_V1, + load_deck(STANDARD_OT3_DECK, 5), + DeckSlotName.SLOT_A2, + "OT-3 Standard", + ), + ], +) +async def test_load_module_raises_wrong_location( + decoy: Decoy, + equipment: EquipmentHandler, + state_view: StateView, + requested_model: HardwareModuleModel, + engine_model: EngineModuleModel, + deck_def: DeckDefinitionV5, + slot_name: DeckSlotName, + robot_type: RobotType, +) -> None: + """It should issue a load module engine command.""" + subject = LoadModuleImplementation(equipment=equipment, state_view=state_view) + + data = LoadModuleParams( + model=engine_model, + location=DeckSlotLocation(slotName=slot_name), + moduleId="some-id", + ) + + decoy.when(state_view.config.robot_type).then_return(robot_type) + + if robot_type == "OT-2 Standard": + decoy.when( + state_view.addressable_areas.get_slot_definition(slot_name.id) + ).then_return(cast(SlotDefV3, {"compatibleModuleTypes": []})) + else: + decoy.when(state_view.addressable_areas.state.deck_definition).then_return( + deck_def + ) + decoy.when( + state_view.addressable_areas.get_cutout_id_by_deck_slot_name(slot_name) + ).then_return("cutout" + slot_name.value) + + with pytest.raises( + ValueError, + match=f"A {ModuleType.from_model(model=requested_model).value} cannot be loaded into slot {slot_name}", + ): + await subject.execute(data) + + +@pytest.mark.parametrize( + ( + "requested_model", + "engine_model", + "deck_def", + "slot_name", + "robot_type", + ), + [ + ( + MagneticModuleModel.MAGNETIC_V2, + EngineModuleModel.MAGNETIC_MODULE_V2, + load_deck(STANDARD_OT3_DECK, 5), + DeckSlotName.SLOT_A2, + "OT-3 Standard", + ), + ], +) +async def test_load_module_raises_module_fixture_id_does_not_exist( + decoy: Decoy, + equipment: EquipmentHandler, + state_view: StateView, + requested_model: HardwareModuleModel, + engine_model: EngineModuleModel, + deck_def: DeckDefinitionV5, + slot_name: DeckSlotName, + robot_type: RobotType, +) -> None: + """It should issue a load module engine command and raise an error for unmatched fixtures.""" + subject = LoadModuleImplementation(equipment=equipment, state_view=state_view) + + data = LoadModuleParams( + model=engine_model, + location=DeckSlotLocation(slotName=slot_name), + moduleId="some-id", + ) + + decoy.when(state_view.config.robot_type).then_return(robot_type) + + if robot_type == "OT-2 Standard": + decoy.when( + state_view.addressable_areas.get_slot_definition(slot_name.id) + ).then_return(cast(SlotDefV3, {"compatibleModuleTypes": []})) + else: + decoy.when(state_view.addressable_areas.state.deck_definition).then_return( + deck_def + ) + decoy.when( + state_view.addressable_areas.get_cutout_id_by_deck_slot_name(slot_name) + ).then_return("cutout" + slot_name.value) + + with pytest.raises( + ValueError, + match=f"Module Type {ModuleType.from_model(requested_model).value} does not have a related fixture ID.", + ): + await subject.execute(data) diff --git a/robot-server/tests/integration/http_api/commands/test_load_module_success.tavern.yaml b/robot-server/tests/integration/http_api/commands/test_load_module_success.tavern.yaml index 8e4e99528a7..c9cc22f8e0d 100644 --- a/robot-server/tests/integration/http_api/commands/test_load_module_success.tavern.yaml +++ b/robot-server/tests/integration/http_api/commands/test_load_module_success.tavern.yaml @@ -49,7 +49,7 @@ stages: params: model: '{model}' location: - slotName: '10' + slotName: '7' response: strict: - json:off From b5a9115a5c95b9ce29f611b478e60766bbeadd28 Mon Sep 17 00:00:00 2001 From: Derek Maggio Date: Fri, 19 Apr 2024 12:51:58 -0700 Subject: [PATCH 171/194] chore: create test-data-generation project in monorepo (#14961) --- test-data-generation/.flake8 | 25 ++ test-data-generation/Makefile | 32 +++ test-data-generation/Pipfile | 20 ++ test-data-generation/Pipfile.lock | 365 ++++++++++++++++++++++++++++++ test-data-generation/mypy.ini | 5 + test-data-generation/pytest.ini | 3 + test-data-generation/setup.py | 91 ++++++++ 7 files changed, 541 insertions(+) create mode 100644 test-data-generation/.flake8 create mode 100644 test-data-generation/Makefile create mode 100644 test-data-generation/Pipfile create mode 100644 test-data-generation/Pipfile.lock create mode 100644 test-data-generation/mypy.ini create mode 100644 test-data-generation/pytest.ini create mode 100755 test-data-generation/setup.py diff --git a/test-data-generation/.flake8 b/test-data-generation/.flake8 new file mode 100644 index 00000000000..4aa1c02d7aa --- /dev/null +++ b/test-data-generation/.flake8 @@ -0,0 +1,25 @@ +[flake8] + +# max cyclomatic complexity +max-complexity = 9 + +extend-ignore = + # defer formatting concerns to black + # E203: space around `:` operator + # E501: maximum line length + E203, + E501, + # do not require type annotations for self nor cls + ANN101, + ANN102 + # do not require docstring for __init__, put them on the class + D107, + +# configure flake8-docstrings +# https://pypi.org/project/flake8-docstrings/ +docstring-convention = google + +noqa-require-code = true + +per-file-ignores = + setup.py:ANN,D \ No newline at end of file diff --git a/test-data-generation/Makefile b/test-data-generation/Makefile new file mode 100644 index 00000000000..03c881dbf89 --- /dev/null +++ b/test-data-generation/Makefile @@ -0,0 +1,32 @@ +include ../scripts/python.mk + +.PHONY: lint +lint: + $(python) -m black --check . + $(python) -m flake8 . + $(python) -m mypy . + +.PHONY: format +format: + $(python) -m black . + +.PHONY: setup +setup: + $(pipenv) sync --dev + +.PHONY: teardown +teardown: + $(pipenv) --rm + +.PHONY: clean +clean: + rm -rf build dist *.egg-info .mypy_cache .pytest_cache src/test_data_generation.egg-info + +.PHONY: wheel +wheel: + $(python) setup.py $(wheel_opts) bdist_wheel + rm -rf build + +.PHONY: test +test: + $(pytest) tests -vvv \ No newline at end of file diff --git a/test-data-generation/Pipfile b/test-data-generation/Pipfile new file mode 100644 index 00000000000..758bcddacb7 --- /dev/null +++ b/test-data-generation/Pipfile @@ -0,0 +1,20 @@ +[[source]] +name = "pypi" +url = "https://pypi.org/simple" +verify_ssl = true + +[packages] +pytest = "==7.4.3" +black = "==23.11.0" +mypy = "==1.7.1" +flake8 = "==7.0.0" +flake8-annotations = "~=3.0.1" +flake8-docstrings = "~=1.7.0" +flake8-noqa = "~=1.4.0" +hypothesis = "==6.96.1" +opentrons-shared-data = {file = "../shared-data/python", editable = true} +test-data-generation = {file = ".", editable = true} + + +[requires] +python_version = "3.10" diff --git a/test-data-generation/Pipfile.lock b/test-data-generation/Pipfile.lock new file mode 100644 index 00000000000..1b223033d61 --- /dev/null +++ b/test-data-generation/Pipfile.lock @@ -0,0 +1,365 @@ +{ + "_meta": { + "hash": { + "sha256": "1df89f797a19f2c0febc582e7452a52858511cece041f9f612a59d35628226c2" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.10" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "attrs": { + "hashes": [ + "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30", + "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1" + ], + "markers": "python_version >= '3.7'", + "version": "==23.2.0" + }, + "black": { + "hashes": [ + "sha256:250d7e60f323fcfc8ea6c800d5eba12f7967400eb6c2d21ae85ad31c204fb1f4", + "sha256:2a9acad1451632021ee0d146c8765782a0c3846e0e0ea46659d7c4f89d9b212b", + "sha256:412f56bab20ac85927f3a959230331de5614aecda1ede14b373083f62ec24e6f", + "sha256:421f3e44aa67138ab1b9bfbc22ee3780b22fa5b291e4db8ab7eee95200726b07", + "sha256:45aa1d4675964946e53ab81aeec7a37613c1cb71647b5394779e6efb79d6d187", + "sha256:4c44b7211a3a0570cc097e81135faa5f261264f4dfaa22bd5ee2875a4e773bd6", + "sha256:4c68855825ff432d197229846f971bc4d6666ce90492e5b02013bcaca4d9ab05", + "sha256:5133f5507007ba08d8b7b263c7aa0f931af5ba88a29beacc4b2dc23fcefe9c06", + "sha256:54caaa703227c6e0c87b76326d0862184729a69b73d3b7305b6288e1d830067e", + "sha256:58e5f4d08a205b11800332920e285bd25e1a75c54953e05502052738fe16b3b5", + "sha256:698c1e0d5c43354ec5d6f4d914d0d553a9ada56c85415700b81dc90125aac244", + "sha256:6c1cac07e64433f646a9a838cdc00c9768b3c362805afc3fce341af0e6a9ae9f", + "sha256:760415ccc20f9e8747084169110ef75d545f3b0932ee21368f63ac0fee86b221", + "sha256:7f622b6822f02bfaf2a5cd31fdb7cd86fcf33dab6ced5185c35f5db98260b055", + "sha256:cf57719e581cfd48c4efe28543fea3d139c6b6f1238b3f0102a9c73992cbb479", + "sha256:d136ef5b418c81660ad847efe0e55c58c8208b77a57a28a503a5f345ccf01394", + "sha256:dbea0bb8575c6b6303cc65017b46351dc5953eea5c0a59d7b7e3a2d2f433a911", + "sha256:fc7f6a44d52747e65a02558e1d807c82df1d66ffa80a601862040a43ec2e3142" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==23.11.0" + }, + "click": { + "hashes": [ + "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", + "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de" + ], + "markers": "python_version >= '3.7'", + "version": "==8.1.7" + }, + "exceptiongroup": { + "hashes": [ + "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad", + "sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16" + ], + "markers": "python_version < '3.11'", + "version": "==1.2.1" + }, + "flake8": { + "hashes": [ + "sha256:33f96621059e65eec474169085dc92bf26e7b2d47366b70be2f67ab80dc25132", + "sha256:a6dfbb75e03252917f2473ea9653f7cd799c3064e54d4c8140044c5c065f53c3" + ], + "index": "pypi", + "markers": "python_full_version >= '3.8.1'", + "version": "==7.0.0" + }, + "flake8-annotations": { + "hashes": [ + "sha256:af78e3216ad800d7e144745ece6df706c81b3255290cbf870e54879d495e8ade", + "sha256:ff37375e71e3b83f2a5a04d443c41e2c407de557a884f3300a7fa32f3c41cb0a" + ], + "index": "pypi", + "markers": "python_full_version >= '3.8.1'", + "version": "==3.0.1" + }, + "flake8-docstrings": { + "hashes": [ + "sha256:4c8cc748dc16e6869728699e5d0d685da9a10b0ea718e090b1ba088e67a941af", + "sha256:51f2344026da083fc084166a9353f5082b01f72901df422f74b4d953ae88ac75" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==1.7.0" + }, + "flake8-noqa": { + "hashes": [ + "sha256:4465e16a19be433980f6f563d05540e2e54797eb11facb9feb50fed60624dc45", + "sha256:771765ab27d1efd157528379acd15131147f9ae578a72d17fb432ca197881243" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==1.4.0" + }, + "hypothesis": { + "hashes": [ + "sha256:848ea0952f0bdfd02eac59e41b03f1cbba8fa2cffeffa8db328bbd6cfe159974", + "sha256:955a57e56be4607c81c17ca53e594af54aadeed91e07b88bb7f84e8208ea7739" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==6.96.1" + }, + "iniconfig": { + "hashes": [ + "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", + "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374" + ], + "markers": "python_version >= '3.7'", + "version": "==2.0.0" + }, + "jsonschema": { + "hashes": [ + "sha256:0f864437ab8b6076ba6707453ef8f98a6a0d512a80e93f8abdb676f737ecb60d", + "sha256:a870ad254da1a8ca84b6a2905cac29d265f805acc57af304784962a2aa6508f6" + ], + "markers": "python_version >= '3.7'", + "version": "==4.17.3" + }, + "mccabe": { + "hashes": [ + "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", + "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e" + ], + "markers": "python_version >= '3.6'", + "version": "==0.7.0" + }, + "mypy": { + "hashes": [ + "sha256:12cce78e329838d70a204293e7b29af9faa3ab14899aec397798a4b41be7f340", + "sha256:1484b8fa2c10adf4474f016e09d7a159602f3239075c7bf9f1627f5acf40ad49", + "sha256:204e0d6de5fd2317394a4eff62065614c4892d5a4d1a7ee55b765d7a3d9e3f82", + "sha256:2643d145af5292ee956aa0a83c2ce1038a3bdb26e033dadeb2f7066fb0c9abce", + "sha256:2c6e4464ed5f01dc44dc9821caf67b60a4e5c3b04278286a85c067010653a0eb", + "sha256:2f7f6985d05a4e3ce8255396df363046c28bea790e40617654e91ed580ca7c51", + "sha256:31902408f4bf54108bbfb2e35369877c01c95adc6192958684473658c322c8a5", + "sha256:40716d1f821b89838589e5b3106ebbc23636ffdef5abc31f7cd0266db936067e", + "sha256:4b901927f16224d0d143b925ce9a4e6b3a758010673eeded9b748f250cf4e8f7", + "sha256:4fc3d14ee80cd22367caaaf6e014494415bf440980a3045bf5045b525680ac33", + "sha256:5cf3f0c5ac72139797953bd50bc6c95ac13075e62dbfcc923571180bebb662e9", + "sha256:6dbdec441c60699288adf051f51a5d512b0d818526d1dcfff5a41f8cd8b4aaf1", + "sha256:72cf32ce7dd3562373f78bd751f73c96cfb441de147cc2448a92c1a308bd0ca6", + "sha256:75aa828610b67462ffe3057d4d8a4112105ed211596b750b53cbfe182f44777a", + "sha256:75c4d2a6effd015786c87774e04331b6da863fc3fc4e8adfc3b40aa55ab516fe", + "sha256:78e25b2fd6cbb55ddfb8058417df193f0129cad5f4ee75d1502248e588d9e0d7", + "sha256:84860e06ba363d9c0eeabd45ac0fde4b903ad7aa4f93cd8b648385a888e23200", + "sha256:8c5091ebd294f7628eb25ea554852a52058ac81472c921150e3a61cdd68f75a7", + "sha256:944bdc21ebd620eafefc090cdf83158393ec2b1391578359776c00de00e8907a", + "sha256:9c7ac372232c928fff0645d85f273a726970c014749b924ce5710d7d89763a28", + "sha256:d9b338c19fa2412f76e17525c1b4f2c687a55b156320acb588df79f2e6fa9fea", + "sha256:ee5d62d28b854eb61889cde4e1dbc10fbaa5560cb39780c3995f6737f7e82120", + "sha256:f2c2521a8e4d6d769e3234350ba7b65ff5d527137cdcde13ff4d99114b0c8e7d", + "sha256:f6efc9bd72258f89a3816e3a98c09d36f079c223aa345c659622f056b760ab42", + "sha256:f7c5d642db47376a0cc130f0de6d055056e010debdaf0707cd2b0fc7e7ef30ea", + "sha256:fcb6d9afb1b6208b4c712af0dafdc650f518836065df0d4fb1d800f5d6773db2", + "sha256:fcd2572dd4519e8a6642b733cd3a8cfc1ef94bafd0c1ceed9c94fe736cb65b6a" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==1.7.1" + }, + "mypy-extensions": { + "hashes": [ + "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", + "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782" + ], + "markers": "python_version >= '3.5'", + "version": "==1.0.0" + }, + "opentrons-shared-data": { + "editable": true, + "file": "../shared-data/python", + "markers": "python_version >= '3.8'" + }, + "packaging": { + "hashes": [ + "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5", + "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9" + ], + "markers": "python_version >= '3.7'", + "version": "==24.0" + }, + "pathspec": { + "hashes": [ + "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", + "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712" + ], + "markers": "python_version >= '3.8'", + "version": "==0.12.1" + }, + "platformdirs": { + "hashes": [ + "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068", + "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768" + ], + "markers": "python_version >= '3.8'", + "version": "==4.2.0" + }, + "pluggy": { + "hashes": [ + "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981", + "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be" + ], + "markers": "python_version >= '3.8'", + "version": "==1.4.0" + }, + "pycodestyle": { + "hashes": [ + "sha256:41ba0e7afc9752dfb53ced5489e89f8186be00e599e712660695b7a75ff2663f", + "sha256:44fe31000b2d866f2e41841b18528a505fbd7fef9017b04eff4e2648a0fadc67" + ], + "markers": "python_version >= '3.8'", + "version": "==2.11.1" + }, + "pydantic": { + "hashes": [ + "sha256:005655cabc29081de8243126e036f2065bd7ea5b9dff95fde6d2c642d39755de", + "sha256:0d142fa1b8f2f0ae11ddd5e3e317dcac060b951d605fda26ca9b234b92214986", + "sha256:22ed12ee588b1df028a2aa5d66f07bf8f8b4c8579c2e96d5a9c1f96b77f3bb55", + "sha256:2746189100c646682eff0bce95efa7d2e203420d8e1c613dc0c6b4c1d9c1fde4", + "sha256:28e552a060ba2740d0d2aabe35162652c1459a0b9069fe0db7f4ee0e18e74d58", + "sha256:3287e1614393119c67bd4404f46e33ae3be3ed4cd10360b48d0a4459f420c6a3", + "sha256:3350f527bb04138f8aff932dc828f154847fbdc7a1a44c240fbfff1b57f49a12", + "sha256:3453685ccd7140715e05f2193d64030101eaad26076fad4e246c1cc97e1bb30d", + "sha256:394f08750bd8eaad714718812e7fab615f873b3cdd0b9d84e76e51ef3b50b6b7", + "sha256:4e316e54b5775d1eb59187f9290aeb38acf620e10f7fd2f776d97bb788199e53", + "sha256:50f1666a9940d3d68683c9d96e39640f709d7a72ff8702987dab1761036206bb", + "sha256:51d405b42f1b86703555797270e4970a9f9bd7953f3990142e69d1037f9d9e51", + "sha256:584f2d4c98ffec420e02305cf675857bae03c9d617fcfdc34946b1160213a948", + "sha256:5e09c19df304b8123938dc3c53d3d3be6ec74b9d7d0d80f4f4b5432ae16c2022", + "sha256:676ed48f2c5bbad835f1a8ed8a6d44c1cd5a21121116d2ac40bd1cd3619746ed", + "sha256:67f1a1fb467d3f49e1708a3f632b11c69fccb4e748a325d5a491ddc7b5d22383", + "sha256:6a51a1dd4aa7b3f1317f65493a182d3cff708385327c1c82c81e4a9d6d65b2e4", + "sha256:6bd7030c9abc80134087d8b6e7aa957e43d35714daa116aced57269a445b8f7b", + "sha256:75279d3cac98186b6ebc2597b06bcbc7244744f6b0b44a23e4ef01e5683cc0d2", + "sha256:7ac9237cd62947db00a0d16acf2f3e00d1ae9d3bd602b9c415f93e7a9fc10528", + "sha256:7ea210336b891f5ea334f8fc9f8f862b87acd5d4a0cbc9e3e208e7aa1775dabf", + "sha256:82790d4753ee5d00739d6cb5cf56bceb186d9d6ce134aca3ba7befb1eedbc2c8", + "sha256:92229f73400b80c13afcd050687f4d7e88de9234d74b27e6728aa689abcf58cc", + "sha256:9bea1f03b8d4e8e86702c918ccfd5d947ac268f0f0cc6ed71782e4b09353b26f", + "sha256:a980a77c52723b0dc56640ced396b73a024d4b74f02bcb2d21dbbac1debbe9d0", + "sha256:af9850d98fc21e5bc24ea9e35dd80a29faf6462c608728a110c0a30b595e58b7", + "sha256:bbc6989fad0c030bd70a0b6f626f98a862224bc2b1e36bfc531ea2facc0a340c", + "sha256:be51dd2c8596b25fe43c0a4a59c2bee4f18d88efb8031188f9e7ddc6b469cf44", + "sha256:c365ad9c394f9eeffcb30a82f4246c0006417f03a7c0f8315d6211f25f7cb654", + "sha256:c3d5731a120752248844676bf92f25a12f6e45425e63ce22e0849297a093b5b0", + "sha256:ca832e124eda231a60a041da4f013e3ff24949d94a01154b137fc2f2a43c3ffb", + "sha256:d207d5b87f6cbefbdb1198154292faee8017d7495a54ae58db06762004500d00", + "sha256:d31ee5b14a82c9afe2bd26aaa405293d4237d0591527d9129ce36e58f19f95c1", + "sha256:d3b5c4cbd0c9cb61bbbb19ce335e1f8ab87a811f6d589ed52b0254cf585d709c", + "sha256:d573082c6ef99336f2cb5b667b781d2f776d4af311574fb53d908517ba523c22", + "sha256:e49db944fad339b2ccb80128ffd3f8af076f9f287197a480bf1e4ca053a866f0" + ], + "markers": "python_version >= '3.7'", + "version": "==1.10.15" + }, + "pydocstyle": { + "hashes": [ + "sha256:118762d452a49d6b05e194ef344a55822987a462831ade91ec5c06fd2169d019", + "sha256:7ce43f0c0ac87b07494eb9c0b462c0b73e6ff276807f204d6b53edc72b7e44e1" + ], + "markers": "python_version >= '3.6'", + "version": "==6.3.0" + }, + "pyflakes": { + "hashes": [ + "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f", + "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a" + ], + "markers": "python_version >= '3.8'", + "version": "==3.2.0" + }, + "pyrsistent": { + "hashes": [ + "sha256:0724c506cd8b63c69c7f883cc233aac948c1ea946ea95996ad8b1380c25e1d3f", + "sha256:09848306523a3aba463c4b49493a760e7a6ca52e4826aa100ee99d8d39b7ad1e", + "sha256:0f3b1bcaa1f0629c978b355a7c37acd58907390149b7311b5db1b37648eb6958", + "sha256:21cc459636983764e692b9eba7144cdd54fdec23ccdb1e8ba392a63666c60c34", + "sha256:2e14c95c16211d166f59c6611533d0dacce2e25de0f76e4c140fde250997b3ca", + "sha256:2e2c116cc804d9b09ce9814d17df5edf1df0c624aba3b43bc1ad90411487036d", + "sha256:4021a7f963d88ccd15b523787d18ed5e5269ce57aa4037146a2377ff607ae87d", + "sha256:4c48f78f62ab596c679086084d0dd13254ae4f3d6c72a83ffdf5ebdef8f265a4", + "sha256:4f5c2d012671b7391803263419e31b5c7c21e7c95c8760d7fc35602353dee714", + "sha256:58b8f6366e152092194ae68fefe18b9f0b4f89227dfd86a07770c3d86097aebf", + "sha256:59a89bccd615551391f3237e00006a26bcf98a4d18623a19909a2c48b8e986ee", + "sha256:5cdd7ef1ea7a491ae70d826b6cc64868de09a1d5ff9ef8d574250d0940e275b8", + "sha256:6288b3fa6622ad8a91e6eb759cfc48ff3089e7c17fb1d4c59a919769314af224", + "sha256:6d270ec9dd33cdb13f4d62c95c1a5a50e6b7cdd86302b494217137f760495b9d", + "sha256:79ed12ba79935adaac1664fd7e0e585a22caa539dfc9b7c7c6d5ebf91fb89054", + "sha256:7d29c23bdf6e5438c755b941cef867ec2a4a172ceb9f50553b6ed70d50dfd656", + "sha256:8441cf9616d642c475684d6cf2520dd24812e996ba9af15e606df5f6fd9d04a7", + "sha256:881bbea27bbd32d37eb24dd320a5e745a2a5b092a17f6debc1349252fac85423", + "sha256:8c3aba3e01235221e5b229a6c05f585f344734bd1ad42a8ac51493d74722bbce", + "sha256:a14798c3005ec892bbada26485c2eea3b54109cb2533713e355c806891f63c5e", + "sha256:b14decb628fac50db5e02ee5a35a9c0772d20277824cfe845c8a8b717c15daa3", + "sha256:b318ca24db0f0518630e8b6f3831e9cba78f099ed5c1d65ffe3e023003043ba0", + "sha256:c1beb78af5423b879edaf23c5591ff292cf7c33979734c99aa66d5914ead880f", + "sha256:c55acc4733aad6560a7f5f818466631f07efc001fd023f34a6c203f8b6df0f0b", + "sha256:ca52d1ceae015859d16aded12584c59eb3825f7b50c6cfd621d4231a6cc624ce", + "sha256:cae40a9e3ce178415040a0383f00e8d68b569e97f31928a3a8ad37e3fde6df6a", + "sha256:e78d0c7c1e99a4a45c99143900ea0546025e41bb59ebc10182e947cf1ece9174", + "sha256:ef3992833fbd686ee783590639f4b8343a57f1f75de8633749d984dc0eb16c86", + "sha256:f058a615031eea4ef94ead6456f5ec2026c19fb5bd6bfe86e9665c4158cf802f", + "sha256:f5ac696f02b3fc01a710427585c855f65cd9c640e14f52abe52020722bb4906b", + "sha256:f920385a11207dc372a028b3f1e1038bb244b3ec38d448e6d8e43c6b3ba20e98", + "sha256:fed2c3216a605dc9a6ea50c7e84c82906e3684c4e80d2908208f662a6cbf9022" + ], + "markers": "python_version >= '3.8'", + "version": "==0.20.0" + }, + "pytest": { + "hashes": [ + "sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac", + "sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==7.4.3" + }, + "snowballstemmer": { + "hashes": [ + "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1", + "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a" + ], + "version": "==2.2.0" + }, + "sortedcontainers": { + "hashes": [ + "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", + "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0" + ], + "version": "==2.4.0" + }, + "test-data-generation": { + "editable": true, + "file": "." + }, + "tomli": { + "hashes": [ + "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", + "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" + ], + "markers": "python_version < '3.11'", + "version": "==2.0.1" + }, + "typing-extensions": { + "hashes": [ + "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0", + "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a" + ], + "markers": "python_version < '3.11'", + "version": "==4.11.0" + } + }, + "develop": {} +} diff --git a/test-data-generation/mypy.ini b/test-data-generation/mypy.ini new file mode 100644 index 00000000000..b94476cbcaa --- /dev/null +++ b/test-data-generation/mypy.ini @@ -0,0 +1,5 @@ +[mypy] +show_error_codes = True +warn_unused_configs = True +strict = True +exclude = setup.py \ No newline at end of file diff --git a/test-data-generation/pytest.ini b/test-data-generation/pytest.ini new file mode 100644 index 00000000000..49f04412746 --- /dev/null +++ b/test-data-generation/pytest.ini @@ -0,0 +1,3 @@ +[pytest] +addopts = --color=yes --strict-markers +asyncio_mode = auto diff --git a/test-data-generation/setup.py b/test-data-generation/setup.py new file mode 100755 index 00000000000..4246340dd68 --- /dev/null +++ b/test-data-generation/setup.py @@ -0,0 +1,91 @@ +# Inspired by: +# https://hynek.me/articles/sharing-your-labor-of-love-pypi-quick-and-dirty/ +import sys +import codecs +import os +import os.path +from setuptools import setup, find_packages + +# make stdout blocking since Travis sets it to nonblocking +if os.name == "posix": + import fcntl + + flags = fcntl.fcntl(sys.stdout, fcntl.F_GETFL) + fcntl.fcntl(sys.stdout, fcntl.F_SETFL, flags & ~os.O_NONBLOCK) + +HERE = os.path.abspath(os.path.dirname(__file__)) +sys.path.append(os.path.join(HERE, "..", "scripts")) + +from python_build_utils import normalize_version # noqa: E402 + + +def get_version(): + buildno = os.getenv("BUILD_NUMBER") + project = os.getenv("OPENTRONS_PROJECT", "robot-stack") + git_dir = os.getenv("OPENTRONS_GIT_DIR", None) + if buildno: + normalize_opts = {"extra_tag": buildno} + else: + normalize_opts = {} + return normalize_version( + "test-data-generation", project, git_dir=git_dir, **normalize_opts + ) + + +VERSION = get_version() + +DISTNAME = "test_data_generation" +LICENSE = "Apache 2.0" +AUTHOR = "Opentrons" +EMAIL = "engineering@opentrons.com" +URL = "https://github.com/Opentrons/opentrons" +DOWNLOAD_URL = "" +CLASSIFIERS = [ + "Development Status :: 5 - Production/Stable", + "Environment :: Console", + "Operating System :: OS Independent", + "Intended Audience :: Science/Research", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Topic :: Scientific/Engineering", +] +KEYWORDS = ["robots", "protocols", "synbio", "pcr", "automation", "lab"] +DESCRIPTION = "Library for working with test data on the Opentrons robots" +PACKAGES = find_packages(where="src", exclude=["tests.*", "tests"]) +INSTALL_REQUIRES = [ + f"opentrons-shared-data=={VERSION}", +] + + +def read(*parts): + """ + Build an absolute path from *parts* and and return the contents of the + resulting file. Assume UTF-8 encoding. + """ + with codecs.open(os.path.join(HERE, *parts), "rb", "utf-8") as f: + return f.read() + + +if __name__ == "__main__": + setup( + python_requires="~=3.10", + name=DISTNAME, + description=DESCRIPTION, + license=LICENSE, + url=URL, + version=VERSION, + author=AUTHOR, + author_email=EMAIL, + maintainer=AUTHOR, + maintainer_email=EMAIL, + keywords=KEYWORDS, + long_description=__doc__, + packages=PACKAGES, + zip_safe=False, + classifiers=CLASSIFIERS, + install_requires=INSTALL_REQUIRES, + include_package_data=True, + package_dir={"": "src"}, + package_data={"test-data-generation": ["py.typed"]}, + ) From 165956d013d498d1dfa953656a0c5aad88e759cd Mon Sep 17 00:00:00 2001 From: Sanniti Pimpley Date: Fri, 19 Apr 2024 15:52:19 -0400 Subject: [PATCH 172/194] fix(api): add case correction to module_context.load_labware (#14964) Closes RESC-235 and RQA-2610 # Overview Fixes the `module_context.load_labware()` method by ensuring that labware names are converted into lower case before using them in any part of the code. The escalations issue above was caused by a cascade of things happening because the protocol had a module labware loaded with the labware name written in mixed case. There are a few places where we do string comparison of the labware names, including when trying to find new versions of a labware and when looking up LPC offsets for a labware. These string comparisons would fail because all labware definitions have lowercase names, and hence would lead to unexpected behavior. It should no longer create such a problem # Risk assessment Very low. Tiny bug fix --- api/src/opentrons/protocol_api/module_contexts.py | 8 ++++---- api/tests/opentrons/protocol_api/test_module_context.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/api/src/opentrons/protocol_api/module_contexts.py b/api/src/opentrons/protocol_api/module_contexts.py index 5e9d412835e..654a6ec46c1 100644 --- a/api/src/opentrons/protocol_api/module_contexts.py +++ b/api/src/opentrons/protocol_api/module_contexts.py @@ -151,7 +151,7 @@ def load_labware( load_location = loaded_adapter._core else: load_location = self._core - + name = validation.ensure_lowercase_name(name) labware_core = self._protocol_core.load_labware( load_name=name, label=label, @@ -467,9 +467,9 @@ def engage( if height is not None: if self._api_version >= _MAGNETIC_MODULE_HEIGHT_PARAM_REMOVED_IN: raise APIVersionError( - "The height parameter of MagneticModuleContext.engage() was removed" - " in {_MAGNETIC_MODULE_HEIGHT_PARAM_REMOVED_IN}." - " Use offset or height_from_base instead." + f"The height parameter of MagneticModuleContext.engage() was removed" + f" in {_MAGNETIC_MODULE_HEIGHT_PARAM_REMOVED_IN}." + f" Use offset or height_from_base instead." ) self._core.engage(height_from_home=height) diff --git a/api/tests/opentrons/protocol_api/test_module_context.py b/api/tests/opentrons/protocol_api/test_module_context.py index 6ce8928abc4..c57f1ff52dc 100644 --- a/api/tests/opentrons/protocol_api/test_module_context.py +++ b/api/tests/opentrons/protocol_api/test_module_context.py @@ -108,7 +108,7 @@ def test_load_labware( decoy.when(mock_labware_core.get_well_columns()).then_return([]) result = subject.load_labware( - name="infinite tip rack", + name="Infinite Tip Rack", label="it doesn't run out", namespace="ideal", version=101, From 229573fd6b381d5eddbf50763ac638e35d073564 Mon Sep 17 00:00:00 2001 From: Brayan Almonte Date: Fri, 19 Apr 2024 16:25:58 -0400 Subject: [PATCH 173/194] fix(api): engage axis to enable the motor before attempting to move the axis. (#14955) There seems to be some issue on the firmware where the motor is enabled but does not seem to get enabled, causing the axis we are attempting to move to throw a `finished movement with a condition not met` error. The firmware should be enabling the motor, and if we query the motor enable status with get_status_request = 0x01 we get the correct response where the motor is enabled. For whatever reason sending an explicit enable_motor_request = 0x06 before moving the axis fixes the problem. --------- Co-authored-by: ahiuchingau <20424172+ahiuchingau@users.noreply.github.com> --- api/src/opentrons/hardware_control/ot3api.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/api/src/opentrons/hardware_control/ot3api.py b/api/src/opentrons/hardware_control/ot3api.py index 5edc327ced1..692d1f120e2 100644 --- a/api/src/opentrons/hardware_control/ot3api.py +++ b/api/src/opentrons/hardware_control/ot3api.py @@ -1539,6 +1539,12 @@ async def _home_axis(self, axis: Axis) -> None: await self._set_plunger_current_and_home(axis, motor_ok, encoder_ok) return + # TODO: (ba, 2024-04-19): We need to explictly engage the axis and enable + # the motor when we are attempting to move. This should be already + # happening but something on the firmware is either not enabling the motor or + # disabling the motor. + await self.engage_axes([axis]) + # we can move to safe home distance! if encoder_ok and motor_ok: origin, target_pos = await self._retrieve_home_position(axis) @@ -1657,6 +1663,12 @@ async def retract_axis(self, axis: Axis) -> None: async with self._motion_lock: if motor_ok and encoder_ok: + # TODO: (ba, 2024-04-19): We need to explictly engage the axis and enable + # the motor when we are attempting to move. This should be already + # happening but something on the firmware is either not enabling the motor or + # disabling the motor. + await self.engage_axes([axis]) + # we can move to the home position without checking the limit switch origin = await self._backend.update_position() target_pos = {axis: self._backend.home_position()[axis]} @@ -1664,6 +1676,7 @@ async def retract_axis(self, axis: Axis) -> None: else: # home the axis await self._home_axis(axis) + await self._cache_current_position() await self._cache_encoder_position() From 4cc69ebef5f0312c8e59835537a3d7b3963dd6bc Mon Sep 17 00:00:00 2001 From: Brian Arthur Cooper Date: Fri, 19 Apr 2024 17:13:01 -0400 Subject: [PATCH 174/194] fix(app): configure modules during calibration, shorten heater shaker fixture name (#14953) # Overview At the beginning of the module calibration flow, the user is asked to locate the module on the deck. This integrated the deck configurator component directly into this location selction step of the module calibration wizard. the selected location will now be saved directly to deck configuration. Closes [RQA-2603](https://opentrons.atlassian.net/browse/RQA-2603) # Review requests - Run module calibration and confirm that the selected location reflects the deck configuration # Risk assessment low [RQA-2603]: https://opentrons.atlassian.net/browse/RQA-2603?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ --- .../DeviceDetailsDeckConfiguration/index.tsx | 4 +- .../ModuleWizardFlows/BeforeBeginning.tsx | 35 +----- .../ModuleWizardFlows/PlaceAdapter.tsx | 30 ++++- .../ModuleWizardFlows/SelectLocation.tsx | 112 ++++++++++++------ app/src/organisms/ModuleWizardFlows/index.tsx | 15 +-- .../QuickTransferFlow/CreateNewTransfer.tsx | 2 +- .../src/hardware-sim/BaseDeck/BaseDeck.tsx | 11 +- .../DeckConfigurator/HeaterShakerFixture.tsx | 2 +- .../hardware-sim/DeckConfigurator/index.tsx | 103 ++++++++-------- shared-data/js/fixtures.ts | 20 +++- 10 files changed, 191 insertions(+), 143 deletions(-) diff --git a/app/src/organisms/DeviceDetailsDeckConfiguration/index.tsx b/app/src/organisms/DeviceDetailsDeckConfiguration/index.tsx index 97194aa90d7..0103fe25051 100644 --- a/app/src/organisms/DeviceDetailsDeckConfiguration/index.tsx +++ b/app/src/organisms/DeviceDetailsDeckConfiguration/index.tsx @@ -269,10 +269,12 @@ export function DeviceDetailsDeckConfiguration({ flexDirection={DIRECTION_COLUMN} > cutoutId) } deckConfig={deckConfig} handleClickAdd={handleClickAdd} diff --git a/app/src/organisms/ModuleWizardFlows/BeforeBeginning.tsx b/app/src/organisms/ModuleWizardFlows/BeforeBeginning.tsx index bd899457b21..a4a18a2f3f3 100644 --- a/app/src/organisms/ModuleWizardFlows/BeforeBeginning.tsx +++ b/app/src/organisms/ModuleWizardFlows/BeforeBeginning.tsx @@ -1,5 +1,4 @@ import * as React from 'react' -import { UseMutateFunction } from 'react-query' import { Trans, useTranslation } from 'react-i18next' import { @@ -12,12 +11,7 @@ import { StyledText } from '@opentrons/components' import { GenericWizardTile } from '../../molecules/GenericWizardTile' import { WizardRequiredEquipmentList } from '../../molecules/WizardRequiredEquipmentList' -import type { - CreateMaintenanceRunData, - MaintenanceRun, - AttachedModule, -} from '@opentrons/api-client' -import type { AxiosError } from 'axios' +import type { AttachedModule } from '@opentrons/api-client' import type { ModuleCalibrationWizardStepProps } from './types' interface EqipmentItem { @@ -26,34 +20,14 @@ interface EqipmentItem { subtitle?: string } -interface BeforeBeginningProps extends ModuleCalibrationWizardStepProps { - createMaintenanceRun: UseMutateFunction< - MaintenanceRun, - AxiosError, - CreateMaintenanceRunData, - unknown - > - isCreateLoading: boolean - createdMaintenanceRunId: string | null -} +type BeforeBeginningProps = ModuleCalibrationWizardStepProps export const BeforeBeginning = ( props: BeforeBeginningProps ): JSX.Element | null => { - const { - proceed, - createMaintenanceRun, - isCreateLoading, - attachedModule, - maintenanceRunId, - createdMaintenanceRunId, - } = props + const { proceed, attachedModule } = props const { t } = useTranslation(['module_wizard_flows', 'shared']) - React.useEffect(() => { - if (createdMaintenanceRunId == null) { - createMaintenanceRun({}) - } - }, []) + const moduleDisplayName = getModuleDisplayName(attachedModule.moduleModel) let adapterLoadname: string @@ -109,7 +83,6 @@ export const BeforeBeginning = ( /> } proceedButtonText={t('start_setup')} - proceedIsDisabled={isCreateLoading || maintenanceRunId == null} proceed={proceed} /> ) diff --git a/app/src/organisms/ModuleWizardFlows/PlaceAdapter.tsx b/app/src/organisms/ModuleWizardFlows/PlaceAdapter.tsx index 1c711eec8d1..b5d5e5cf80d 100644 --- a/app/src/organisms/ModuleWizardFlows/PlaceAdapter.tsx +++ b/app/src/organisms/ModuleWizardFlows/PlaceAdapter.tsx @@ -24,6 +24,7 @@ import { TEMPERATURE_MODULE_MODELS, THERMOCYCLER_MODULE_MODELS, FLEX_SINGLE_SLOT_BY_CUTOUT_ID, + THERMOCYCLER_V2_FRONT_FIXTURE, } from '@opentrons/shared-data' import { InProgressModal } from '../../molecules/InProgressModal/InProgressModal' @@ -32,10 +33,24 @@ import { LEFT_SLOTS } from './constants' import type { DeckConfiguration, CreateCommand } from '@opentrons/shared-data' import type { ModuleCalibrationWizardStepProps } from './types' +import type { AxiosError } from 'axios' +import type { UseMutateFunction } from 'react-query' +import type { + CreateMaintenanceRunData, + MaintenanceRun, +} from '@opentrons/api-client' interface PlaceAdapterProps extends ModuleCalibrationWizardStepProps { deckConfig: DeckConfiguration setCreatedAdapterId: (adapterId: string) => void + createMaintenanceRun: UseMutateFunction< + MaintenanceRun, + AxiosError, + CreateMaintenanceRunData, + unknown + > + isCreateLoading: boolean + createdMaintenanceRunId: string | null } export const BODY_STYLE = css` @@ -58,11 +73,23 @@ export const PlaceAdapter = (props: PlaceAdapterProps): JSX.Element | null => { setCreatedAdapterId, attachedPipette, isRobotMoving, + maintenanceRunId, + createMaintenanceRun, + isCreateLoading, + createdMaintenanceRunId, } = props const { t } = useTranslation('module_wizard_flows') + React.useEffect(() => { + if (createdMaintenanceRunId == null) { + createMaintenanceRun({}) + } + }, []) const mount = attachedPipette.mount const cutoutId = deckConfig.find( - cc => cc.opentronsModuleSerialNumber === attachedModule.serialNumber + cc => + cc.opentronsModuleSerialNumber === attachedModule.serialNumber && + (attachedModule.moduleType !== THERMOCYCLER_MODULE_TYPE || + cc.cutoutFixtureId === THERMOCYCLER_V2_FRONT_FIXTURE) )?.cutoutId const slotName = cutoutId != null ? FLEX_SINGLE_SLOT_BY_CUTOUT_ID[cutoutId] : null @@ -204,6 +231,7 @@ export const PlaceAdapter = (props: PlaceAdapterProps): JSX.Element | null => { bodyText={bodyText} proceedButtonText={t('confirm_placement')} proceed={handleOnClick} + proceedIsDisabled={isCreateLoading || maintenanceRunId == null} back={goBack} /> ) diff --git a/app/src/organisms/ModuleWizardFlows/SelectLocation.tsx b/app/src/organisms/ModuleWizardFlows/SelectLocation.tsx index 38a44b96219..af0301549d0 100644 --- a/app/src/organisms/ModuleWizardFlows/SelectLocation.tsx +++ b/app/src/organisms/ModuleWizardFlows/SelectLocation.tsx @@ -1,22 +1,23 @@ import * as React from 'react' +import isEqual from 'lodash/isEqual' import { useTranslation } from 'react-i18next' import { css } from 'styled-components' import { useUpdateDeckConfigurationMutation } from '@opentrons/react-api-client' import { getModuleDisplayName, - THERMOCYCLER_MODULE_TYPE, getDeckDefFromRobotType, FLEX_ROBOT_TYPE, - getCutoutIdsFromModuleSlotName, getCutoutFixturesForModuleModel, SINGLE_CENTER_SLOT_FIXTURE, SINGLE_CENTER_CUTOUTS, SINGLE_LEFT_SLOT_FIXTURE, SINGLE_RIGHT_CUTOUTS, SINGLE_RIGHT_SLOT_FIXTURE, + getFixtureIdByCutoutIdFromModuleAnchorCutoutId, + SINGLE_SLOT_FIXTURES, } from '@opentrons/shared-data' import { - DeckLocationSelect, + DeckConfigurator, RESPONSIVENESS, SIZE_1, SPACING, @@ -31,7 +32,6 @@ import type { DeckConfiguration, CutoutFixtureId, CutoutId, - ModuleLocation, } from '@opentrons/shared-data' export const BODY_STYLE = css` @@ -46,7 +46,7 @@ interface SelectLocationProps extends ModuleCalibrationWizardStepProps { availableSlotNames: string[] occupiedCutouts: CutoutConfig[] deckConfig: DeckConfiguration - fixtureIdByCutoutId: { [cutoutId in CutoutId]?: CutoutFixtureId } + configuredFixtureIdByCutoutId: { [cutoutId in CutoutId]?: CutoutFixtureId } } export const SelectLocation = ( props: SelectLocationProps @@ -55,9 +55,7 @@ export const SelectLocation = ( proceed, attachedModule, deckConfig, - availableSlotNames, - occupiedCutouts, - fixtureIdByCutoutId, + configuredFixtureIdByCutoutId, } = props const { t } = useTranslation('module_wizard_flows') const moduleName = getModuleDisplayName(attachedModule.moduleModel) @@ -80,24 +78,41 @@ export const SelectLocation = ( ) - const handleSetLocation = (loc: ModuleLocation): void => { - const moduleFixtures = getCutoutFixturesForModuleModel( - attachedModule.moduleModel, - deckDef - ) - const selectedCutoutIds = getCutoutIdsFromModuleSlotName( - loc.slotName, - moduleFixtures, - deckDef + const moduleFixtures = getCutoutFixturesForModuleModel( + attachedModule.moduleModel, + deckDef + ) + const mayMountToCutoutIds = moduleFixtures.reduce( + (acc, { mayMountTo }) => [...acc, ...mayMountTo], + [] + ) + const editableCutoutIds = deckConfig.reduce( + (acc, { cutoutId, cutoutFixtureId, opentronsModuleSerialNumber }) => { + const isCurrentConfiguration = + Object.values(configuredFixtureIdByCutoutId).includes( + cutoutFixtureId + ) && attachedModule.serialNumber === opentronsModuleSerialNumber + if ( + mayMountToCutoutIds.includes(cutoutId) && + (isCurrentConfiguration || + SINGLE_SLOT_FIXTURES.includes(cutoutFixtureId)) + ) { + return [...acc, cutoutId] + } + return acc + }, + [] + ) + + const handleAddFixture = (anchorCutoutId: CutoutId): void => { + const selectedFixtureIdByCutoutIds = getFixtureIdByCutoutIdFromModuleAnchorCutoutId( + anchorCutoutId, + moduleFixtures ) - if ( - selectedCutoutIds.every( - selectedCutoutId => !(selectedCutoutId in fixtureIdByCutoutId) - ) - ) { + if (!isEqual(selectedFixtureIdByCutoutIds, configuredFixtureIdByCutoutId)) { updateDeckConfiguration( deckConfig.map(cc => { - if (cc.cutoutId in fixtureIdByCutoutId) { + if (cc.cutoutId in configuredFixtureIdByCutoutId) { let replacementFixtureId: CutoutFixtureId = SINGLE_LEFT_SLOT_FIXTURE if (SINGLE_CENTER_CUTOUTS.includes(cc.cutoutId)) { replacementFixtureId = SINGLE_CENTER_SLOT_FIXTURE @@ -109,13 +124,11 @@ export const SelectLocation = ( cutoutFixtureId: replacementFixtureId, opentronsModuleSerialNumber: undefined, } - } else if (selectedCutoutIds.includes(cc.cutoutId)) { + } else if (cc.cutoutId in selectedFixtureIdByCutoutIds) { return { ...cc, cutoutFixtureId: - Object.values(fixtureIdByCutoutId)[0] ?? - moduleFixtures[0]?.id ?? - cc.cutoutFixtureId, + selectedFixtureIdByCutoutIds[cc.cutoutId] ?? cc.cutoutFixtureId, opentronsModuleSerialNumber: attachedModule.serialNumber, } } else { @@ -125,22 +138,43 @@ export const SelectLocation = ( ) } } + + const handleRemoveFixture = (anchorCutoutId: CutoutId): void => { + const removedFixtureIdByCutoutIds = getFixtureIdByCutoutIdFromModuleAnchorCutoutId( + anchorCutoutId, + moduleFixtures + ) + updateDeckConfiguration( + deckConfig.map(cc => { + if (cc.cutoutId in removedFixtureIdByCutoutIds) { + let replacementFixtureId: CutoutFixtureId = SINGLE_LEFT_SLOT_FIXTURE + if (SINGLE_CENTER_CUTOUTS.includes(cc.cutoutId)) { + replacementFixtureId = SINGLE_CENTER_SLOT_FIXTURE + } else if (SINGLE_RIGHT_CUTOUTS.includes(cc.cutoutId)) { + replacementFixtureId = SINGLE_RIGHT_SLOT_FIXTURE + } + return { + ...cc, + cutoutFixtureId: replacementFixtureId, + opentronsModuleSerialNumber: undefined, + } + } else { + return cc + } + }) + ) + } + return ( } bodyText={bodyText} diff --git a/app/src/organisms/ModuleWizardFlows/index.tsx b/app/src/organisms/ModuleWizardFlows/index.tsx index f3196b54fbc..3e0977a4f23 100644 --- a/app/src/organisms/ModuleWizardFlows/index.tsx +++ b/app/src/organisms/ModuleWizardFlows/index.tsx @@ -300,15 +300,7 @@ export const ModuleWizardFlows = ( } else if (isExiting) { modalContent = } else if (currentStep.section === SECTIONS.BEFORE_BEGINNING) { - modalContent = ( - - ) + modalContent = } else if (currentStep.section === SECTIONS.SELECT_LOCATION) { modalContent = ( ) } else if (currentStep.section === SECTIONS.PLACE_ADAPTER) { @@ -327,6 +319,9 @@ export const ModuleWizardFlows = ( {...calibrateBaseProps} deckConfig={deckConfig} setCreatedAdapterId={setCreatedAdapterId} + createMaintenanceRun={createTargetedMaintenanceRun} + isCreateLoading={isCreateLoading} + createdMaintenanceRunId={createdMaintenanceRunId} /> ) } else if (currentStep.section === SECTIONS.ATTACH_PROBE) { diff --git a/app/src/organisms/QuickTransferFlow/CreateNewTransfer.tsx b/app/src/organisms/QuickTransferFlow/CreateNewTransfer.tsx index f1b795e5fc3..57d6ce14b54 100644 --- a/app/src/organisms/QuickTransferFlow/CreateNewTransfer.tsx +++ b/app/src/organisms/QuickTransferFlow/CreateNewTransfer.tsx @@ -57,7 +57,7 @@ export function CreateNewTransfer(props: CreateNewTransferProps): JSX.Element { {}} handleClickRemove={() => {}} additionalStaticFixtures={[ diff --git a/components/src/hardware-sim/BaseDeck/BaseDeck.tsx b/components/src/hardware-sim/BaseDeck/BaseDeck.tsx index e664cb10277..d896c0c9370 100644 --- a/components/src/hardware-sim/BaseDeck/BaseDeck.tsx +++ b/components/src/hardware-sim/BaseDeck/BaseDeck.tsx @@ -15,6 +15,7 @@ import { WASTE_CHUTE_ONLY_FIXTURES, WASTE_CHUTE_STAGING_AREA_FIXTURES, HEATERSHAKER_MODULE_V1, + MODULE_FIXTURES_BY_MODEL, } from '@opentrons/shared-data' import { RobotCoordinateSpace } from '../RobotCoordinateSpace' @@ -32,6 +33,7 @@ import { WasteChuteFixture } from './WasteChuteFixture' import { WasteChuteStagingAreaFixture } from './WasteChuteStagingAreaFixture' import type { + CutoutFixtureId, DeckConfiguration, LabwareDefinition2, LabwareLocation, @@ -101,7 +103,14 @@ export function BaseDeck(props: BaseDeckProps): JSX.Element { const singleSlotFixtures = deckConfig.filter( fixture => fixture.cutoutFixtureId != null && - SINGLE_SLOT_FIXTURES.includes(fixture.cutoutFixtureId) + (SINGLE_SLOT_FIXTURES.includes(fixture.cutoutFixtureId) || + // If module fixture is loaded, still visualize singleSlotFixture underneath for consistency + Object.entries(MODULE_FIXTURES_BY_MODEL) + .reduce( + (acc, [_model, fixtures]) => [...acc, ...fixtures], + [] + ) + .includes(fixture.cutoutFixtureId)) ) const stagingAreaFixtures = deckConfig.filter( fixture => diff --git a/components/src/hardware-sim/DeckConfigurator/HeaterShakerFixture.tsx b/components/src/hardware-sim/DeckConfigurator/HeaterShakerFixture.tsx index 15c91a04f5e..129fd46993a 100644 --- a/components/src/hardware-sim/DeckConfigurator/HeaterShakerFixture.tsx +++ b/components/src/hardware-sim/DeckConfigurator/HeaterShakerFixture.tsx @@ -32,7 +32,7 @@ interface HeaterShakerFixtureProps { ) => void } -const HEATER_SHAKER_MODULE_FIXTURE_DISPLAY_NAME = 'Heater Shaker Module' +const HEATER_SHAKER_MODULE_FIXTURE_DISPLAY_NAME = 'Heater-Shaker' export function HeaterShakerFixture( props: HeaterShakerFixtureProps diff --git a/components/src/hardware-sim/DeckConfigurator/index.tsx b/components/src/hardware-sim/DeckConfigurator/index.tsx index ad69bd6c36d..8de6ba4da70 100644 --- a/components/src/hardware-sim/DeckConfigurator/index.tsx +++ b/components/src/hardware-sim/DeckConfigurator/index.tsx @@ -44,10 +44,11 @@ interface DeckConfiguratorProps { ) => void lightFill?: string darkFill?: string - readOnly?: boolean + editableCutoutIds?: CutoutId[] showExpansion?: boolean children?: React.ReactNode additionalStaticFixtures?: Array<{ location: CutoutId; label: string }> + height?: string } export function DeckConfigurator(props: DeckConfiguratorProps): JSX.Element { @@ -55,77 +56,57 @@ export function DeckConfigurator(props: DeckConfiguratorProps): JSX.Element { deckConfig, handleClickAdd, handleClickRemove, + additionalStaticFixtures, + children, lightFill = COLORS.grey35, darkFill = COLORS.black90, - readOnly = false, + editableCutoutIds = deckConfig.map(({ cutoutId }) => cutoutId), showExpansion = true, - additionalStaticFixtures, - children, + height = '455px', } = props const deckDef = getDeckDefFromRobotType(FLEX_ROBOT_TYPE) - // restrict configuration to certain locations - const configurableFixtureLocations: CutoutId[] = [ - 'cutoutA1', - 'cutoutB1', - 'cutoutC1', - 'cutoutD1', - 'cutoutA2', - 'cutoutB2', - 'cutoutC2', - 'cutoutD2', - 'cutoutA3', - 'cutoutB3', - 'cutoutC3', - 'cutoutD3', - ] - const configurableDeckConfig = deckConfig.filter(({ cutoutId }) => - configurableFixtureLocations.includes(cutoutId) - ) - - const stagingAreaFixtures = configurableDeckConfig.filter( + const stagingAreaFixtures = deckConfig.filter( ({ cutoutFixtureId }) => cutoutFixtureId === STAGING_AREA_RIGHT_SLOT_FIXTURE ) - const wasteChuteFixtures = configurableDeckConfig.filter( + const wasteChuteFixtures = deckConfig.filter( ({ cutoutFixtureId }) => cutoutFixtureId != null && WASTE_CHUTE_ONLY_FIXTURES.includes(cutoutFixtureId) ) - const wasteChuteStagingAreaFixtures = configurableDeckConfig.filter( + const wasteChuteStagingAreaFixtures = deckConfig.filter( ({ cutoutFixtureId }) => cutoutFixtureId != null && WASTE_CHUTE_STAGING_AREA_FIXTURES.includes(cutoutFixtureId) ) - const emptyFixtures = readOnly - ? [] - : configurableDeckConfig.filter( - ({ cutoutFixtureId }) => - cutoutFixtureId != null && - SINGLE_SLOT_FIXTURES.includes(cutoutFixtureId) - ) - const trashBinFixtures = configurableDeckConfig.filter( + const emptyCutouts = deckConfig.filter( + ({ cutoutFixtureId, cutoutId }) => + editableCutoutIds.includes(cutoutId) && + cutoutFixtureId != null && + SINGLE_SLOT_FIXTURES.includes(cutoutFixtureId) + ) + const trashBinFixtures = deckConfig.filter( ({ cutoutFixtureId }) => cutoutFixtureId === TRASH_BIN_ADAPTER_FIXTURE ) - const thermocyclerFixtures = configurableDeckConfig.filter( + const thermocyclerFixtures = deckConfig.filter( ({ cutoutFixtureId }) => cutoutFixtureId === THERMOCYCLER_V2_FRONT_FIXTURE ) - const heaterShakerFixtures = configurableDeckConfig.filter( + const heaterShakerFixtures = deckConfig.filter( ({ cutoutFixtureId }) => cutoutFixtureId === HEATERSHAKER_MODULE_V1_FIXTURE ) - const temperatureModuleFixtures = configurableDeckConfig.filter( + const temperatureModuleFixtures = deckConfig.filter( ({ cutoutFixtureId }) => cutoutFixtureId === TEMPERATURE_MODULE_V2_FIXTURE ) - const magneticBlockFixtures = configurableDeckConfig.filter( - ({ cutoutFixtureId }) => - ([ - MAGNETIC_BLOCK_V1_FIXTURE, - STAGING_AREA_SLOT_WITH_MAGNETIC_BLOCK_V1_FIXTURE, - ] as CutoutFixtureId[]).includes(cutoutFixtureId) + const magneticBlockFixtures = deckConfig.filter(({ cutoutFixtureId }) => + ([ + MAGNETIC_BLOCK_V1_FIXTURE, + STAGING_AREA_SLOT_WITH_MAGNETIC_BLOCK_V1_FIXTURE, + ] as CutoutFixtureId[]).includes(cutoutFixtureId) ) return ( @@ -145,12 +126,14 @@ export function DeckConfigurator(props: DeckConfiguratorProps): JSX.Element { ))} - {emptyFixtures.map(({ cutoutId }) => ( + {emptyCutouts.map(({ cutoutId }) => ( @@ -171,7 +156,9 @@ export function DeckConfigurator(props: DeckConfiguratorProps): JSX.Element { @@ -190,7 +179,9 @@ export function DeckConfigurator(props: DeckConfiguratorProps): JSX.Element { @@ -199,7 +190,9 @@ export function DeckConfigurator(props: DeckConfiguratorProps): JSX.Element { @@ -208,7 +201,9 @@ export function DeckConfigurator(props: DeckConfiguratorProps): JSX.Element { diff --git a/shared-data/js/fixtures.ts b/shared-data/js/fixtures.ts index 42a46b84a9f..057bce01503 100644 --- a/shared-data/js/fixtures.ts +++ b/shared-data/js/fixtures.ts @@ -173,12 +173,10 @@ export function getCutoutFixturesForModuleModel( }, []) } -export function getFixtureIdByCutoutIdFromModuleSlotName( - slotName: string, - moduleFixtures: CutoutFixture[], // cutout fixtures for a specific module model - deckDef: DeckDefinition +export function getFixtureIdByCutoutIdFromModuleAnchorCutoutId( + anchorCutoutId: CutoutId | null, + moduleFixtures: CutoutFixture[] // cutout fixtures for a specific module model ): { [cutoutId in CutoutId]?: CutoutFixtureId } { - const anchorCutoutId = getCutoutIdForSlotName(slotName, deckDef) // find the first fixture for this specific module model that may mount to the cutout implied by the slotName const anchorFixture = moduleFixtures.find(fixture => fixture.mayMountTo.some(cutoutId => cutoutId === anchorCutoutId) @@ -190,6 +188,18 @@ export function getFixtureIdByCutoutIdFromModuleSlotName( return {} } +export function getFixtureIdByCutoutIdFromModuleSlotName( + slotName: string, + moduleFixtures: CutoutFixture[], // cutout fixtures for a specific module model + deckDef: DeckDefinition +): { [cutoutId in CutoutId]?: CutoutFixtureId } { + const anchorCutoutId = getCutoutIdForSlotName(slotName, deckDef) + return getFixtureIdByCutoutIdFromModuleAnchorCutoutId( + anchorCutoutId, + moduleFixtures + ) +} + export function getCutoutIdsFromModuleSlotName( slotName: string, moduleFixtures: CutoutFixture[], // cutout fixtures for a specific module model From de2b1eb803b948feaa77f160b67a18830a1ff47b Mon Sep 17 00:00:00 2001 From: koji Date: Mon, 22 Apr 2024 09:44:14 -0400 Subject: [PATCH 175/194] feat(opentrons-ai-client): introduce react-markdown to chat display component (#14965) * feat(opentrons-ai-client): introduce react-markdown to chat display component --- opentrons-ai-client/package.json | 1 + .../molecules/ChatDisplay/ChatDisplay.stories.tsx | 12 ++++++++---- .../ChatDisplay/__tests__/ChatDisplay.test.tsx | 4 ++-- .../src/molecules/ChatDisplay/index.tsx | 11 +++++++---- 4 files changed, 18 insertions(+), 10 deletions(-) diff --git a/opentrons-ai-client/package.json b/opentrons-ai-client/package.json index f3dd1d2f2a1..39d4f6d275c 100644 --- a/opentrons-ai-client/package.json +++ b/opentrons-ai-client/package.json @@ -26,6 +26,7 @@ "react-dom": "18.2.0", "react-error-boundary": "^4.0.10", "react-i18next": "13.5.0", + "react-markdown": "9.0.1", "styled-components": "5.3.6" }, "engines": { diff --git a/opentrons-ai-client/src/molecules/ChatDisplay/ChatDisplay.stories.tsx b/opentrons-ai-client/src/molecules/ChatDisplay/ChatDisplay.stories.tsx index cd4d08a1701..ae03a25f754 100644 --- a/opentrons-ai-client/src/molecules/ChatDisplay/ChatDisplay.stories.tsx +++ b/opentrons-ai-client/src/molecules/ChatDisplay/ChatDisplay.stories.tsx @@ -21,12 +21,14 @@ const meta: Meta = { } export default meta type Story = StoryObj + export const OpentronsAI: Story = { args: { - text: ` - \`\`\`python -from opentrons import protocol_api + content: ` +## sample output from OpentronsAI +\`\`\`py +from opentrons import protocol_api # Metadata metadata = { 'protocolName': 'ThermoPrime Taq DNA Polymerase PCR Amplification', @@ -46,13 +48,15 @@ def run(protocol: protocol_api.ProtocolContext): TC_SAMPLE_MASTERMIX_MIX_VOLUME = SAMPLE_VOL + MASTERMIX_VOL MASTERMIX_BLOCK_TEMP = 10 # degree C TEMP_DECK_WAIT_TIME = 50 # seconds +\`\`\` `, isUserInput: false, }, } + export const User: Story = { args: { - text: ` + content: ` - Application: Reagent transfer - Robot: OT-2 - API: 2.13 diff --git a/opentrons-ai-client/src/molecules/ChatDisplay/__tests__/ChatDisplay.test.tsx b/opentrons-ai-client/src/molecules/ChatDisplay/__tests__/ChatDisplay.test.tsx index ad9bf527a0b..75b99717abb 100644 --- a/opentrons-ai-client/src/molecules/ChatDisplay/__tests__/ChatDisplay.test.tsx +++ b/opentrons-ai-client/src/molecules/ChatDisplay/__tests__/ChatDisplay.test.tsx @@ -15,7 +15,7 @@ describe('ChatDisplay', () => { beforeEach(() => { props = { - text: 'mock text from the backend', + content: 'mock text from the backend', isUserInput: false, } }) @@ -29,7 +29,7 @@ describe('ChatDisplay', () => { }) it('should display input from use and label', () => { props = { - text: 'mock text from user input', + content: 'mock text from user input', isUserInput: true, } render(props) diff --git a/opentrons-ai-client/src/molecules/ChatDisplay/index.tsx b/opentrons-ai-client/src/molecules/ChatDisplay/index.tsx index f18bc9f4998..c2d52e6a593 100644 --- a/opentrons-ai-client/src/molecules/ChatDisplay/index.tsx +++ b/opentrons-ai-client/src/molecules/ChatDisplay/index.tsx @@ -1,5 +1,6 @@ import React from 'react' import { useTranslation } from 'react-i18next' +import Markdown from 'react-markdown' import { BORDERS, COLORS, @@ -10,12 +11,12 @@ import { } from '@opentrons/components' interface ChatDisplayProps { - text: string + content: string isUserInput: boolean } export function ChatDisplay({ - text, + content, isUserInput, }: ChatDisplayProps): JSX.Element { const { t } = useTranslation('protocol_generator') @@ -25,7 +26,6 @@ export function ChatDisplay({ gridGap={SPACING.spacing12} paddingLeft={isUserInput ? SPACING.spacing40 : undefined} paddingRight={isUserInput ? undefined : SPACING.spacing40} - // max-width="58.125rem" > {isUserInput ? t('you') : t('opentronsai')} {/* text should be markdown so this component will have a package or function to parse markdown */} @@ -35,8 +35,11 @@ export function ChatDisplay({ data-testid={`ChatDisplay_from_${isUserInput ? 'user' : 'backend'}`} borderRadius={BORDERS.borderRadius12} width="100%" + flexDirection={DIRECTION_COLUMN} + gridGap={SPACING.spacing16} > - {text} + {/* ToDo (kk:04/19/2024) I will get feedback for additional styling from the design team. */} + {content} ) From 0f07f975e6795bffe09c4b624dc78cac63dd3c4c Mon Sep 17 00:00:00 2001 From: koji Date: Mon, 22 Apr 2024 09:44:41 -0400 Subject: [PATCH 176/194] fix(components): fix icon stories (#14969) * fix(components): fix icon stories --- components/src/icons/Icon.stories.tsx | 33 +++++++++++++++------------ 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/components/src/icons/Icon.stories.tsx b/components/src/icons/Icon.stories.tsx index 1be5df8581c..9d7b0f1141a 100644 --- a/components/src/icons/Icon.stories.tsx +++ b/components/src/icons/Icon.stories.tsx @@ -1,33 +1,38 @@ import * as React from 'react' - -import { Box, SIZE_3 } from '@opentrons/components' +import { Flex } from '../primitives' +import { SPACING } from '../ui-style-constants' import { ICON_DATA_BY_NAME } from './icon-data' import { Icon as IconComponent } from './Icon' +import type { Meta, StoryObj } from '@storybook/react' -import type { Story, Meta } from '@storybook/react' - -export default { +const meta: Meta = { title: 'Library/Atoms/Icon', + component: IconComponent, argTypes: { name: { + options: Object.keys(ICON_DATA_BY_NAME), control: { type: 'select', - options: Object.keys(ICON_DATA_BY_NAME), }, - defaultValue: 'alert', }, }, decorators: [ Story => ( - + - + ), ], -} as Meta +} -const Template: Story> = args => { - return +export default meta + +type Story = StoryObj + +export const Icon: Story = { + args: { + name: 'alert', + spin: false, + size: '4rem', + }, } -export const Icon = Template.bind({}) -Icon.args = { spin: false } From 8776ed9ce779702b8da3cc715ba10d1ffc41ed8f Mon Sep 17 00:00:00 2001 From: Shlok Amin Date: Mon, 22 Apr 2024 09:54:51 -0400 Subject: [PATCH 177/194] ci(shared-data): install dependencies in workflow (#14958) # Overview Follow up to https://github.com/Opentrons/opentrons/pull/14935 This PR adds make a make setup call that got deleted on accident. # Risk assessment Low --- .github/workflows/shared-data-test-lint-deploy.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/shared-data-test-lint-deploy.yaml b/.github/workflows/shared-data-test-lint-deploy.yaml index 94c56f16a56..57653337132 100644 --- a/.github/workflows/shared-data-test-lint-deploy.yaml +++ b/.github/workflows/shared-data-test-lint-deploy.yaml @@ -237,7 +237,8 @@ jobs: - name: 'js deps' run: | npm config set cache ./.npm-cache - yarn config set cache-folder ./.yarn-cache + yarn config set cache-folder ./.yarn-cache + make setup-js - name: 'build typescript' run: make build-ts - name: 'build library' From b42927a74661a0264dc33d4aa0d2ca6cc63f5d87 Mon Sep 17 00:00:00 2001 From: Sarah Breen Date: Mon, 22 Apr 2024 11:31:02 -0400 Subject: [PATCH 178/194] feat(app): add tiprack selection step to quick transfer flow (#14950) fix PLAT-290 --- .../QuickTransferFlow/SelectPipette.tsx | 7 +- .../QuickTransferFlow/SelectTipRack.tsx | 80 +++++++++++++++++ .../__tests__/SelectTipRack.test.tsx | 86 +++++++++++++++++++ app/src/organisms/QuickTransferFlow/index.tsx | 20 ++++- app/src/organisms/QuickTransferFlow/types.ts | 14 +-- components/src/atoms/StepMeter/index.tsx | 14 ++- 6 files changed, 207 insertions(+), 14 deletions(-) create mode 100644 app/src/organisms/QuickTransferFlow/SelectTipRack.tsx create mode 100644 app/src/organisms/QuickTransferFlow/__tests__/SelectTipRack.test.tsx diff --git a/app/src/organisms/QuickTransferFlow/SelectPipette.tsx b/app/src/organisms/QuickTransferFlow/SelectPipette.tsx index 0f92ca0d508..6ef31157fdf 100644 --- a/app/src/organisms/QuickTransferFlow/SelectPipette.tsx +++ b/app/src/organisms/QuickTransferFlow/SelectPipette.tsx @@ -79,9 +79,12 @@ export function SelectPipette(props: SelectPipetteProps): JSX.Element { marginTop={SPACING.spacing120} flexDirection={DIRECTION_COLUMN} padding={`${SPACING.spacing16} ${SPACING.spacing60} ${SPACING.spacing40} ${SPACING.spacing60}`} - gridGap={SPACING.spacing16} + gridGap={SPACING.spacing4} > - + {t('pipette_currently_attached')} {leftPipetteSpecs != null ? ( diff --git a/app/src/organisms/QuickTransferFlow/SelectTipRack.tsx b/app/src/organisms/QuickTransferFlow/SelectTipRack.tsx new file mode 100644 index 00000000000..bed59baa54b --- /dev/null +++ b/app/src/organisms/QuickTransferFlow/SelectTipRack.tsx @@ -0,0 +1,80 @@ +import * as React from 'react' +import { useTranslation } from 'react-i18next' +import { Flex, SPACING, DIRECTION_COLUMN } from '@opentrons/components' +import { getAllDefinitions } from '@opentrons/shared-data' +import { SmallButton, LargeButton } from '../../atoms/buttons' +import { ChildNavigation } from '../ChildNavigation' + +import type { LabwareDefinition2 } from '@opentrons/shared-data' +import type { + QuickTransferSetupState, + QuickTransferWizardAction, +} from './types' + +interface SelectTipRackProps { + onNext: () => void + onBack: () => void + exitButtonProps: React.ComponentProps + state: QuickTransferSetupState + dispatch: React.Dispatch +} + +export function SelectTipRack(props: SelectTipRackProps): JSX.Element { + const { onNext, onBack, exitButtonProps, state, dispatch } = props + const { i18n, t } = useTranslation(['quick_transfer', 'shared']) + + const allLabwareDefinitionsByUri = getAllDefinitions() + const selectedPipetteDefaultTipracks = + state.pipette?.liquids.default.defaultTipracks ?? [] + + const [selectedTipRack, setSelectedTipRack] = React.useState< + LabwareDefinition2 | undefined + >(state.tipRack) + + const handleClickNext = (): void => { + // the button will be disabled if this values is null + if (selectedTipRack != null) { + dispatch({ + type: 'SELECT_TIP_RACK', + tipRack: selectedTipRack, + }) + onNext() + } + } + return ( + + + + {selectedPipetteDefaultTipracks.map(tipRack => { + const tipRackDef = allLabwareDefinitionsByUri[tipRack] + + return tipRackDef != null ? ( + { + setSelectedTipRack(tipRackDef) + }} + buttonText={tipRackDef.metadata.displayName} + /> + ) : null + })} + + + ) +} diff --git a/app/src/organisms/QuickTransferFlow/__tests__/SelectTipRack.test.tsx b/app/src/organisms/QuickTransferFlow/__tests__/SelectTipRack.test.tsx new file mode 100644 index 00000000000..b32b3188910 --- /dev/null +++ b/app/src/organisms/QuickTransferFlow/__tests__/SelectTipRack.test.tsx @@ -0,0 +1,86 @@ +import * as React from 'react' +import { fireEvent, screen } from '@testing-library/react' +import { describe, it, expect, afterEach, vi, beforeEach } from 'vitest' + +import { renderWithProviders } from '../../../__testing-utils__' +import { i18n } from '../../../i18n' +import { SelectTipRack } from '../SelectTipRack' + +vi.mock('@opentrons/react-api-client') +const render = (props: React.ComponentProps) => { + return renderWithProviders(, { + i18nInstance: i18n, + }) +} + +describe('SelectTipRack', () => { + let props: React.ComponentProps + + beforeEach(() => { + props = { + onNext: vi.fn(), + onBack: vi.fn(), + exitButtonProps: { + buttonType: 'tertiaryLowLight', + buttonText: 'Exit', + onClick: vi.fn(), + }, + state: { + mount: 'left', + pipette: { + liquids: { + default: { + defaultTipracks: [ + 'opentrons/opentrons_flex_96_tiprack_1000ul/1', + 'opentrons/opentrons_flex_96_tiprack_200ul/1', + 'opentrons/opentrons_flex_96_tiprack_50ul/1', + 'opentrons/opentrons_flex_96_filtertiprack_1000ul/1', + 'opentrons/opentrons_flex_96_filtertiprack_200ul/1', + 'opentrons/opentrons_flex_96_filtertiprack_50ul/1', + ], + }, + }, + } as any, + }, + dispatch: vi.fn(), + } + }) + afterEach(() => { + vi.resetAllMocks() + }) + + it('renders the select tip rack screen, header, and exit button', () => { + render(props) + screen.getByText('Select tip rack') + const exitBtn = screen.getByText('Exit') + fireEvent.click(exitBtn) + expect(props.exitButtonProps.onClick).toHaveBeenCalled() + }) + + it('renders continue button and it is disabled if no tip rack is selected', () => { + render(props) + screen.getByText('Continue') + const continueBtn = screen.getByTestId('ChildNavigation_Primary_Button') + expect(continueBtn).toBeDisabled() + }) + + it('selects tip rack by default if there is one in state, button will be enabled', () => { + render({ ...props, state: { tipRack: { def: 'definition' } as any } }) + const continueBtn = screen.getByTestId('ChildNavigation_Primary_Button') + expect(continueBtn).toBeEnabled() + fireEvent.click(continueBtn) + expect(props.onNext).toHaveBeenCalled() + }) + + it('enables continue button if you click a tip rack', () => { + render(props) + const continueBtn = screen.getByTestId('ChildNavigation_Primary_Button') + expect(continueBtn).toBeDisabled() + const tipRackButton = screen.getByText('Opentrons Flex 96 Tip Rack 200 µL') + fireEvent.click(tipRackButton) + expect(continueBtn).toBeEnabled() + fireEvent.click(continueBtn) + expect(props.dispatch).toHaveBeenCalled() + expect(props.onNext).toHaveBeenCalled() + }) +}) diff --git a/app/src/organisms/QuickTransferFlow/index.tsx b/app/src/organisms/QuickTransferFlow/index.tsx index 36d0175b0db..cdfecc4fbe2 100644 --- a/app/src/organisms/QuickTransferFlow/index.tsx +++ b/app/src/organisms/QuickTransferFlow/index.tsx @@ -1,11 +1,17 @@ import * as React from 'react' import { useHistory } from 'react-router-dom' import { useTranslation } from 'react-i18next' -import { Flex, StepMeter, SPACING } from '@opentrons/components' +import { + Flex, + StepMeter, + SPACING, + POSITION_STICKY, +} from '@opentrons/components' import { SmallButton } from '../../atoms/buttons' import { ChildNavigation } from '../ChildNavigation' import { CreateNewTransfer } from './CreateNewTransfer' import { SelectPipette } from './SelectPipette' +import { SelectTipRack } from './SelectTipRack' import { quickTransferReducer } from './utils' import type { QuickTransferSetupState } from './types' @@ -66,6 +72,16 @@ export const QuickTransferFlow = (): JSX.Element => { exitButtonProps={exitButtonProps} /> ) + } else if (currentStep === 3) { + modalContent = ( + setCurrentStep(prevStep => prevStep - 1)} + onNext={() => setCurrentStep(prevStep => prevStep + 1)} + exitButtonProps={exitButtonProps} + /> + ) } else { modalContent = null } @@ -76,6 +92,8 @@ export const QuickTransferFlow = (): JSX.Element => { {modalContent == null ? ( diff --git a/app/src/organisms/QuickTransferFlow/types.ts b/app/src/organisms/QuickTransferFlow/types.ts index 814dae22a71..1d43017a58c 100644 --- a/app/src/organisms/QuickTransferFlow/types.ts +++ b/app/src/organisms/QuickTransferFlow/types.ts @@ -1,14 +1,14 @@ import { ACTIONS } from './constants' import type { Mount } from '@opentrons/api-client' -import type { LabwareDefinition1, PipetteV2Specs } from '@opentrons/shared-data' +import type { LabwareDefinition2, PipetteV2Specs } from '@opentrons/shared-data' export interface QuickTransferSetupState { pipette?: PipetteV2Specs mount?: Mount - tipRack?: LabwareDefinition1 - source?: LabwareDefinition1 + tipRack?: LabwareDefinition2 + source?: LabwareDefinition2 sourceWells?: string[] - destination?: LabwareDefinition1 + destination?: LabwareDefinition2 destinationWells?: string[] volume?: number } @@ -29,11 +29,11 @@ interface SelectPipetteAction { } interface SelectTipRackAction { type: typeof ACTIONS.SELECT_TIP_RACK - tipRack: LabwareDefinition1 + tipRack: LabwareDefinition2 } interface SetSourceLabwareAction { type: typeof ACTIONS.SET_SOURCE_LABWARE - labware: LabwareDefinition1 + labware: LabwareDefinition2 } interface SetSourceWellsAction { type: typeof ACTIONS.SET_SOURCE_WELLS @@ -41,7 +41,7 @@ interface SetSourceWellsAction { } interface SetDestLabwareAction { type: typeof ACTIONS.SET_DEST_LABWARE - labware: LabwareDefinition1 + labware: LabwareDefinition2 } interface SetDestWellsAction { type: typeof ACTIONS.SET_DEST_WELLS diff --git a/components/src/atoms/StepMeter/index.tsx b/components/src/atoms/StepMeter/index.tsx index 14bbf48c6ca..91f151fb5c9 100644 --- a/components/src/atoms/StepMeter/index.tsx +++ b/components/src/atoms/StepMeter/index.tsx @@ -5,13 +5,15 @@ import { RESPONSIVENESS, SPACING } from '../../ui-style-constants' import { COLORS } from '../../helix-design-system' import { POSITION_ABSOLUTE, POSITION_RELATIVE } from '../../styles' -interface StepMeterProps { +import type { StyleProps } from '../../primitives' + +interface StepMeterProps extends StyleProps { totalSteps: number currentStep: number | null } export const StepMeter = (props: StepMeterProps): JSX.Element => { - const { totalSteps, currentStep } = props + const { totalSteps, currentStep, ...styleProps } = props const progress = currentStep != null ? currentStep : 0 const percentComplete = `${ // this logic puts a cap at 100% percentComplete which we should never run into @@ -21,7 +23,7 @@ export const StepMeter = (props: StepMeterProps): JSX.Element => { }%` const StepMeterContainer = css` - position: ${POSITION_RELATIVE}; + position: ${styleProps.position ? styleProps.position : POSITION_RELATIVE}; height: ${SPACING.spacing4}; background-color: ${COLORS.grey30}; @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { @@ -41,7 +43,11 @@ export const StepMeter = (props: StepMeterProps): JSX.Element => { ` return ( - + ) From 58a1fc014e7dc4f7c710a23777c677a9546841c6 Mon Sep 17 00:00:00 2001 From: Jethary Rader <66035149+jerader@users.noreply.github.com> Date: Mon, 22 Apr 2024 11:48:24 -0400 Subject: [PATCH 179/194] refactor(protocol-designer): tip position modal max values round down (#14972) closes AUTH-352 --- .../TipPositionField/TipPositionModal.tsx | 14 +++++++------- .../TipPositionField/ZTipPositionModal.tsx | 8 ++++---- .../__tests__/TipPositionModal.test.tsx | 8 ++++---- .../fields/TipPositionField/utils.ts | 17 +++++++++++++++-- 4 files changed, 30 insertions(+), 17 deletions(-) diff --git a/protocol-designer/src/components/StepEditForm/fields/TipPositionField/TipPositionModal.tsx b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/TipPositionModal.tsx index 2a303f92c2f..56a9148270f 100644 --- a/protocol-designer/src/components/StepEditForm/fields/TipPositionField/TipPositionModal.tsx +++ b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/TipPositionModal.tsx @@ -93,12 +93,12 @@ export const TipPositionModal = ( } => { if (getIsTouchTipField(zSpec?.name ?? '')) { return { - maxMmFromBottom: utils.roundValue(wellDepthMm), - minMmFromBottom: utils.roundValue(wellDepthMm / 2), + maxMmFromBottom: utils.roundValue(wellDepthMm, 'up'), + minMmFromBottom: utils.roundValue(wellDepthMm / 2, 'up'), } } return { - maxMmFromBottom: utils.roundValue(wellDepthMm * 2), + maxMmFromBottom: utils.roundValue(wellDepthMm * 2, 'up'), minMmFromBottom: 0, } } @@ -138,10 +138,10 @@ export const TipPositionModal = ( return utils.getErrorText({ errors, minMm: min, maxMm: max, isPristine, t }) } - const roundedXMin = utils.roundValue(xMinWidth) - const roundedYMin = utils.roundValue(yMinWidth) - const roundedXMax = utils.roundValue(xMaxWidth) - const roundedYMax = utils.roundValue(yMaxWidth) + const roundedXMin = utils.roundValue(xMinWidth, 'up') + const roundedYMin = utils.roundValue(yMinWidth, 'up') + const roundedXMax = utils.roundValue(xMaxWidth, 'down') + const roundedYMax = utils.roundValue(yMaxWidth, 'down') const zErrorText = createErrorText(zErrors, minMmFromBottom, maxMmFromBottom) const xErrorText = createErrorText(xErrors, roundedXMin, roundedXMax) diff --git a/protocol-designer/src/components/StepEditForm/fields/TipPositionField/ZTipPositionModal.tsx b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/ZTipPositionModal.tsx index b2812a35309..db2972c06e2 100644 --- a/protocol-designer/src/components/StepEditForm/fields/TipPositionField/ZTipPositionModal.tsx +++ b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/ZTipPositionModal.tsx @@ -67,12 +67,12 @@ export function ZTipPositionModal(props: ZTipPositionModalProps): JSX.Element { } => { if (getIsTouchTipField(name)) { return { - maxMmFromBottom: utils.roundValue(wellDepthMm), - minMmFromBottom: utils.roundValue(wellDepthMm / 2), + maxMmFromBottom: utils.roundValue(wellDepthMm, 'up'), + minMmFromBottom: utils.roundValue(wellDepthMm / 2, 'up'), } } return { - maxMmFromBottom: utils.roundValue(wellDepthMm * 2), + maxMmFromBottom: utils.roundValue(wellDepthMm * 2, 'up'), minMmFromBottom: 0, } } @@ -148,7 +148,7 @@ export function ZTipPositionModal(props: ZTipPositionModalProps): JSX.Element { const handleIncrementDecrement = (delta: number): void => { const prevValue = value === null ? defaultMm : Number(value) setIsDefault(false) - handleChange(utils.roundValue(prevValue + delta)) + handleChange(utils.roundValue(prevValue + delta, 'up')) } const makeHandleIncrement = (step: number): (() => void) => () => { diff --git a/protocol-designer/src/components/StepEditForm/fields/TipPositionField/__tests__/TipPositionModal.test.tsx b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/__tests__/TipPositionModal.test.tsx index 6054bd2eb2d..28b96c4c429 100644 --- a/protocol-designer/src/components/StepEditForm/fields/TipPositionField/__tests__/TipPositionModal.test.tsx +++ b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/__tests__/TipPositionModal.test.tsx @@ -82,9 +82,9 @@ describe('TipPositionModal', () => { fireEvent.click(screen.getByRole('radio', { name: 'Custom' })) expect(screen.getAllByRole('textbox', { name: '' })).toHaveLength(3) screen.getByText('X position') - screen.getByText('between -5.1 and 5.2') + screen.getByText('between -5.1 and 5.1') screen.getByText('Y position') - screen.getByText('between -5.2 and 5.3') + screen.getByText('between -5.2 and 5.2') screen.getByText('Z position') screen.getByText('between 0 and 100') screen.getByText('mock TipPositionViz') @@ -129,8 +129,8 @@ describe('TipPositionModal', () => { fireEvent.click(screen.getByText('done')) // display out of bounds error screen.getByText('accepted range is 0 to 100') - screen.getByText('accepted range is -5.2 to 5.3') - screen.getByText('accepted range is -5.1 to 5.2') + screen.getByText('accepted range is -5.2 to 5.2') + screen.getByText('accepted range is -5.1 to 5.1') const xInputField = screen.getAllByRole('textbox', { name: '' })[0] fireEvent.change(xInputField, { target: { value: 3.55555 } }) fireEvent.click(screen.getByText('done')) diff --git a/protocol-designer/src/components/StepEditForm/fields/TipPositionField/utils.ts b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/utils.ts index 96ed4729d49..4648aa78933 100644 --- a/protocol-designer/src/components/StepEditForm/fields/TipPositionField/utils.ts +++ b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/utils.ts @@ -1,3 +1,4 @@ +import floor from 'lodash/floor' import round from 'lodash/round' import { getIsTouchTipField } from '../../../../form-types' import { @@ -46,8 +47,20 @@ export function getDefaultMmFromBottom(args: { } } -export const roundValue = (value: number | string | null): number => { - return value === null ? 0 : round(Number(value), DECIMALS_ALLOWED) +export const roundValue = ( + value: number | string | null, + direction: 'up' | 'down' +): number => { + if (value === null) return 0 + + switch (direction) { + case 'up': { + return round(Number(value), DECIMALS_ALLOWED) + } + case 'down': { + return floor(Number(value), DECIMALS_ALLOWED) + } + } } const OUT_OF_BOUNDS: 'OUT_OF_BOUNDS' = 'OUT_OF_BOUNDS' From 26d55ec7416080120b2dbe06ca6f39885db01ba4 Mon Sep 17 00:00:00 2001 From: koji Date: Mon, 22 Apr 2024 11:59:46 -0400 Subject: [PATCH 180/194] fix(app, api-client): fix choose protocol slideout issue (#14949) * fix(app, api-client): fix choose protocol slideout issue --- .../__fixtures__/simpleAnalysisFile.json | 3 +- .../__tests__/ChooseProtocolSlideout.test.tsx | 72 ++++++++++++------- .../ChooseProtocolSlideout/index.tsx | 10 +-- .../useStoredProtocolAnalysis.test.tsx | 14 ++-- .../protocol-storage/__fixtures__/index.ts | 11 +++ 5 files changed, 72 insertions(+), 38 deletions(-) diff --git a/api-client/src/protocols/__fixtures__/simpleAnalysisFile.json b/api-client/src/protocols/__fixtures__/simpleAnalysisFile.json index 74faa60fcb6..bb6aacccd6e 100644 --- a/api-client/src/protocols/__fixtures__/simpleAnalysisFile.json +++ b/api-client/src/protocols/__fixtures__/simpleAnalysisFile.json @@ -3989,5 +3989,6 @@ } ] } - ] + ], + "robotType": "OT-2 Standard" } diff --git a/app/src/organisms/ChooseProtocolSlideout/__tests__/ChooseProtocolSlideout.test.tsx b/app/src/organisms/ChooseProtocolSlideout/__tests__/ChooseProtocolSlideout.test.tsx index 11583264b3e..7973023d184 100644 --- a/app/src/organisms/ChooseProtocolSlideout/__tests__/ChooseProtocolSlideout.test.tsx +++ b/app/src/organisms/ChooseProtocolSlideout/__tests__/ChooseProtocolSlideout.test.tsx @@ -3,15 +3,21 @@ import { vi, it, describe, expect, beforeEach } from 'vitest' import { StaticRouter } from 'react-router-dom' import { fireEvent, screen } from '@testing-library/react' +import { simpleAnalysisFileFixture } from '@opentrons/api-client' +import { OT2_ROBOT_TYPE } from '@opentrons/shared-data' import { renderWithProviders } from '../../../__testing-utils__' import { i18n } from '../../../i18n' import { getStoredProtocols } from '../../../redux/protocol-storage' import { mockConnectableRobot } from '../../../redux/discovery/__fixtures__' -import { storedProtocolData as storedProtocolDataFixture } from '../../../redux/protocol-storage/__fixtures__' +import { + storedProtocolData as storedProtocolDataFixture, + storedProtocolDataWithoutRunTimeParameters, +} from '../../../redux/protocol-storage/__fixtures__' import { useTrackCreateProtocolRunEvent } from '../../../organisms/Devices/hooks' import { useCreateRunFromProtocol } from '../../ChooseRobotToRunProtocolSlideout/useCreateRunFromProtocol' import { ChooseProtocolSlideout } from '../' import { useNotifyService } from '../../../resources/useNotifyService' +import type { ProtocolAnalysisOutput } from '@opentrons/shared-data' vi.mock('../../ChooseRobotToRunProtocolSlideout/useCreateRunFromProtocol') vi.mock('../../../redux/protocol-storage') @@ -30,6 +36,20 @@ const render = (props: React.ComponentProps) => { ) } +const modifiedSimpleAnalysisFileFixture = { + ...simpleAnalysisFileFixture, + robotType: OT2_ROBOT_TYPE, +} +const mockStoredProtocolDataFixture = [ + { + ...storedProtocolDataFixture, + mostRecentAnalysis: ({ + ...modifiedSimpleAnalysisFileFixture, + runTimeParameters: [], + } as any) as ProtocolAnalysisOutput, + }, +] + describe('ChooseProtocolSlideout', () => { let mockCreateRunFromProtocol = vi.fn() let mockTrackCreateProtocolRunEvent = vi.fn() @@ -38,7 +58,7 @@ describe('ChooseProtocolSlideout', () => { mockTrackCreateProtocolRunEvent = vi.fn( () => new Promise(resolve => resolve({})) ) - vi.mocked(getStoredProtocols).mockReturnValue([storedProtocolDataFixture]) + vi.mocked(getStoredProtocols).mockReturnValue(mockStoredProtocolDataFixture) vi.mocked(useCreateRunFromProtocol).mockReturnValue({ createRunFromProtocolSource: mockCreateRunFromProtocol, reset: vi.fn(), @@ -86,34 +106,32 @@ describe('ChooseProtocolSlideout', () => { ).toBeInTheDocument() }) - // it('calls createRunFromProtocolSource if CTA clicked', () => { - // const protocolDataWithoutRunTimeParameter = { - // ...storedProtocolDataFixture, - // runTimeParameters: [], - // } - // vi.mocked(getStoredProtocols).mockReturnValue([ - // protocolDataWithoutRunTimeParameter, - // ]) - // render({ - // robot: mockConnectableRobot, - // onCloseClick: vi.fn(), - // showSlideout: true, - // }) - // const proceedButton = screen.getByRole('button', { - // name: 'Proceed to setup', - // }) - // fireEvent.click(proceedButton) - // expect(mockCreateRunFromProtocol).toHaveBeenCalledWith({ - // files: [expect.any(File)], - // protocolKey: storedProtocolDataFixture.protocolKey, - // }) - // expect(mockTrackCreateProtocolRunEvent).toHaveBeenCalled() - // }) + it('calls createRunFromProtocolSource if CTA clicked', () => { + const protocolDataWithoutRunTimeParameter = { + ...storedProtocolDataWithoutRunTimeParameters, + } + vi.mocked(getStoredProtocols).mockReturnValue([ + protocolDataWithoutRunTimeParameter, + ]) + render({ + robot: mockConnectableRobot, + onCloseClick: vi.fn(), + showSlideout: true, + }) + const proceedButton = screen.getByRole('button', { + name: 'Proceed to setup', + }) + fireEvent.click(proceedButton) + expect(mockCreateRunFromProtocol).toHaveBeenCalledWith({ + files: [expect.any(File)], + protocolKey: storedProtocolDataFixture.protocolKey, + }) + expect(mockTrackCreateProtocolRunEvent).toHaveBeenCalled() + }) it('move to the second slideout if CTA clicked', () => { const protocolDataWithoutRunTimeParameter = { ...storedProtocolDataFixture, - runTimeParameters: [], } vi.mocked(getStoredProtocols).mockReturnValue([ protocolDataWithoutRunTimeParameter, @@ -132,7 +150,7 @@ describe('ChooseProtocolSlideout', () => { screen.getByText('Restore default values') }) - // ToDo (kk:04/08) update test for RTP + // ToDo (kk:04/18/2024) I will update test for RTP /* it('renders error state when there is a run creation error', () => { vi.mocked(useCreateRunFromProtocol).mockReturnValue({ diff --git a/app/src/organisms/ChooseProtocolSlideout/index.tsx b/app/src/organisms/ChooseProtocolSlideout/index.tsx index 6f00082013a..dfd7b7c12a2 100644 --- a/app/src/organisms/ChooseProtocolSlideout/index.tsx +++ b/app/src/organisms/ChooseProtocolSlideout/index.tsx @@ -461,7 +461,7 @@ export function ChooseProtocolSlideoutComponent( setSelectedProtocol(storedProtocol) } }} - robotName={robot.name} + robot={robot} {...{ selectedProtocol, runCreationError, runCreationErrorCode }} /> ) : ( @@ -483,7 +483,7 @@ interface StoredProtocolListProps { handleSelectProtocol: (storedProtocol: StoredProtocolData | null) => void runCreationError: string | null runCreationErrorCode: number | null - robotName: string + robot: Robot } function StoredProtocolList(props: StoredProtocolListProps): JSX.Element { @@ -492,11 +492,13 @@ function StoredProtocolList(props: StoredProtocolListProps): JSX.Element { handleSelectProtocol, runCreationError, runCreationErrorCode, - robotName, + robot, } = props const { t } = useTranslation(['device_details', 'protocol_details', 'shared']) const storedProtocols = useSelector((state: State) => getStoredProtocols(state) + ).filter( + protocol => protocol.mostRecentAnalysis?.robotType === robot.robotModel ) React.useEffect(() => { handleSelectProtocol(first(storedProtocols) ?? null) @@ -585,7 +587,7 @@ function StoredProtocolList(props: StoredProtocolListProps): JSX.Element { color: ${COLORS.red60}; text-decoration: ${TYPOGRAPHY.textDecorationUnderline}; `} - to={`/devices/${robotName}`} + to={`/devices/${robot.name}`} /> ), }} diff --git a/app/src/organisms/Devices/hooks/__tests__/useStoredProtocolAnalysis.test.tsx b/app/src/organisms/Devices/hooks/__tests__/useStoredProtocolAnalysis.test.tsx index fa63db104c6..4a165f628c5 100644 --- a/app/src/organisms/Devices/hooks/__tests__/useStoredProtocolAnalysis.test.tsx +++ b/app/src/organisms/Devices/hooks/__tests__/useStoredProtocolAnalysis.test.tsx @@ -1,9 +1,9 @@ import * as React from 'react' import { vi, it, expect, describe, beforeEach, afterEach } from 'vitest' import { when } from 'vitest-when' -import { QueryClient, QueryClientProvider, UseQueryResult } from 'react-query' +import { QueryClient, QueryClientProvider } from 'react-query' import { Provider } from 'react-redux' -import { createStore, Store } from 'redux' +import { createStore } from 'redux' import { renderHook } from '@testing-library/react' import { @@ -12,12 +12,10 @@ import { parsePipetteEntity, } from '@opentrons/api-client' import { useProtocolQuery } from '@opentrons/react-api-client' +import { OT2_ROBOT_TYPE } from '@opentrons/shared-data' import { storedProtocolData } from '../../../../redux/protocol-storage/__fixtures__' -import { - getStoredProtocol, - StoredProtocolData, -} from '../../../../redux/protocol-storage' +import { getStoredProtocol } from '../../../../redux/protocol-storage' import { useStoredProtocolAnalysis } from '../useStoredProtocolAnalysis' import { LABWARE_ENTITY, @@ -27,7 +25,10 @@ import { } from '../__fixtures__/storedProtocolAnalysis' import { useNotifyRunQuery } from '../../../../resources/runs' +import type { Store } from 'redux' +import type { UseQueryResult } from 'react-query' import type { Protocol, Run } from '@opentrons/api-client' +import type { StoredProtocolData } from '../../../../redux/protocol-storage' vi.mock('@opentrons/api-client') vi.mock('@opentrons/react-api-client') @@ -44,6 +45,7 @@ const modifiedStoredProtocolData = { errors: storedProtocolData?.mostRecentAnalysis?.errors, runTimeParameters: storedProtocolData?.mostRecentAnalysis?.runTimeParameters, + robotType: OT2_ROBOT_TYPE, }, } diff --git a/app/src/redux/protocol-storage/__fixtures__/index.ts b/app/src/redux/protocol-storage/__fixtures__/index.ts index 56f7f4d021a..be5500203a2 100644 --- a/app/src/redux/protocol-storage/__fixtures__/index.ts +++ b/app/src/redux/protocol-storage/__fixtures__/index.ts @@ -11,6 +11,17 @@ export const storedProtocolData: StoredProtocolData = { modified: 123456789, } +export const storedProtocolDataWithoutRunTimeParameters: StoredProtocolData = { + protocolKey: 'protocolKeyStub', + mostRecentAnalysis: ({ + ...simpleAnalysisFileFixture, + runTimeParameters: [], + } as any) as ProtocolAnalysisOutput, + srcFileNames: ['fakeSrcFileName'], + srcFiles: ['fakeSrcFile' as any], + modified: 123456789, +} + export const storedProtocolDir: StoredProtocolDir = { dirPath: 'path/to/protocol/dir', modified: 1234556789, From 838e356ad3e934d7fe12a1f0e9aa7ad13e4ffc80 Mon Sep 17 00:00:00 2001 From: Jethary Rader <66035149+jerader@users.noreply.github.com> Date: Mon, 22 Apr 2024 12:28:41 -0400 Subject: [PATCH 181/194] refactor(protocol-designer): assign module slot in createFileWizard instead of modal (#14951) closes AUTH-355 AUTH-22 --- .../CreateFileWizard/ModulesAndOtherTile.tsx | 59 ++-- .../CreateFileWizard/__tests__/utils.test.tsx | 280 +++++++----------- .../modals/CreateFileWizard/index.tsx | 27 +- .../modals/CreateFileWizard/utils.ts | 148 +++------ .../components/modules/EditModulesCard.tsx | 4 +- ...leModuleRow.tsx => MultipleModulesRow.tsx} | 4 +- .../__tests__/MultipleModuleRow.test.tsx | 8 +- .../src/modules/__tests__/moduleData.test.tsx | 88 ++++++ protocol-designer/src/modules/index.ts | 1 + protocol-designer/src/modules/moduleData.ts | 48 ++- protocol-designer/src/modules/thunks.ts | 33 +++ 11 files changed, 359 insertions(+), 341 deletions(-) rename protocol-designer/src/components/modules/{MultipleModuleRow.tsx => MultipleModulesRow.tsx} (98%) create mode 100644 protocol-designer/src/modules/__tests__/moduleData.test.tsx create mode 100644 protocol-designer/src/modules/thunks.ts diff --git a/protocol-designer/src/components/modals/CreateFileWizard/ModulesAndOtherTile.tsx b/protocol-designer/src/components/modals/CreateFileWizard/ModulesAndOtherTile.tsx index bcebf6313c3..b1ad18b0752 100644 --- a/protocol-designer/src/components/modals/CreateFileWizard/ModulesAndOtherTile.tsx +++ b/protocol-designer/src/components/modals/CreateFileWizard/ModulesAndOtherTile.tsx @@ -30,7 +30,6 @@ import { getModuleDisplayName, getModuleType, FLEX_ROBOT_TYPE, - THERMOCYCLER_MODULE_TYPE, MAGNETIC_BLOCK_TYPE, } from '@opentrons/shared-data' import { getIsCrashablePipetteSelected } from '../../../step-forms' @@ -45,9 +44,8 @@ import { ModuleFields } from '../FilePipettesModal/ModuleFields' import { GoBack } from './GoBack' import { getCrashableModuleSelected, - getDisabledEquipment, - getNextAvailableModuleSlot, - getTrashBinOptionDisabled, + getIsSlotAvailable, + getTrashOptionDisabled, } from './utils' import { EquipmentOption } from './EquipmentOption' import { HandleEnter } from './HandleEnter' @@ -197,10 +195,6 @@ function FlexModuleFields(props: WizardTileProps): JSX.Element { const additionalEquipment = watch('additionalEquipment') const moduleTypesOnDeck = modules != null ? Object.values(modules).map(module => module.type) : [] - const trashBinDisabled = getTrashBinOptionDisabled({ - additionalEquipment, - modules, - }) const handleSetEquipmentOption = (equipment: AdditionalEquipment): void => { if (additionalEquipment.includes(equipment)) { @@ -209,6 +203,11 @@ function FlexModuleFields(props: WizardTileProps): JSX.Element { setValue('additionalEquipment', [...additionalEquipment, equipment]) } } + const trashBinDisabled = getTrashOptionDisabled({ + additionalEquipment, + modules, + trashType: 'trashBin', + }) React.useEffect(() => { if (trashBinDisabled) { @@ -220,21 +219,10 @@ function FlexModuleFields(props: WizardTileProps): JSX.Element { {FLEX_SUPPORTED_MODULE_MODELS.map(moduleModel => { const moduleType = getModuleType(moduleModel) - const moduleOnDeck = moduleTypesOnDeck.includes(moduleType) + const isModuleOnDeck = moduleTypesOnDeck.includes(moduleType) + + const isDisabled = !getIsSlotAvailable(modules, additionalEquipment) - let defaultSlot = getNextAvailableModuleSlot( - modules, - additionalEquipment - ) - if (moduleType === THERMOCYCLER_MODULE_TYPE) { - defaultSlot = 'B1' - } else if (moduleType === MAGNETIC_BLOCK_TYPE) { - defaultSlot = 'D2' - } - const isDisabled = getDisabledEquipment({ - additionalEquipment, - modules, - })?.includes(moduleType) const handleMultiplesClick = (num: number): void => { const temperatureModules = modules != null @@ -250,10 +238,7 @@ function FlexModuleFields(props: WizardTileProps): JSX.Element { [uuid()]: { model: moduleModel, type: moduleType, - slot: getNextAvailableModuleSlot( - modules, - additionalEquipment - ), + slot: null, }, }) } @@ -274,7 +259,7 @@ function FlexModuleFields(props: WizardTileProps): JSX.Element { (moduleType !== TEMPERATURE_MODULE_TYPE && enableMoamFf) || !enableMoamFf ) { - if (moduleOnDeck) { + if (isModuleOnDeck) { const updatedModules = modules != null ? Object.fromEntries( @@ -290,7 +275,7 @@ function FlexModuleFields(props: WizardTileProps): JSX.Element { [uuid()]: { model: moduleModel, type: moduleType, - slot: defaultSlot, + slot: DEFAULT_SLOT_MAP[moduleModel], }, }) } @@ -301,10 +286,14 @@ function FlexModuleFields(props: WizardTileProps): JSX.Element { } text={getModuleDisplayName(moduleModel)} - disabled={isDisabled && !moduleOnDeck} + disabled={ + moduleType === MAGNETIC_BLOCK_TYPE + ? false + : isDisabled && !isModuleOnDeck + } onClick={handleOnClick} multiples={ moduleType === TEMPERATURE_MODULE_TYPE && enableMoamFf @@ -345,11 +334,11 @@ function FlexModuleFields(props: WizardTileProps): JSX.Element { robotType={FLEX_ROBOT_TYPE} onClick={() => handleSetEquipmentOption('wasteChute')} isSelected={additionalEquipment.includes('wasteChute')} - disabled={ - modules != null - ? Object.values(modules).some(module => module.slot === 'D3') - : false - } + disabled={getTrashOptionDisabled({ + additionalEquipment, + modules, + trashType: 'wasteChute', + })} image={ { { cutoutId: 'cutoutD3', cutoutFixtureId: SINGLE_RIGHT_SLOT_FIXTURE }, ]) }) - describe('getNextAvailableModuleSlot', () => { - it('should return D1 when there are no modules or staging areas', () => { - const result = getNextAvailableModuleSlot(null, []) - expect(result).toStrictEqual('D1') - }) - it('should return a C3 when all the modules are on the deck', () => { - const result = getNextAvailableModuleSlot( - { - 0: { - model: 'magneticBlockV1', - type: 'magneticBlockType', - slot: 'D1', - }, - 1: { - model: 'thermocyclerModuleV2', - type: 'thermocyclerModuleType', - slot: 'B1', - }, - 2: { - model: 'temperatureModuleV2', - type: 'temperatureModuleType', - slot: 'C1', - }, - }, - [] - ) - expect(result).toStrictEqual('C3') - }) +}) +describe('getIsSlotAvailable', () => { + it('should return true when there are no modules or additional equipment', () => { + const result = getIsSlotAvailable(null, []) + expect(result).toBe(true) }) - it('should return an empty string when all the modules and staging area slots are on the deck without TC', () => { - const result = getNextAvailableModuleSlot( - { - 0: { - model: 'heaterShakerModuleV1', - type: 'heaterShakerModuleType', - slot: 'D1', - }, - 1: { - model: 'temperatureModuleV2', - type: 'temperatureModuleType', - slot: 'C1', - }, - 2: { - model: 'temperatureModuleV2', - type: 'temperatureModuleType', - slot: 'B1', - }, + it('should return false when there is a TC and 7 modules', () => { + const mockModules = { + 0: { + model: 'heaterShakerModuleV1', + type: 'heaterShakerModuleType', + slot: 'D1', }, - [ - 'stagingArea_cutoutA3', - 'stagingArea_cutoutB3', - 'stagingArea_cutoutC3', - 'stagingArea_cutoutD3', - 'trashBin', - ] - ) - expect(result).toStrictEqual('') - }) - it('should return an empty string when all the modules and staging area slots are on the deck with TC', () => { - const result = getNextAvailableModuleSlot( - { - 0: { - model: 'heaterShakerModuleV1', - type: 'heaterShakerModuleType', - slot: 'D1', - }, - 1: { - model: 'thermocyclerModuleV2', - type: 'thermocyclerModuleType', - slot: 'B1', - }, + 1: { + model: 'temperatureModuleV2', + type: 'temperatureModuleType', + slot: 'D3', }, - [ - 'stagingArea_cutoutA3', - 'stagingArea_cutoutB3', - 'stagingArea_cutoutC3', - 'stagingArea_cutoutD3', - 'trashBin', - ] - ) - expect(result).toStrictEqual('') + 2: { + model: 'temperatureModuleV2', + type: 'temperatureModuleType', + slot: 'C1', + }, + 3: { + model: 'temperatureModuleV2', + type: 'temperatureModuleType', + slot: 'B3', + }, + 4: { + model: 'thermocyclerModuleV2', + type: 'thermocyclerModuleType', + slot: 'B1', + }, + 5: { + model: 'temperatureModuleV2', + type: 'temperatureModuleType', + slot: 'A3', + }, + 6: { + model: 'temperatureModuleV2', + type: 'temperatureModuleType', + slot: 'C3', + }, + } as any + const result = getIsSlotAvailable(mockModules, []) + expect(result).toBe(false) + }) + it('should return true when there are 9 additional equipment and 1 is a waste chute on the staging area and one is a gripper', () => { + const mockAdditionalEquipment: AdditionalEquipment[] = [ + 'trashBin', + 'stagingArea_cutoutA3', + 'stagingArea_cutoutB3', + 'stagingArea_cutoutC3', + 'stagingArea_cutoutD3', + 'wasteChute', + 'trashBin', + 'gripper', + 'trashBin', + ] + const result = getIsSlotAvailable(null, mockAdditionalEquipment) + expect(result).toBe(true) }) }) -describe('getNextAvailableModuleSlot', () => { - it('should return nothing as disabled', () => { - const result = getDisabledEquipment({ - additionalEquipment: [], - modules: null, - }) - expect(result).toStrictEqual([]) +describe('getTrashSlot', () => { + it('should return the default slot A3 when there is no staging area or module in that slot', () => { + MOCK_FORM_STATE = { + ...MOCK_FORM_STATE, + additionalEquipment: ['trashBin'], + } + const result = getTrashSlot(MOCK_FORM_STATE) + expect(result).toBe(FLEX_TRASH_DEFAULT_SLOT) + }) + it('should return cutoutA1 when there is a staging area in slot A3', () => { + MOCK_FORM_STATE = { + ...MOCK_FORM_STATE, + additionalEquipment: ['stagingArea_cutoutA3'], + } + const result = getTrashSlot(MOCK_FORM_STATE) + expect(result).toBe('cutoutA1') }) - it('should return the TC as disabled', () => { - const result = getDisabledEquipment({ - additionalEquipment: [], +}) +describe('getTrashOptionDisabled', () => { + it('returns false when there is a trash bin already', () => { + const result = getTrashOptionDisabled({ + trashType: 'trashBin', + additionalEquipment: ['trashBin'], modules: { 0: { model: 'heaterShakerModuleV1', type: 'heaterShakerModuleType', - slot: 'A1', + slot: 'D1', }, }, }) - expect(result).toStrictEqual([THERMOCYCLER_MODULE_TYPE]) + expect(result).toBe(false) }) - it('should return all module types if there is no available slot', () => { - const result = getDisabledEquipment({ + it('returns false when there is an available slot', () => { + const result = getTrashOptionDisabled({ + trashType: 'trashBin', + additionalEquipment: ['trashBin'], + modules: null, + }) + expect(result).toBe(false) + }) + it('returns true when there is no available slot and trash bin is not selected yet', () => { + const result = getTrashOptionDisabled({ + trashType: 'trashBin', additionalEquipment: [ 'stagingArea_cutoutA3', 'stagingArea_cutoutB3', 'stagingArea_cutoutC3', 'stagingArea_cutoutD3', - 'trashBin', ], modules: { 0: { @@ -185,85 +181,13 @@ describe('getNextAvailableModuleSlot', () => { type: 'temperatureModuleType', slot: 'B1', }, - }, - }) - expect(result).toStrictEqual([ - THERMOCYCLER_MODULE_TYPE, - TEMPERATURE_MODULE_TYPE, - HEATERSHAKER_MODULE_TYPE, - ]) - }) -}) -describe('getTrashSlot', () => { - it('should return the default slot A3 when there is no staging area or module in that slot', () => { - MOCK_FORM_STATE = { - ...MOCK_FORM_STATE, - additionalEquipment: ['trashBin'], - } - const result = getTrashSlot(MOCK_FORM_STATE) - expect(result).toBe(FLEX_TRASH_DEFAULT_SLOT) - }) - it('should return cutoutA1 when there is a staging area in slot A3', () => { - MOCK_FORM_STATE = { - ...MOCK_FORM_STATE, - additionalEquipment: ['stagingArea_cutoutA3'], - } - const result = getTrashSlot(MOCK_FORM_STATE) - expect(result).toBe('cutoutA1') - }) - describe('getTrashBinOptionDisabled', () => { - it('returns false when there is a trash bin already', () => { - const result = getTrashBinOptionDisabled({ - additionalEquipment: ['trashBin'], - modules: { - 0: { - model: 'heaterShakerModuleV1', - type: 'heaterShakerModuleType', - slot: 'D1', - }, - }, - }) - expect(result).toBe(false) - }) - it('returns false when there is an available slot', () => { - const result = getTrashBinOptionDisabled({ - additionalEquipment: ['trashBin'], - modules: null, - }) - expect(result).toBe(false) - }) - it('returns true when there is no available slot and trash bin is not selected yet', () => { - const result = getTrashBinOptionDisabled({ - additionalEquipment: [ - 'stagingArea_cutoutA3', - 'stagingArea_cutoutB3', - 'stagingArea_cutoutC3', - 'stagingArea_cutoutD3', - ], - modules: { - 0: { - model: 'heaterShakerModuleV1', - type: 'heaterShakerModuleType', - slot: 'D1', - }, - 1: { - model: 'temperatureModuleV2', - type: 'temperatureModuleType', - slot: 'C1', - }, - 2: { - model: 'temperatureModuleV2', - type: 'temperatureModuleType', - slot: 'B1', - }, - 3: { - model: 'temperatureModuleV2', - type: 'temperatureModuleType', - slot: 'A1', - }, + 3: { + model: 'temperatureModuleV2', + type: 'temperatureModuleType', + slot: 'A1', }, - }) - expect(result).toBe(true) + }, }) + expect(result).toBe(true) }) }) diff --git a/protocol-designer/src/components/modals/CreateFileWizard/index.tsx b/protocol-designer/src/components/modals/CreateFileWizard/index.tsx index b19ab426f65..53ae9a88f6a 100644 --- a/protocol-designer/src/components/modals/CreateFileWizard/index.tsx +++ b/protocol-designer/src/components/modals/CreateFileWizard/index.tsx @@ -43,6 +43,7 @@ import { createDeckFixture, toggleIsGripperRequired, } from '../../../step-forms/actions/additionalItems' +import { createModuleWithNoSlot } from '../../../modules' import { RobotTypeTile } from './RobotTypeTile' import { MetadataTile } from './MetadataTile' import { FirstPipetteTypeTile, SecondPipetteTypeTile } from './PipetteTypeTile' @@ -229,9 +230,29 @@ export function CreateFileWizard(): JSX.Element | null { } // create modules - modules.forEach(moduleArgs => - dispatch(stepFormActions.createModule(moduleArgs)) - ) + // sort so modules with slot are created first + // then modules without a slot are generated in remaining available slots + modules.sort((a, b) => { + if (a.slot == null && b.slot != null) { + return 1 + } + if (b.slot == null && a.slot != null) { + return -1 + } + return 0 + }) + + modules.forEach(moduleArgs => { + return moduleArgs.slot != null + ? dispatch(stepFormActions.createModule(moduleArgs)) + : dispatch( + createModuleWithNoSlot({ + model: moduleArgs.model, + type: moduleArgs.type, + }) + ) + }) + // add gripper if (values.additionalEquipment.includes('gripper')) { dispatch(toggleIsGripperRequired()) diff --git a/protocol-designer/src/components/modals/CreateFileWizard/utils.ts b/protocol-designer/src/components/modals/CreateFileWizard/utils.ts index 20abcf27cb3..7a23706a680 100644 --- a/protocol-designer/src/components/modals/CreateFileWizard/utils.ts +++ b/protocol-designer/src/components/modals/CreateFileWizard/utils.ts @@ -1,6 +1,4 @@ import { - HEATERSHAKER_MODULE_TYPE, - TEMPERATURE_MODULE_TYPE, THERMOCYCLER_MODULE_TYPE, WASTE_CHUTE_CUTOUT, } from '@opentrons/shared-data' @@ -13,41 +11,6 @@ import type { AdditionalEquipment, FormState } from './types' export const FLEX_TRASH_DEFAULT_SLOT = 'cutoutA3' -const MODULES_SLOTS_FLEX = [ - { - value: 'cutoutD1', - slot: 'D1', - }, - { - value: 'cutoutC3', - slot: 'C3', - }, - { - value: 'cutoutB1', - slot: 'B1', - }, - { - value: 'cutoutB3', - slot: 'B3', - }, - { - value: 'cutoutA3', - slot: 'A3', - }, - { - value: 'cutoutD3', - slot: 'D3', - }, - { - value: 'cutoutC1', - slot: 'C1', - }, - { - value: 'cutoutA1', - slot: 'A1', - }, -] - export const getCrashableModuleSelected = ( modules: FormModules | null, moduleType: ModuleType @@ -120,102 +83,57 @@ export const getUnoccupiedStagingAreaSlots = ( return unoccupiedSlots } -export const getNextAvailableModuleSlot = ( +const TOTAL_MODULE_SLOTS = 8 + +export const getIsSlotAvailable = ( modules: FormState['modules'], additionalEquipment: FormState['additionalEquipment'] -): string => { - const moduleSlots = - modules != null - ? Object.values(modules).flatMap(module => - module.type === THERMOCYCLER_MODULE_TYPE - ? [module.slot, 'A1'] - : module.slot - ) - : [] - const stagingAreas = additionalEquipment.filter(equipment => - equipment.includes('stagingArea') +): boolean => { + const moduleLength = modules != null ? Object.keys(modules).length : 0 + const additionalEquipmentLength = additionalEquipment.length + const hasTC = Object.values(modules || {}).some( + module => module.type === THERMOCYCLER_MODULE_TYPE ) - const stagingAreaCutouts = stagingAreas.map(cutout => cutout.split('_')[1]) - const hasWasteChute = additionalEquipment.find(equipment => + + const filteredModuleLength = hasTC ? moduleLength + 1 : moduleLength + const hasWasteChute = additionalEquipment.some(equipment => equipment.includes('wasteChute') ) - const wasteChuteSlot = Boolean(hasWasteChute) - ? [WASTE_CHUTE_CUTOUT as string] - : [] - const trashBin = additionalEquipment.find(equipment => - equipment.includes('trashBin') + const isStagingAreaInD3 = additionalEquipment + .filter(equipment => equipment.includes('stagingArea')) + .find(stagingArea => stagingArea.split('_')[1] === 'cutoutD3') + const hasGripper = additionalEquipment.some(equipment => + equipment.includes('gripper') ) - const hasTC = - modules != null - ? Object.values(modules).some( - module => module.type === THERMOCYCLER_MODULE_TYPE - ) - : false - // removing slot(s) for the trash if spaces are limited - let removeSlotForTrash = MODULES_SLOTS_FLEX - if (trashBin != null && hasTC) { - removeSlotForTrash = MODULES_SLOTS_FLEX.slice(0, -2) - } else if (trashBin != null && !hasTC) { - removeSlotForTrash = MODULES_SLOTS_FLEX.slice(0, -1) + let filteredAdditionalEquipmentLength = additionalEquipmentLength + if (hasWasteChute && isStagingAreaInD3) { + filteredAdditionalEquipmentLength = filteredAdditionalEquipmentLength - 1 } - const unoccupiedSlot = removeSlotForTrash.find( - cutout => - !stagingAreaCutouts.includes(cutout.value) && - !moduleSlots.includes(cutout.slot) && - !wasteChuteSlot.includes(cutout.value) - ) - if (unoccupiedSlot == null) { - return '' + if (hasGripper) { + filteredAdditionalEquipmentLength = filteredAdditionalEquipmentLength - 1 } - return unoccupiedSlot?.slot ?? '' + return ( + filteredModuleLength + filteredAdditionalEquipmentLength < + TOTAL_MODULE_SLOTS + ) } -interface DisabledEquipmentProps { +interface TrashOptionDisabledProps { + trashType: 'trashBin' | 'wasteChute' additionalEquipment: AdditionalEquipment[] modules: FormModules | null } -export const getDisabledEquipment = ( - props: DisabledEquipmentProps -): string[] => { - const { additionalEquipment, modules } = props - const nextAvailableSlot = getNextAvailableModuleSlot( - modules, - additionalEquipment - ) - const disabledEquipment: string[] = [] - - const moduleSlots = - modules != null - ? Object.values(modules).flatMap(module => - module.type === THERMOCYCLER_MODULE_TYPE - ? [module.slot, 'A1'] - : module.slot - ) - : [] - - if (moduleSlots.includes('A1') || moduleSlots.includes('B1')) { - disabledEquipment.push(THERMOCYCLER_MODULE_TYPE) - } - if (nextAvailableSlot === '') { - disabledEquipment.push(TEMPERATURE_MODULE_TYPE, HEATERSHAKER_MODULE_TYPE) - } - - return disabledEquipment -} - -export const getTrashBinOptionDisabled = ( - props: DisabledEquipmentProps +export const getTrashOptionDisabled = ( + props: TrashOptionDisabledProps ): boolean => { - const { additionalEquipment, modules } = props - const nextAvailableSlot = getNextAvailableModuleSlot( - modules, - additionalEquipment + const { additionalEquipment, modules, trashType } = props + return ( + !getIsSlotAvailable(modules, additionalEquipment) && + !additionalEquipment.includes(trashType) ) - const hasTrashBinAlready = additionalEquipment.includes('trashBin') - return nextAvailableSlot === '' && !hasTrashBinAlready } export const getTrashSlot = (values: FormState): string => { diff --git a/protocol-designer/src/components/modules/EditModulesCard.tsx b/protocol-designer/src/components/modules/EditModulesCard.tsx index 896463c295c..27dcc233ede 100644 --- a/protocol-designer/src/components/modules/EditModulesCard.tsx +++ b/protocol-designer/src/components/modules/EditModulesCard.tsx @@ -28,7 +28,7 @@ import { ModuleRow } from './ModuleRow' import { AdditionalItemsRow } from './AdditionalItemsRow' import { isModuleWithCollisionIssue } from './utils' import { StagingAreasRow } from './StagingAreasRow' -import { MultipleModuleRow } from './MultipleModuleRow' +import { MultipleModulesRow } from './MultipleModulesRow' import type { AdditionalEquipmentEntity } from '@opentrons/step-generation' @@ -146,7 +146,7 @@ export function EditModulesCard(props: Props): JSX.Element { ) } else if (moduleData != null && moduleData.length > 1) { return ( - ) => { - return renderWithProviders(, { +const render = (props: React.ComponentProps) => { + return renderWithProviders(, { i18nInstance: i18n, })[0] } @@ -37,7 +37,7 @@ const mockTemp2: ModuleOnDeck = { } describe('MultipleModuleRow', () => { - let props: React.ComponentProps + let props: React.ComponentProps beforeEach(() => { props = { moduleType: TEMPERATURE_MODULE_TYPE, diff --git a/protocol-designer/src/modules/__tests__/moduleData.test.tsx b/protocol-designer/src/modules/__tests__/moduleData.test.tsx new file mode 100644 index 00000000000..9d27732bf56 --- /dev/null +++ b/protocol-designer/src/modules/__tests__/moduleData.test.tsx @@ -0,0 +1,88 @@ +import { describe, it, expect } from 'vitest' +import { getNextAvailableModuleSlot } from '../moduleData' +import type { InitialDeckSetup } from '../../step-forms' + +describe('getNextAvailableModuleSlot', () => { + it('renders slot D1 when no slots are occupied', () => { + const mockInitialDeckSetup: InitialDeckSetup = { + modules: {}, + labware: {}, + pipettes: {}, + additionalEquipmentOnDeck: {}, + } + const result = getNextAvailableModuleSlot(mockInitialDeckSetup) + expect(result).toBe('D1') + }) + it('renders slot C1 when other slots are occupied', () => { + const mockInitialDeckSetup: InitialDeckSetup = { + modules: {}, + labware: {}, + pipettes: {}, + additionalEquipmentOnDeck: { + wasteChuteId: { + name: 'wasteChute', + id: 'wasteChuteId', + location: 'D3', + }, + trashBinId: { + name: 'trashBin', + id: 'trashBinId', + location: 'D1', + }, + }, + } + const result = getNextAvailableModuleSlot(mockInitialDeckSetup) + expect(result).toBe('C1') + }) + it('renders undefined when all slots are occupied', () => { + const mockInitialDeckSetup: InitialDeckSetup = { + modules: { + thermocycler: { + model: 'thermocyclerModuleV2', + id: 'thermocycler', + type: 'thermocyclerModuleType', + slot: 'B1', + moduleState: {} as any, + }, + temperature: { + model: 'temperatureModuleV2', + id: 'temperature', + type: 'temperatureModuleType', + slot: 'C1', + moduleState: {} as any, + }, + }, + labware: {}, + pipettes: {}, + additionalEquipmentOnDeck: { + wasteChuteId: { + name: 'wasteChute', + id: 'wasteChuteId', + location: 'D3', + }, + trashBinId: { + name: 'trashBin', + id: 'trashBinId', + location: 'D1', + }, + stagingArea1: { + name: 'stagingArea', + id: 'stagingArea1', + location: 'A3', + }, + stagingArea2: { + name: 'stagingArea', + id: 'stagingArea2', + location: 'B3', + }, + stagingArea3: { + name: 'stagingArea', + id: 'stagingArea3', + location: 'C3', + }, + }, + } + const result = getNextAvailableModuleSlot(mockInitialDeckSetup) + expect(result).toBe(undefined) + }) +}) diff --git a/protocol-designer/src/modules/index.ts b/protocol-designer/src/modules/index.ts index 82b41275ed4..8ca029f4e14 100644 --- a/protocol-designer/src/modules/index.ts +++ b/protocol-designer/src/modules/index.ts @@ -1 +1,2 @@ export * from './moduleData' +export * from './thunks' diff --git a/protocol-designer/src/modules/moduleData.ts b/protocol-designer/src/modules/moduleData.ts index a2d05f33bc8..240a2e11eae 100644 --- a/protocol-designer/src/modules/moduleData.ts +++ b/protocol-designer/src/modules/moduleData.ts @@ -1,14 +1,27 @@ -import { SPAN7_8_10_11_SLOT } from '../constants' +import { COLUMN_4_SLOTS } from '@opentrons/step-generation' import { MAGNETIC_MODULE_TYPE, TEMPERATURE_MODULE_TYPE, THERMOCYCLER_MODULE_TYPE, HEATERSHAKER_MODULE_TYPE, - ModuleType, MAGNETIC_BLOCK_TYPE, + MOVABLE_TRASH_ADDRESSABLE_AREAS, + WASTE_CHUTE_ADDRESSABLE_AREAS, + FIXED_TRASH_ID, +} from '@opentrons/shared-data' +import { SPAN7_8_10_11_SLOT } from '../constants' +import { getStagingAreaAddressableAreas } from '../utils' +import { getSlotIsEmpty } from '../step-forms' +import type { + ModuleType, RobotType, + CutoutId, + AddressableAreaName, } from '@opentrons/shared-data' -import { DropdownOption } from '@opentrons/components' +import type { DropdownOption } from '@opentrons/components' +import type { InitialDeckSetup } from '../step-forms' +import type { DeckSlot } from '../types' + export const SUPPORTED_MODULE_TYPES: ModuleType[] = [ HEATERSHAKER_MODULE_TYPE, MAGNETIC_BLOCK_TYPE, @@ -270,3 +283,32 @@ export function getAllModuleSlotsByType( } return slot } + +const FLEX_MODULE_SLOTS = ['D1', 'D3', 'C1', 'C3', 'B1', 'B3', 'A1', 'A3'] + +export function getNextAvailableModuleSlot( + initialDeckSetup: InitialDeckSetup +): DeckSlot | undefined { + return FLEX_MODULE_SLOTS.find(slot => { + const cutoutIds = Object.values(initialDeckSetup.additionalEquipmentOnDeck) + .filter(ae => ae.name === 'stagingArea') + .map(ae => ae.location as CutoutId) + const stagingAreaAddressableAreaNames = getStagingAreaAddressableAreas( + cutoutIds + ) + const addressableAreaName = stagingAreaAddressableAreaNames.find( + aa => aa === slot + ) + let isSlotEmpty: boolean = getSlotIsEmpty(initialDeckSetup, slot, true) + if (addressableAreaName == null && COLUMN_4_SLOTS.includes(slot)) { + isSlotEmpty = false + } else if ( + MOVABLE_TRASH_ADDRESSABLE_AREAS.includes(slot as AddressableAreaName) || + WASTE_CHUTE_ADDRESSABLE_AREAS.includes(slot as AddressableAreaName) || + slot === FIXED_TRASH_ID + ) { + isSlotEmpty = false + } + return isSlotEmpty + }) +} diff --git a/protocol-designer/src/modules/thunks.ts b/protocol-designer/src/modules/thunks.ts new file mode 100644 index 00000000000..655eeb07a7c --- /dev/null +++ b/protocol-designer/src/modules/thunks.ts @@ -0,0 +1,33 @@ +import { selectors as stepFormSelectors } from '../step-forms' +import { uuid } from '../utils' +import { getNextAvailableModuleSlot } from './moduleData' +import type { ModuleModel, ModuleType } from '@opentrons/shared-data' +import type { CreateModuleAction } from '../step-forms/actions' +import type { ThunkAction } from '../types' + +interface CreateModuleWithNoSloArgs { + type: ModuleType + model: ModuleModel +} +export const createModuleWithNoSlot: ( + args: CreateModuleWithNoSloArgs +) => ThunkAction = args => (dispatch, getState) => { + const { model, type } = args + const state = getState() + const initialDeckSetup = stepFormSelectors.getInitialDeckSetup(state) + const slot = getNextAvailableModuleSlot(initialDeckSetup) + + if (slot == null) { + console.assert(slot, 'expected to find available slot but could not') + } + + dispatch({ + type: 'CREATE_MODULE', + payload: { + model, + type, + slot: slot ?? '', + id: `${uuid()}:${type}}`, + }, + }) +} From ec73d82853f31a56b94e73d095efa65ac350191d Mon Sep 17 00:00:00 2001 From: koji Date: Mon, 22 Apr 2024 13:01:43 -0400 Subject: [PATCH 182/194] refactor(components): refactor roundtab stories (#14956) * refactor(components): refactor roundtab stories --- components/src/molecules/RoundTab.stories.tsx | 109 +++++++++++------- 1 file changed, 67 insertions(+), 42 deletions(-) diff --git a/components/src/molecules/RoundTab.stories.tsx b/components/src/molecules/RoundTab.stories.tsx index be08c541743..fc0821c793d 100644 --- a/components/src/molecules/RoundTab.stories.tsx +++ b/components/src/molecules/RoundTab.stories.tsx @@ -1,55 +1,80 @@ import * as React from 'react' import { SPACING, TYPOGRAPHY } from '../ui-style-constants' -import { Flex, Text } from '../primitives' -import { DIRECTION_ROW } from '../styles' -import { RoundTab } from './RoundTab' -import type { Story, Meta } from '@storybook/react' +import { Flex } from '../primitives' +import { StyledText } from '../atoms/StyledText' +import { DIRECTION_COLUMN, DIRECTION_ROW } from '../styles' +import { RoundTab as RoundTabComponent } from './RoundTab' +import type { Meta, StoryObj } from '@storybook/react' -export default { +const meta: Meta = { title: 'Library/Molecules/RoundTab', - component: RoundTab, -} as Meta - -const Template: Story< - React.ComponentProps -> = (): JSX.Element => { - const [step, setStep] = React.useState<'details' | 'pipette' | 'module'>( - 'details' - ) + component: RoundTabComponent, + decorators: [Story => ], +} +export default meta + +const Tabs = (): JSX.Element => { + const [step, setStep] = React.useState< + 'setup' | 'parameters' | 'module controls' | 'run preview' + >('setup') return ( - setStep('details')} - > - - {'Protocol Name and Description'} - - - - setStep('pipette')} + + setStep('setup')} + tabName={'setup'} + > + + {'Setup'} + + + + setStep('parameters')} + > + + {'Parameters'} + + + + setStep('module controls')} + > + + {'Module Controls'} + + + + setStep('run preview')} + > + + {'Run Preview'} + + + + - - {'Pipette Selection'} - - - - setStep('module')}> - - {'Module Selection'} - - + {step} + ) } -export const Basic = Template.bind({}) -Basic.args = {} +type Story = StoryObj + +export const RoundTab: Story = { + args: {}, +} From 25329979523764ef0010870f46fd63f5202d0f5b Mon Sep 17 00:00:00 2001 From: Jamey Huffnagle Date: Mon, 22 Apr 2024 14:24:46 -0400 Subject: [PATCH 183/194] fix(app): prevent "run again" banner from rendering after navigating away from the current run (#14973) Closes RQA-2620 --- .../Devices/ProtocolRun/ProtocolRunHeader.tsx | 111 ++++++++++-------- .../__tests__/ProtocolRunHeader.test.tsx | 19 ++- 2 files changed, 76 insertions(+), 54 deletions(-) diff --git a/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader.tsx b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader.tsx index 8d65ef71417..0bfa08ce47b 100644 --- a/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader.tsx +++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader.tsx @@ -174,7 +174,6 @@ export function ProtocolRunHeader({ const [pipettesWithTip, setPipettesWithTip] = React.useState< PipettesWithTip[] >([]) - const [closeTerminalBanner, setCloseTerminalBanner] = React.useState(false) const isResetRunLoadingRef = React.useRef(false) const { data: runRecord } = useNotifyRunQuery(runId, { staleTime: Infinity }) const highestPriorityError = @@ -200,7 +199,7 @@ export function ProtocolRunHeader({ const { data: doorStatus } = useDoorQuery({ refetchInterval: EQUIPMENT_POLL_MS, }) - let isDoorOpen = false + let isDoorOpen: boolean if (isFlex) { isDoorOpen = doorStatus?.data.status === 'open' } else if (!isFlex && Boolean(doorSafetySetting?.value)) { @@ -248,7 +247,9 @@ export function ProtocolRunHeader({ } }, [protocolData, isRobotViewable, history]) + // Side effects dependent on the current run state. React.useEffect(() => { + // After a user-initiated stopped run, close the run current run automatically. if (runStatus === RUN_STATUS_STOPPED && isRunCurrent && runId != null) { trackProtocolRunEvent({ name: ANALYTICS_PROTOCOL_RUN_FINISH, @@ -260,12 +261,6 @@ export function ProtocolRunHeader({ } }, [runStatus, isRunCurrent, runId, closeCurrentRun]) - React.useEffect(() => { - if (runStatus === RUN_STATUS_IDLE) { - setCloseTerminalBanner(false) - } - }, [runStatus]) - const startedAtTimestamp = startedAt != null ? formatTimestamp(startedAt) : EMPTY_TIMESTAMP @@ -310,7 +305,6 @@ export function ProtocolRunHeader({ properties: robotAnalyticsData ?? undefined, }) closeCurrentRun() - setCloseTerminalBanner(true) } return ( @@ -375,7 +369,7 @@ export function ProtocolRunHeader({ CANCELLABLE_STATUSES.includes(runStatus) ? ( {t('shared:close_robot_door')} ) : null} - {mostRecentRunId === runId && !closeTerminalBanner ? ( + {mostRecentRunId === runId ? ( ) : null} {mostRecentRunId === runId && @@ -479,7 +474,9 @@ export function ProtocolRunHeader({ setShowDropTipWizard(false) setPipettesWithTip(prevPipettesWithTip => { const pipettesWithTip = prevPipettesWithTip.slice(1) ?? [] - if (pipettesWithTip.length === 0) closeCurrentRun() + if (pipettesWithTip.length === 0) { + closeCurrentRun() + } return pipettesWithTip }) }} @@ -570,6 +567,7 @@ interface ActionButtonProps { isResetRunLoadingRef: React.MutableRefObject } +// TODO(jh, 04-22-2024): Refactor switch cases into separate factories to increase readability and testability. function ActionButton(props: ActionButtonProps): JSX.Element { const { runId, @@ -613,9 +611,7 @@ function ActionButton(props: ActionButtonProps): JSX.Element { robotName, runId ) - const [showIsShakingModal, setShowIsShakingModal] = React.useState( - false - ) + const [showIsShakingModal, setShowIsShakingModal] = React.useState(false) const isSetupComplete = isCalibrationComplete && isModuleCalibrationComplete && @@ -804,12 +800,14 @@ function ActionButton(props: ActionButtonProps): JSX.Element { ) } +// TODO(jh 04-24-2024): Split TerminalRunBanner into a RunSuccessBanner and RunFailedBanner. interface TerminalRunProps { runStatus: RunStatus | null handleClearClick: () => void isClosingCurrentRun: boolean setShowRunFailedModal: (showRunFailedModal: boolean) => void isResetRunLoading: boolean + isRunCurrent: boolean highestPriorityError?: RunError | null } function TerminalRunBanner(props: TerminalRunProps): JSX.Element | null { @@ -820,51 +818,64 @@ function TerminalRunBanner(props: TerminalRunProps): JSX.Element | null { setShowRunFailedModal, highestPriorityError, isResetRunLoading, + isRunCurrent, } = props const { t } = useTranslation('run_details') - const handleClick = (): void => { + const handleRunSuccessClick = (): void => { + handleClearClick() + } + + const handleFailedRunClick = (): void => { handleClearClick() setShowRunFailedModal(true) } - if ( - isResetRunLoading === false && - (runStatus === RUN_STATUS_FAILED || runStatus === RUN_STATUS_SUCCEEDED) - ) { + const buildSuccessBanner = (): JSX.Element => { return ( - <> - {runStatus === RUN_STATUS_SUCCEEDED ? ( - - - {t('run_completed')} - - - ) : ( - - - - {t('error_info', { - errorType: highestPriorityError?.errorType, - errorCode: highestPriorityError?.errorCode, - })} - + + + {t('run_completed')} + + + ) + } - - {t('view_error')} - - - - )} - + const buildErrorBanner = (): JSX.Element => { + return ( + + + + {t('error_info', { + errorType: highestPriorityError?.errorType, + errorCode: highestPriorityError?.errorCode, + })} + + + + {t('view_error')} + + + ) } - return null + + if ( + runStatus === RUN_STATUS_SUCCEEDED && + isRunCurrent && + !isResetRunLoading + ) { + return buildSuccessBanner() + } else if (runStatus === RUN_STATUS_FAILED && !isResetRunLoading) { + return buildErrorBanner() + } else { + return null + } } diff --git a/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunHeader.test.tsx b/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunHeader.test.tsx index 65ea98c906f..3b6f0f9025b 100644 --- a/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunHeader.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunHeader.test.tsx @@ -814,7 +814,7 @@ describe('ProtocolRunHeader', () => { screen.getByText('Run completed.') }) - it('clicking close on a terminal run banner closes the run context and dismisses the banner', async () => { + it('clicking close on a terminal run banner closes the run context', async () => { when(vi.mocked(useNotifyRunQuery)) .calledWith(RUN_ID) .thenReturn({ @@ -827,9 +827,20 @@ describe('ProtocolRunHeader', () => { fireEvent.click(screen.getByTestId('Banner_close-button')) expect(mockCloseCurrentRun).toBeCalled() - await waitFor(() => { - expect(screen.queryByText('Run completed.')).not.toBeInTheDocument() - }) + }) + + it('does not display the "run successful" banner if the successful run is not current', async () => { + when(vi.mocked(useNotifyRunQuery)) + .calledWith(RUN_ID) + .thenReturn({ + data: { data: { ...mockSucceededRun, current: false } }, + } as UseQueryResult) + when(vi.mocked(useRunStatus)) + .calledWith(RUN_ID) + .thenReturn(RUN_STATUS_SUCCEEDED) + render() + + expect(screen.queryByText('Run completed.')).not.toBeInTheDocument() }) it('if a heater shaker is shaking, clicking on start run should render HeaterShakerIsRunningModal', async () => { From d4f7f17c8b57f0c1bc38f3440bbee86861366c41 Mon Sep 17 00:00:00 2001 From: Ryan Howard Date: Mon, 22 Apr 2024 14:30:11 -0400 Subject: [PATCH 184/194] feat(hardware-testing): enable multi sensor processing in liquid probe (#14883) # Overview This allows the liquid level detection script to tell the pipette to buffer the data from both pipettes and fetch them afterwards, it will now spit out seprate CSVs for each sensor. post processing not yet updated so the final report just grabs one from each trial, will implement in EXEC-268 # Test Plan # Changelog # Review requests # Risk assessment --- api/src/opentrons/config/defaults_ot3.py | 64 ++++- api/src/opentrons/config/types.py | 4 +- .../backends/flex_protocol.py | 2 +- .../backends/ot3controller.py | 12 +- .../hardware_control/backends/ot3simulator.py | 2 +- .../hardware_control/backends/ot3utils.py | 1 + api/src/opentrons/hardware_control/ot3api.py | 2 +- api/src/opentrons/hardware_control/types.py | 1 + api/tests/opentrons/config/ot3_settings.py | 2 +- .../backends/test_ot3_controller.py | 3 +- .../hardware_control/test_ot3_api.py | 6 +- hardware-testing/Makefile | 8 +- .../hardware_testing/gravimetric/config.py | 3 +- .../hardware_testing/liquid_sense/__main__.py | 4 +- .../hardware_testing/liquid_sense/execute.py | 26 +- .../pipette_assembly_qc_ot3/__main__.py | 2 +- .../firmware_bindings/constants.py | 2 + .../hardware_control/motion.py | 6 +- .../hardware_control/move_group_runner.py | 4 +- .../hardware_control/tool_sensors.py | 230 ++++++++++++------ .../hardware_control/test_tool_sensors.py | 31 +-- 21 files changed, 277 insertions(+), 138 deletions(-) diff --git a/api/src/opentrons/config/defaults_ot3.py b/api/src/opentrons/config/defaults_ot3.py index ba4ed09d078..0b2499feaab 100644 --- a/api/src/opentrons/config/defaults_ot3.py +++ b/api/src/opentrons/config/defaults_ot3.py @@ -1,8 +1,8 @@ -from typing import Any, Dict, cast, List, Iterable, Tuple +from typing import Any, Dict, cast, List, Iterable, Tuple, Optional from typing_extensions import Final from dataclasses import asdict -from opentrons.hardware_control.types import OT3AxisKind +from opentrons.hardware_control.types import OT3AxisKind, InstrumentProbeType from .types import ( OT3Config, ByGantryLoad, @@ -34,7 +34,7 @@ aspirate_while_sensing=False, auto_zero_sensor=True, num_baseline_reads=10, - data_file="/var/pressure_sensor_data.csv", + data_files={InstrumentProbeType.PRIMARY: "/data/pressure_sensor_data.csv"}, ) DEFAULT_CALIBRATION_SETTINGS: Final[OT3CalibrationSettings] = OT3CalibrationSettings( @@ -194,6 +194,49 @@ ) +def _build_output_option_with_default( + from_conf: Any, default: OutputOptions +) -> OutputOptions: + if from_conf is None: + return default + else: + if isinstance(from_conf, OutputOptions): + return from_conf + else: + try: + enumval = OutputOptions[from_conf] + except KeyError: # not an enum entry + return default + else: + return enumval + + +def _build_log_files_with_default( + from_conf: Any, + default: Optional[Dict[InstrumentProbeType, str]], +) -> Optional[Dict[InstrumentProbeType, str]]: + print(f"from_conf {from_conf} default {default}") + if not isinstance(from_conf, dict): + if default is None: + return None + else: + return {k: v for k, v in default.items()} + else: + validated: Dict[InstrumentProbeType, str] = {} + for k, v in from_conf.items(): + if isinstance(k, InstrumentProbeType): + validated[k] = v + else: + try: + enumval = InstrumentProbeType[k] + except KeyError: # not an enum entry + pass + else: + validated[enumval] = v + print(f"result {validated}") + return validated + + def _build_dict_with_default( from_conf: Any, default: Dict[OT3AxisKind, float], @@ -278,6 +321,17 @@ def _build_default_cap_pass( def _build_default_liquid_probe( from_conf: Any, default: LiquidProbeSettings ) -> LiquidProbeSettings: + output_option = _build_output_option_with_default( + from_conf.get("output_option", None), default.output_option + ) + data_files: Optional[Dict[InstrumentProbeType, str]] = None + if ( + output_option is OutputOptions.sync_buffer_to_csv + or output_option is OutputOptions.stream_to_csv + ): + data_files = _build_log_files_with_default( + from_conf.get("data_files", {}), default.data_files + ) return LiquidProbeSettings( starting_mount_height=from_conf.get( "starting_mount_height", default.starting_mount_height @@ -302,7 +356,7 @@ def _build_default_liquid_probe( num_baseline_reads=from_conf.get( "num_baseline_reads", default.num_baseline_reads ), - data_file=from_conf.get("data_file", default.data_file), + data_files=data_files, ) @@ -412,7 +466,7 @@ def build_with_defaults(robot_settings: Dict[str, Any]) -> OT3Config: def serialize(config: OT3Config) -> Dict[str, Any]: def _build_dict(pairs: Iterable[Tuple[Any, Any]]) -> Dict[str, Any]: def _normalize_key(key: Any) -> Any: - if isinstance(key, OT3AxisKind): + if isinstance(key, OT3AxisKind) or isinstance(key, InstrumentProbeType): return key.name return key diff --git a/api/src/opentrons/config/types.py b/api/src/opentrons/config/types.py index 0a526ee5336..f13d5a5e6e3 100644 --- a/api/src/opentrons/config/types.py +++ b/api/src/opentrons/config/types.py @@ -2,7 +2,7 @@ from dataclasses import dataclass, asdict, fields from typing import Dict, Tuple, TypeVar, Generic, List, cast, Optional from typing_extensions import TypedDict, Literal -from opentrons.hardware_control.types import OT3AxisKind +from opentrons.hardware_control.types import OT3AxisKind, InstrumentProbeType class AxisDict(TypedDict): @@ -139,7 +139,7 @@ class LiquidProbeSettings: aspirate_while_sensing: bool auto_zero_sensor: bool num_baseline_reads: int - data_file: Optional[str] + data_files: Optional[Dict[InstrumentProbeType, str]] @dataclass(frozen=True) diff --git a/api/src/opentrons/hardware_control/backends/flex_protocol.py b/api/src/opentrons/hardware_control/backends/flex_protocol.py index 1a63ec04f08..53efde79a23 100644 --- a/api/src/opentrons/hardware_control/backends/flex_protocol.py +++ b/api/src/opentrons/hardware_control/backends/flex_protocol.py @@ -147,7 +147,7 @@ async def liquid_probe( plunger_speed: float, threshold_pascals: float, output_format: OutputOptions = OutputOptions.can_bus_only, - data_file: Optional[str] = None, + data_files: Optional[Dict[InstrumentProbeType, str]] = None, auto_zero_sensor: bool = True, num_baseline_reads: int = 10, probe: InstrumentProbeType = InstrumentProbeType.PRIMARY, diff --git a/api/src/opentrons/hardware_control/backends/ot3controller.py b/api/src/opentrons/hardware_control/backends/ot3controller.py index 0edf7e4dfd3..9316fb67e90 100644 --- a/api/src/opentrons/hardware_control/backends/ot3controller.py +++ b/api/src/opentrons/hardware_control/backends/ot3controller.py @@ -1351,7 +1351,7 @@ async def liquid_probe( plunger_speed: float, threshold_pascals: float, output_option: OutputOptions = OutputOptions.can_bus_only, - data_file: Optional[str] = None, + data_files: Optional[Dict[InstrumentProbeType, str]] = None, auto_zero_sensor: bool = True, num_baseline_reads: int = 10, probe: InstrumentProbeType = InstrumentProbeType.PRIMARY, @@ -1372,6 +1372,14 @@ async def liquid_probe( can_bus_only_output = bool( output_option.value & OutputOptions.can_bus_only.value ) + data_files_transposed = ( + None + if data_files is None + else { + sensor_id_for_instrument(probe): data_files[probe] + for probe in data_files.keys() + } + ) positions = await liquid_probe( messenger=self._messenger, tool=tool, @@ -1383,7 +1391,7 @@ async def liquid_probe( csv_output=csv_output, sync_buffer_output=sync_buffer_output, can_bus_only_output=can_bus_only_output, - data_file=data_file, + data_files=data_files_transposed, auto_zero_sensor=auto_zero_sensor, num_baseline_reads=num_baseline_reads, sensor_id=sensor_id_for_instrument(probe), diff --git a/api/src/opentrons/hardware_control/backends/ot3simulator.py b/api/src/opentrons/hardware_control/backends/ot3simulator.py index 741018adc52..b96be54026e 100644 --- a/api/src/opentrons/hardware_control/backends/ot3simulator.py +++ b/api/src/opentrons/hardware_control/backends/ot3simulator.py @@ -346,7 +346,7 @@ async def liquid_probe( plunger_speed: float, threshold_pascals: float, output_format: OutputOptions = OutputOptions.can_bus_only, - data_file: Optional[str] = None, + data_files: Optional[Dict[InstrumentProbeType, str]] = None, auto_zero_sensor: bool = True, num_baseline_reads: int = 10, probe: InstrumentProbeType = InstrumentProbeType.PRIMARY, diff --git a/api/src/opentrons/hardware_control/backends/ot3utils.py b/api/src/opentrons/hardware_control/backends/ot3utils.py index d585a48f99d..a9108c2365e 100644 --- a/api/src/opentrons/hardware_control/backends/ot3utils.py +++ b/api/src/opentrons/hardware_control/backends/ot3utils.py @@ -544,6 +544,7 @@ def sensor_node_for_pipette(mount: OT3Mount) -> PipetteProbeTarget: _instr_sensor_id_lookup: Dict[InstrumentProbeType, SensorId] = { InstrumentProbeType.PRIMARY: SensorId.S0, InstrumentProbeType.SECONDARY: SensorId.S1, + InstrumentProbeType.BOTH: SensorId.BOTH, } diff --git a/api/src/opentrons/hardware_control/ot3api.py b/api/src/opentrons/hardware_control/ot3api.py index 692d1f120e2..93763876575 100644 --- a/api/src/opentrons/hardware_control/ot3api.py +++ b/api/src/opentrons/hardware_control/ot3api.py @@ -2601,7 +2601,7 @@ async def liquid_probe( (probe_settings.plunger_speed * plunger_direction), probe_settings.sensor_threshold_pascals, probe_settings.output_option, - probe_settings.data_file, + probe_settings.data_files, probe_settings.auto_zero_sensor, probe_settings.num_baseline_reads, probe=probe if probe else InstrumentProbeType.PRIMARY, diff --git a/api/src/opentrons/hardware_control/types.py b/api/src/opentrons/hardware_control/types.py index 9a153a447d5..1ea79652f34 100644 --- a/api/src/opentrons/hardware_control/types.py +++ b/api/src/opentrons/hardware_control/types.py @@ -624,6 +624,7 @@ class GripperJawState(enum.Enum): class InstrumentProbeType(enum.Enum): PRIMARY = enum.auto() SECONDARY = enum.auto() + BOTH = enum.auto() class GripperProbe(enum.Enum): diff --git a/api/tests/opentrons/config/ot3_settings.py b/api/tests/opentrons/config/ot3_settings.py index e9f840486af..3cfa9b7c34c 100644 --- a/api/tests/opentrons/config/ot3_settings.py +++ b/api/tests/opentrons/config/ot3_settings.py @@ -129,7 +129,7 @@ "aspirate_while_sensing": False, "auto_zero_sensor": True, "num_baseline_reads": 10, - "data_file": "/var/pressure_sensor_data.csv", + "data_files": {"PRIMARY": "/data/pressure_sensor_data.csv"}, }, "calibration": { "z_offset": { diff --git a/api/tests/opentrons/hardware_control/backends/test_ot3_controller.py b/api/tests/opentrons/hardware_control/backends/test_ot3_controller.py index 12743993d33..ed639444b3d 100644 --- a/api/tests/opentrons/hardware_control/backends/test_ot3_controller.py +++ b/api/tests/opentrons/hardware_control/backends/test_ot3_controller.py @@ -61,6 +61,7 @@ UpdateState, EstopState, CurrentConfig, + InstrumentProbeType, ) from opentrons.hardware_control.errors import ( InvalidPipetteName, @@ -185,7 +186,7 @@ def fake_liquid_settings() -> LiquidProbeSettings: aspirate_while_sensing=False, auto_zero_sensor=False, num_baseline_reads=8, - data_file="fake_data_file", + data_files={InstrumentProbeType.PRIMARY: "fake_file_name"}, ) diff --git a/api/tests/opentrons/hardware_control/test_ot3_api.py b/api/tests/opentrons/hardware_control/test_ot3_api.py index b10628cf99e..7ab0a2f1c00 100644 --- a/api/tests/opentrons/hardware_control/test_ot3_api.py +++ b/api/tests/opentrons/hardware_control/test_ot3_api.py @@ -124,7 +124,7 @@ def fake_liquid_settings() -> LiquidProbeSettings: aspirate_while_sensing=False, auto_zero_sensor=False, num_baseline_reads=10, - data_file="fake_file_name", + data_files={InstrumentProbeType.PRIMARY: "fake_file_name"}, ) @@ -809,7 +809,7 @@ async def test_liquid_probe( aspirate_while_sensing=True, auto_zero_sensor=False, num_baseline_reads=10, - data_file="fake_file_name", + data_files={InstrumentProbeType.PRIMARY: "fake_file_name"}, ) await ot3_hardware.liquid_probe(mount, fake_settings_aspirate) mock_move_to_plunger_bottom.assert_called_once() @@ -820,7 +820,7 @@ async def test_liquid_probe( (fake_settings_aspirate.plunger_speed * -1), fake_settings_aspirate.sensor_threshold_pascals, fake_settings_aspirate.output_option, - fake_settings_aspirate.data_file, + fake_settings_aspirate.data_files, fake_settings_aspirate.auto_zero_sensor, fake_settings_aspirate.num_baseline_reads, probe=InstrumentProbeType.PRIMARY, diff --git a/hardware-testing/Makefile b/hardware-testing/Makefile index a48b794977f..afe2a57c2ee 100755 --- a/hardware-testing/Makefile +++ b/hardware-testing/Makefile @@ -257,9 +257,11 @@ scp $(ssh_helper_ot3) $(4) root@$(1):/tmp/ ssh $(ssh_helper_ot3) root@$(1) \ "function cleanup () { (rm -rf /tmp/$(4) || true) && mount -o remount,ro / ; } ;\ mount -o remount,rw / &&\ -(unzip -o /tmp/$(4) -d /usr/lib/firmware || cleanup) &&\ +(unzip -o /tmp/$(5) -d /usr/lib/firmware || cleanup) &&\ python3 -m json.tool /usr/lib/firmware/opentrons-firmware.json &&\ -cleanup" +cleanup &&\ +echo "Restarting robot server" &&\ +systemctl restart opentrons-robot-server" endef .PHONY: sync-sw-ot3 @@ -284,7 +286,7 @@ remove-patches-fixture: .PHONY: sync-fw-ot3 sync-fw-ot3: - $(call push-and-update-fw,$(host),$(ssh_key),$(ssh_opts),$(zip)) + $(call push-and-update-fw,$(host),$(ssh_key),$(ssh_opts),$(zip),$(notdir $(zip))) .PHONY: sync-ot3 sync-ot3: sync-sw-ot3 sync-fw-ot3 diff --git a/hardware-testing/hardware_testing/gravimetric/config.py b/hardware-testing/hardware_testing/gravimetric/config.py index 993e8716a92..f80d87d7124 100644 --- a/hardware-testing/hardware_testing/gravimetric/config.py +++ b/hardware-testing/hardware_testing/gravimetric/config.py @@ -5,6 +5,7 @@ from enum import Enum from opentrons.config.types import LiquidProbeSettings, OutputOptions from opentrons.protocol_api.labware import Well +from opentrons.hardware_control.types import InstrumentProbeType class ConfigType(Enum): @@ -197,7 +198,7 @@ def _get_liquid_probe_settings( aspirate_while_sensing=False, auto_zero_sensor=True, num_baseline_reads=10, - data_file="/data/testing_data/pressure.csv", + data_files={InstrumentProbeType.PRIMARY: "/data/testing_data/pressure.csv"}, ) diff --git a/hardware-testing/hardware_testing/liquid_sense/__main__.py b/hardware-testing/hardware_testing/liquid_sense/__main__.py index 10db70e67c8..fae4f502315 100644 --- a/hardware-testing/hardware_testing/liquid_sense/__main__.py +++ b/hardware-testing/hardware_testing/liquid_sense/__main__.py @@ -270,6 +270,7 @@ def build_run_args(cls, args: argparse.Namespace) -> "RunArgs": args = parser.parse_args() run_args = RunArgs.build_run_args(args) + exit_error = os.EX_OK try: if not run_args.ctx.is_simulating(): data_dir = get_testing_data_directory() @@ -292,6 +293,7 @@ def build_run_args(cls, args: argparse.Namespace) -> "RunArgs": except Exception as e: ui.print_info(f"got error {e}") ui.print_info(traceback.format_exc()) + exit_error = 1 finally: if run_args.recorder is not None: ui.print_info("ending recording") @@ -314,4 +316,4 @@ def build_run_args(cls, args: argparse.Namespace) -> "RunArgs": run_args.ctx.cleanup() if not args.simulate: helpers_ot3.restart_server_ot3() - os._exit(os.EX_OK) + os._exit(exit_error) diff --git a/hardware-testing/hardware_testing/liquid_sense/execute.py b/hardware-testing/hardware_testing/liquid_sense/execute.py index 1fc95d62d44..9ce6f71b2a8 100644 --- a/hardware-testing/hardware_testing/liquid_sense/execute.py +++ b/hardware-testing/hardware_testing/liquid_sense/execute.py @@ -177,14 +177,15 @@ def run(tip: int, run_args: RunArgs) -> None: run_args.pipette._retract() def _get_baseline() -> float: - run_args.pipette.pick_up_tip(tips.pop(0)) + run_args.pipette.pick_up_tip(tips[0]) + del tips[: run_args.pipette_channels] liquid_height = _jog_to_find_liquid_height( run_args.ctx, run_args.pipette, test_well ) target_height = test_well.bottom(liquid_height).point.z run_args.pipette._retract() - # tip_offset = 0.0 + tip_offset = 0.0 if run_args.dial_indicator is not None: run_args.pipette.move_to(dial_well.top()) tip_offset = run_args.dial_indicator.read_stable() @@ -214,7 +215,8 @@ def _get_baseline() -> float: tip_offset = _get_baseline() ui.print_info(f"Picking up {tip}ul tip") - run_args.pipette.pick_up_tip(tips.pop(0)) + run_args.pipette.pick_up_tip(tips[0]) + del tips[: run_args.pipette_channels] run_args.pipette.move_to(test_well.top()) start_pos = hw_api.current_position_ot3(OT3Mount.LEFT) @@ -274,9 +276,17 @@ def _run_trial(run_args: RunArgs, tip: int, well: Well, trial: int) -> float: run_args.pipette_channels ][tip] data_dir = get_testing_data_directory() - data_filename = f"pressure_sensor_data-trial{trial}-tip{tip}.csv" - data_file = f"{data_dir}/{run_args.name}/{run_args.run_id}/{data_filename}" - ui.print_info(f"logging pressure data to {data_file}") + probes: List[InstrumentProbeType] = [InstrumentProbeType.PRIMARY] + probe_target: InstrumentProbeType = InstrumentProbeType.PRIMARY + if run_args.pipette_channels > 1: + probes.append(InstrumentProbeType.SECONDARY) + probe_target = InstrumentProbeType.BOTH + data_files: Dict[InstrumentProbeType, str] = {} + for probe in probes: + data_filename = f"pressure_sensor_data-trial{trial}-tip{tip}-{probe.name}.csv" + data_file = f"{data_dir}/{run_args.name}/{run_args.run_id}/{data_filename}" + ui.print_info(f"logging pressure data to {data_file}") + data_files[probe] = data_file plunger_speed = ( lqid_cfg["plunger_speed"] @@ -295,13 +305,13 @@ def _run_trial(run_args: RunArgs, tip: int, well: Well, trial: int) -> float: aspirate_while_sensing=run_args.aspirate, auto_zero_sensor=True, num_baseline_reads=10, - data_file=data_file, + data_files=data_files, ) hw_mount = OT3Mount.LEFT if run_args.pipette.mount == "left" else OT3Mount.RIGHT run_args.recorder.set_sample_tag(f"trial-{trial}-{tip}ul") # TODO add in stuff for secondary probe - height = hw_api.liquid_probe(hw_mount, lps, InstrumentProbeType.PRIMARY) + height = hw_api.liquid_probe(hw_mount, lps, probe_target) ui.print_info(f"Trial {trial} complete") run_args.recorder.clear_sample_tag() return height diff --git a/hardware-testing/hardware_testing/production_qc/pipette_assembly_qc_ot3/__main__.py b/hardware-testing/hardware_testing/production_qc/pipette_assembly_qc_ot3/__main__.py index 1ec595974b4..5e482afa6e7 100644 --- a/hardware-testing/hardware_testing/production_qc/pipette_assembly_qc_ot3/__main__.py +++ b/hardware-testing/hardware_testing/production_qc/pipette_assembly_qc_ot3/__main__.py @@ -1386,7 +1386,7 @@ async def _test_liquid_probe( aspirate_while_sensing=False, # FIXME: I heard this doesn't work auto_zero_sensor=True, # TODO: when would we want to adjust this? num_baseline_reads=10, # TODO: when would we want to adjust this? - data_file="", # FIXME: remove + data_files=None, ) end_z = await api.liquid_probe(mount, probe_settings, probe=probe) if probe == InstrumentProbeType.PRIMARY: diff --git a/hardware/opentrons_hardware/firmware_bindings/constants.py b/hardware/opentrons_hardware/firmware_bindings/constants.py index 5c9ec46d806..cd91ced91b7 100644 --- a/hardware/opentrons_hardware/firmware_bindings/constants.py +++ b/hardware/opentrons_hardware/firmware_bindings/constants.py @@ -338,6 +338,8 @@ class SensorId(int, Enum): S0 = 0x0 S1 = 0x1 + UNUSED = 0x2 + BOTH = 0x3 @unique diff --git a/hardware/opentrons_hardware/hardware_control/motion.py b/hardware/opentrons_hardware/hardware_control/motion.py index 5d38a763ca1..4b482cf01a3 100644 --- a/hardware/opentrons_hardware/hardware_control/motion.py +++ b/hardware/opentrons_hardware/hardware_control/motion.py @@ -1,5 +1,5 @@ """A collection of motions that define a single move.""" -from typing import List, Dict, Iterable, Union +from typing import List, Dict, Iterable, Union, Optional from dataclasses import dataclass import numpy as np from logging import getLogger @@ -8,6 +8,7 @@ NodeId, PipetteTipActionType, MoveStopCondition as MoveStopCondition, + SensorId, ) LOG = getLogger(__name__) @@ -52,6 +53,7 @@ class MoveGroupSingleAxisStep: acceleration_mm_sec_sq: np.float64 = np.float64(0) stop_condition: MoveStopCondition = MoveStopCondition.none move_type: MoveType = MoveType.linear + sensor_id: Optional[SensorId] = None def is_moving_step(self) -> bool: """Check if this step involves any actual movement.""" @@ -131,6 +133,7 @@ def create_step( duration: np.float64, present_nodes: Iterable[NodeId], stop_condition: MoveStopCondition = MoveStopCondition.none, + sensor_to_use: Optional[SensorId] = None, ) -> MoveGroupStep: """Create a move from a block. @@ -157,6 +160,7 @@ def create_step( duration_sec=duration, stop_condition=stop_condition, move_type=MoveType.get_move_type(stop_condition), + sensor_id=sensor_to_use, ) return step diff --git a/hardware/opentrons_hardware/hardware_control/move_group_runner.py b/hardware/opentrons_hardware/hardware_control/move_group_runner.py index b5ab03db8fc..4b7f409b38b 100644 --- a/hardware/opentrons_hardware/hardware_control/move_group_runner.py +++ b/hardware/opentrons_hardware/hardware_control/move_group_runner.py @@ -24,7 +24,6 @@ GearMotorId, MoveAckId, MotorDriverErrorCode, - SensorId, ) from opentrons_hardware.drivers.can_bus.can_messenger import CanMessenger from opentrons_hardware.firmware_bindings.messages import MessageDefinition @@ -308,6 +307,7 @@ def _get_stepper_motor_message( return HomeRequest(payload=home_payload) elif step.move_type == MoveType.sensor: # stop_condition = step.stop_condition.value + assert step.sensor_id is not None stop_condition = MoveStopCondition.sync_line sensor_move_payload = AddSensorLinearMoveBasePayload( request_stop_condition=MoveStopConditionField(stop_condition), @@ -328,7 +328,7 @@ def _get_stepper_motor_message( velocity_mm=Int32Field( int((step.velocity_mm_sec / interrupts_per_sec) * (2**31)) ), - sensor_id=SensorIdField(SensorId.S0), + sensor_id=SensorIdField(step.sensor_id), ) return AddSensorLinearMoveRequest(payload=sensor_move_payload) else: diff --git a/hardware/opentrons_hardware/hardware_control/tool_sensors.py b/hardware/opentrons_hardware/hardware_control/tool_sensors.py index 67e85a1554b..ee1bc46c676 100644 --- a/hardware/opentrons_hardware/hardware_control/tool_sensors.py +++ b/hardware/opentrons_hardware/hardware_control/tool_sensors.py @@ -77,6 +77,7 @@ def _build_pass_step( distance: Dict[NodeId, float], speed: Dict[NodeId, float], stop_condition: MoveStopCondition = MoveStopCondition.sync_line, + sensor_to_use: Optional[SensorId] = None, ) -> MoveGroupStep: pipette_nodes = [ i for i in movers if i in [NodeId.pipette_left, NodeId.pipette_right] @@ -105,6 +106,7 @@ def _build_pass_step( duration=float64(abs(distance[movers[0]] / speed[movers[0]])), present_nodes=pipette_nodes, stop_condition=MoveStopCondition.sensor_report, + sensor_to_use=sensor_to_use, ) for node in pipette_nodes: move_group[node] = pipette_move[node] @@ -114,82 +116,176 @@ def _build_pass_step( async def run_sync_buffer_to_csv( messenger: CanMessenger, sensor_driver: SensorDriver, - pressure_sensor: PressureSensor, mount_speed: float, plunger_speed: float, threshold_pascals: float, head_node: NodeId, move_group: MoveGroupRunner, - log_file: str, + log_files: Dict[SensorId, str], tool: PipetteProbeTarget, - sensor_id: SensorId, ) -> Dict[NodeId, MotorPositionStatus]: """Runs the sensor pass move group and creates a csv file with the results.""" sensor_metadata = [0, 0, mount_speed, plunger_speed, threshold_pascals] - sensor_capturer = LogListener( - mount=head_node, - data_file=log_file, - file_heading=pressure_output_file_heading, - sensor_metadata=sensor_metadata, - ) - async with sensor_capturer: - print("starting move group runner") - positions = await move_group.run(can_messenger=messenger) - messenger.add_listener(sensor_capturer, None) + positions = await move_group.run(can_messenger=messenger) + for sensor_id in log_files.keys(): + sensor_capturer = LogListener( + mount=head_node, + data_file=log_files[sensor_id], + file_heading=pressure_output_file_heading, + sensor_metadata=sensor_metadata, + ) + async with sensor_capturer: + messenger.add_listener(sensor_capturer, None) + await messenger.send( + node_id=tool, + message=SendAccumulatedPressureDataRequest( + payload=SendAccumulatedPressureDataPayload( + sensor_id=SensorIdField(sensor_id) + ) + ), + ) + await asyncio.sleep(10) + messenger.remove_listener(sensor_capturer) await messenger.send( node_id=tool, - message=SendAccumulatedPressureDataRequest( - payload=SendAccumulatedPressureDataPayload( - sensor_id=SensorIdField(sensor_id) + message=BindSensorOutputRequest( + payload=BindSensorOutputRequestPayload( + sensor=SensorTypeField(SensorType.pressure), + sensor_id=SensorIdField(sensor_id), + binding=SensorOutputBindingField(SensorOutputBinding.none), ) ), ) - await asyncio.sleep(10) - messenger.remove_listener(sensor_capturer) - await messenger.send( - node_id=tool, - message=BindSensorOutputRequest( - payload=BindSensorOutputRequestPayload( - sensor=SensorTypeField(SensorType.pressure), - sensor_id=SensorIdField(sensor_id), - binding=SensorOutputBindingField(SensorOutputBinding.none), - ) - ), - ) return positions async def run_stream_output_to_csv( messenger: CanMessenger, sensor_driver: SensorDriver, - pressure_sensor: PressureSensor, + pressure_sensors: Dict[SensorId, PressureSensor], mount_speed: float, plunger_speed: float, threshold_pascals: float, head_node: NodeId, move_group: MoveGroupRunner, - log_file: str, + log_files: Dict[SensorId, str], ) -> Dict[NodeId, MotorPositionStatus]: """Runs the sensor pass move group and creates a csv file with the results.""" sensor_metadata = [0, 0, mount_speed, plunger_speed, threshold_pascals] sensor_capturer = LogListener( mount=head_node, - data_file=log_file, + data_file=log_files[ + next(iter(log_files)) + ], # hardcode to the first file, need to think more on this file_heading=pressure_output_file_heading, sensor_metadata=sensor_metadata, ) binding = [SensorOutputBinding.sync, SensorOutputBinding.report] + binding_field = SensorOutputBindingField.from_flags(binding) + for sensor_id in pressure_sensors.keys(): + sensor_info = pressure_sensors[sensor_id].sensor + await messenger.send( + node_id=sensor_info.node_id, + message=BindSensorOutputRequest( + payload=BindSensorOutputRequestPayload( + sensor=SensorTypeField(sensor_info.sensor_type), + sensor_id=SensorIdField(sensor_info.sensor_id), + binding=binding_field, + ) + ), + ) - async with sensor_driver.bind_output(messenger, pressure_sensor, binding): - messenger.add_listener(sensor_capturer, None) - - async with sensor_capturer: - positions = await move_group.run(can_messenger=messenger) - messenger.remove_listener(sensor_capturer) + messenger.add_listener(sensor_capturer, None) + async with sensor_capturer: + positions = await move_group.run(can_messenger=messenger) + messenger.remove_listener(sensor_capturer) + for sensor_id in pressure_sensors.keys(): + sensor_info = pressure_sensors[sensor_id].sensor + await messenger.send( + node_id=sensor_info.node_id, + message=BindSensorOutputRequest( + payload=BindSensorOutputRequestPayload( + sensor=SensorTypeField(sensor_info.sensor_type), + sensor_id=SensorIdField(sensor_info.sensor_id), + binding=SensorOutputBindingField(SensorOutputBinding.none), + ) + ), + ) return positions +async def _setup_pressure_sensors( + messenger: CanMessenger, + sensor_id: SensorId, + tool: PipetteProbeTarget, + num_baseline_reads: int, + threshold_fixed_point: float, + sensor_driver: SensorDriver, + auto_zero_sensor: bool, +) -> Dict[SensorId, PressureSensor]: + sensors: List[SensorId] = [] + result: Dict[SensorId, PressureSensor] = {} + if sensor_id == SensorId.BOTH: + sensors.append(SensorId.S0) + sensors.append(SensorId.S1) + else: + sensors.append(sensor_id) + + for sensor in sensors: + pressure_sensor = PressureSensor.build( + sensor_id=sensor_id, + node_id=tool, + stop_threshold=threshold_fixed_point, + ) + + if auto_zero_sensor: + pressure_baseline = await sensor_driver.get_baseline( + messenger, pressure_sensor, num_baseline_reads + ) + LOG.debug(f"found baseline pressure: {pressure_baseline} pascals") + + await sensor_driver.send_stop_threshold(messenger, pressure_sensor) + result[sensor] = pressure_sensor + return result + + +async def _run_with_binding( + messenger: CanMessenger, + pressure_sensors: Dict[SensorId, PressureSensor], + sensor_runner: MoveGroupRunner, + binding: List[SensorOutputBinding], +) -> Dict[NodeId, MotorPositionStatus]: + binding_field = SensorOutputBindingField.from_flags(binding) + for sensor_id in pressure_sensors.keys(): + sensor_info = pressure_sensors[sensor_id].sensor + await messenger.send( + node_id=sensor_info.node_id, + message=BindSensorOutputRequest( + payload=BindSensorOutputRequestPayload( + sensor=SensorTypeField(sensor_info.sensor_type), + sensor_id=SensorIdField(sensor_info.sensor_id), + binding=binding_field, + ) + ), + ) + + result = await sensor_runner.run(can_messenger=messenger) + for sensor_id in pressure_sensors.keys(): + sensor_info = pressure_sensors[sensor_id].sensor + await messenger.send( + node_id=sensor_info.node_id, + message=BindSensorOutputRequest( + payload=BindSensorOutputRequestPayload( + sensor=SensorTypeField(sensor_info.sensor_type), + sensor_id=SensorIdField(sensor_info.sensor_id), + binding=SensorOutputBindingField(SensorOutputBinding.none), + ) + ), + ) + return result + + async def liquid_probe( messenger: CanMessenger, tool: PipetteProbeTarget, @@ -201,82 +297,68 @@ async def liquid_probe( csv_output: bool = False, sync_buffer_output: bool = False, can_bus_only_output: bool = False, - data_file: Optional[str] = None, + data_files: Optional[Dict[SensorId, str]] = None, auto_zero_sensor: bool = True, num_baseline_reads: int = 10, sensor_id: SensorId = SensorId.S0, ) -> Dict[NodeId, MotorPositionStatus]: """Move the mount and pipette simultaneously while reading from the pressure sensor.""" + log_files: Dict[SensorId, str] = {} if not data_files else data_files sensor_driver = SensorDriver() threshold_fixed_point = threshold_pascals * sensor_fixed_point_conversion - pressure_sensor = PressureSensor.build( - sensor_id=sensor_id, - node_id=tool, - stop_threshold=threshold_fixed_point, + pressure_sensors = await _setup_pressure_sensors( + messenger, + sensor_id, + tool, + num_baseline_reads, + threshold_fixed_point, + sensor_driver, + auto_zero_sensor, ) - if auto_zero_sensor: - pressure_baseline = await sensor_driver.get_baseline( - messenger, pressure_sensor, num_baseline_reads - ) - LOG.debug(f"found baseline pressure: {pressure_baseline} pascals") - - await sensor_driver.send_stop_threshold(messenger, pressure_sensor) - sensor_group = _build_pass_step( movers=[head_node, tool], distance={head_node: max_z_distance, tool: max_z_distance}, speed={head_node: mount_speed, tool: plunger_speed}, stop_condition=MoveStopCondition.sync_line, + sensor_to_use=sensor_id, ) sensor_runner = MoveGroupRunner(move_groups=[[sensor_group]]) - log_file: str = "/data/pressure_sensor_data.csv" if not data_file else data_file if csv_output: return await run_stream_output_to_csv( messenger, sensor_driver, - pressure_sensor, + pressure_sensors, mount_speed, plunger_speed, threshold_pascals, head_node, sensor_runner, - log_file, + log_files, ) elif sync_buffer_output: return await run_sync_buffer_to_csv( messenger, sensor_driver, - pressure_sensor, mount_speed, plunger_speed, threshold_pascals, head_node, sensor_runner, - log_file, - tool=tool, - sensor_id=sensor_id, + log_files, + tool, ) elif can_bus_only_output: - async with sensor_driver.bind_output( - messenger, - pressure_sensor, - [ - SensorOutputBinding.sync, - SensorOutputBinding.report, - ], - ): - return await sensor_runner.run(can_messenger=messenger) + binding = [SensorOutputBinding.sync, SensorOutputBinding.report] + return await _run_with_binding( + messenger, pressure_sensors, sensor_runner, binding + ) else: # none - async with sensor_driver.bind_output( - messenger, - pressure_sensor, - [ - SensorOutputBinding.sync, - ], - ): - return await sensor_runner.run(can_messenger=messenger) + binding = [SensorOutputBinding.sync] + return await _run_with_binding( + messenger, pressure_sensors, sensor_runner, binding + ) async def check_overpressure( diff --git a/hardware/tests/opentrons_hardware/hardware_control/test_tool_sensors.py b/hardware/tests/opentrons_hardware/hardware_control/test_tool_sensors.py index 5db17d16cb4..ba391da2c14 100644 --- a/hardware/tests/opentrons_hardware/hardware_control/test_tool_sensors.py +++ b/hardware/tests/opentrons_hardware/hardware_control/test_tool_sensors.py @@ -50,7 +50,7 @@ SensorOutputBinding, ) from opentrons_hardware.sensors.scheduler import SensorScheduler -from opentrons_hardware.sensors.sensor_driver import LogListener, SensorDriver +from opentrons_hardware.sensors.sensor_driver import SensorDriver from opentrons_hardware.sensors.types import SensorDataType from opentrons_hardware.sensors.sensor_types import SensorInformation from opentrons_hardware.sensors.utils import SensorThresholdInformation @@ -193,35 +193,6 @@ def move_responder( data=SensorDataType.build(threshold_pascals * 65536, sensor_info.sensor_type), mode=SensorThresholdMode.absolute, ) - mock_bind_output.assert_called_once() - assert mock_bind_output.call_args_list[0][0][3] == [SensorOutputBinding.sync] - - with patch( - "opentrons_hardware.hardware_control.tool_sensors", LogListener - ) as mock_log: - - mock_log.__aenter__ = AsyncMock(return_value=mock_log) # type: ignore - mock_log.__aexit__ = AsyncMock(return_value=None) # type: ignore - - await liquid_probe( - messenger=mock_messenger, - tool=target_node, - head_node=motor_node, - max_z_distance=40, - mount_speed=10, - plunger_speed=8, - threshold_pascals=threshold_pascals, - csv_output=False, - sync_buffer_output=False, - can_bus_only_output=False, - auto_zero_sensor=True, - num_baseline_reads=8, - sensor_id=SensorId.S0, - ) - mock_bind_output.assert_called() - assert mock_bind_output.call_args_list[1][0][3] == [ - SensorOutputBinding.sync, - ] @pytest.mark.parametrize( From 2d5712601c3167704fe50d779a7250d57060a39c Mon Sep 17 00:00:00 2001 From: TamarZanzouri Date: Mon, 22 Apr 2024 15:44:43 -0400 Subject: [PATCH 185/194] feature(api, robot-server): Allow fixit commands to recover from an error (#14908) --- .../protocol_engine/actions/actions.py | 1 + .../protocol_engine/commands/__init__.py | 4 +- .../protocol_engine/commands/command.py | 7 ++ .../commands/hash_command_params.py | 17 ++- .../protocol_engine/errors/__init__.py | 7 +- .../protocol_engine/errors/exceptions.py | 39 ++++++ .../protocol_engine/protocol_engine.py | 28 ++++- .../protocol_engine/state/command_history.py | 23 ++++ .../protocol_engine/state/commands.py | 59 +++++++-- .../commands/test_hash_command_params.py | 33 +++-- .../protocol_engine/state/command_fixtures.py | 2 + .../state/test_command_history.py | 73 +++++++++++ .../state/test_command_store_old.py | 26 ++-- .../state/test_command_view_old.py | 108 ++++++++++++++-- .../protocol_engine/test_protocol_engine.py | 118 +++++++++++++++++- .../runs/router/commands_router.py | 35 +++++- .../tests/runs/router/test_commands_router.py | 42 ++++++- shared-data/command/schemas/8.json | 2 +- 18 files changed, 551 insertions(+), 73 deletions(-) diff --git a/api/src/opentrons/protocol_engine/actions/actions.py b/api/src/opentrons/protocol_engine/actions/actions.py index 2d46f614ec3..adcf4f9e40b 100644 --- a/api/src/opentrons/protocol_engine/actions/actions.py +++ b/api/src/opentrons/protocol_engine/actions/actions.py @@ -116,6 +116,7 @@ class QueueCommandAction: created_at: datetime request: CommandCreate request_hash: Optional[str] + failed_command_id: Optional[str] = None @dataclass(frozen=True) diff --git a/api/src/opentrons/protocol_engine/commands/__init__.py b/api/src/opentrons/protocol_engine/commands/__init__.py index 3dfe6eaf51f..7ce6e07eb68 100644 --- a/api/src/opentrons/protocol_engine/commands/__init__.py +++ b/api/src/opentrons/protocol_engine/commands/__init__.py @@ -19,7 +19,7 @@ from . import thermocycler from . import calibration -from .hash_command_params import hash_command_params +from .hash_command_params import hash_protocol_command_params from .generate_command_schema import generate_command_schema from .command import ( @@ -333,7 +333,7 @@ "CommandStatus", "CommandIntent", # command parameter hashing - "hash_command_params", + "hash_protocol_command_params", # command schema generation "generate_command_schema", # aspirate command models diff --git a/api/src/opentrons/protocol_engine/commands/command.py b/api/src/opentrons/protocol_engine/commands/command.py index 5c2ab46b06f..ad43128236d 100644 --- a/api/src/opentrons/protocol_engine/commands/command.py +++ b/api/src/opentrons/protocol_engine/commands/command.py @@ -55,6 +55,7 @@ class CommandIntent(str, Enum): PROTOCOL = "protocol" SETUP = "setup" + FIXIT = "fixit" class BaseCommandCreate(GenericModel, Generic[CommandParamsT]): @@ -159,6 +160,12 @@ class BaseCommand(GenericModel, Generic[CommandParamsT, CommandResultT]): " the command's execution or the command's generation." ), ) + failedCommandId: Optional[str] = Field( + None, + description=( + "FIXIT command use only. Reference of the failed command id we are trying to fix." + ), + ) class AbstractCommandImpl( diff --git a/api/src/opentrons/protocol_engine/commands/hash_command_params.py b/api/src/opentrons/protocol_engine/commands/hash_command_params.py index 39a042e55dd..9b927aab014 100644 --- a/api/src/opentrons/protocol_engine/commands/hash_command_params.py +++ b/api/src/opentrons/protocol_engine/commands/hash_command_params.py @@ -9,7 +9,7 @@ # TODO(mm, 2023-04-28): # This implementation will not notice that commands are different if they have different params # but share the same commandType. We should also hash command params. (Jira RCORE-326.) -def hash_command_params( +def hash_protocol_command_params( create: CommandCreate, last_hash: Optional[str] ) -> Optional[str]: """Given a command create object, return a hash. @@ -28,12 +28,11 @@ def hash_command_params( The command hash, if the command is a protocol command. `None` if the command is a setup command. """ - if create.intent == CommandIntent.SETUP: + if create.intent != CommandIntent.PROTOCOL: return None - else: - # We avoid Python's built-in hash() function because it's not stable across - # runs of the Python interpreter. (Jira RSS-215.) - last_contribution = b"" if last_hash is None else last_hash.encode("ascii") - this_contribution = md5(create.commandType.encode("ascii")).digest() - to_hash = last_contribution + this_contribution - return md5(to_hash).hexdigest() + # We avoid Python's built-in hash() function because it's not stable across + # runs of the Python interpreter. (Jira RSS-215.) + last_contribution = b"" if last_hash is None else last_hash.encode("ascii") + this_contribution = md5(create.commandType.encode("ascii")).digest() + to_hash = last_contribution + this_contribution + return md5(to_hash).hexdigest() diff --git a/api/src/opentrons/protocol_engine/errors/__init__.py b/api/src/opentrons/protocol_engine/errors/__init__.py index d3c3bb6d79e..994e4cc9ed3 100644 --- a/api/src/opentrons/protocol_engine/errors/__init__.py +++ b/api/src/opentrons/protocol_engine/errors/__init__.py @@ -39,6 +39,7 @@ MustHomeError, RunStoppedError, SetupCommandNotAllowedError, + FixitCommandNotAllowedError, ModuleNotAttachedError, ModuleAlreadyPresentError, WrongModuleTypeError, @@ -55,6 +56,7 @@ InvalidHoldTimeError, CannotPerformModuleAction, PauseNotAllowedError, + ResumeFromRecoveryNotAllowedError, GripperNotAttachedError, CannotPerformGripperAction, HardwareNotSupportedError, @@ -65,6 +67,7 @@ LocationIsStagingSlotError, InvalidAxisForRobotType, NotSupportedOnRobotType, + CommandNotAllowedError, ) from .error_occurrence import ErrorOccurrence, ProtocolCommandFailedError @@ -109,6 +112,7 @@ "MustHomeError", "RunStoppedError", "SetupCommandNotAllowedError", + "FixitCommandNotAllowedError", "ModuleNotAttachedError", "ModuleAlreadyPresentError", "WrongModuleTypeError", @@ -124,6 +128,7 @@ "InvalidBlockVolumeError", "InvalidHoldTimeError", "CannotPerformModuleAction", + "ResumeFromRecoveryNotAllowedError", "PauseNotAllowedError", "ProtocolCommandFailedError", "GripperNotAttachedError", @@ -138,5 +143,5 @@ "NotSupportedOnRobotType", # error occurrence models "ErrorOccurrence", - "FailedGripperPickupError", + "CommandNotAllowedError", ] diff --git a/api/src/opentrons/protocol_engine/errors/exceptions.py b/api/src/opentrons/protocol_engine/errors/exceptions.py index 0e27a270c94..7f022652d71 100644 --- a/api/src/opentrons/protocol_engine/errors/exceptions.py +++ b/api/src/opentrons/protocol_engine/errors/exceptions.py @@ -505,6 +505,32 @@ def __init__( super().__init__(ErrorCodes.POSITION_UNKNOWN, message, details, wrapping) +class CommandNotAllowedError(ProtocolEngineError): + """Raised when adding a command with bad data.""" + + def __init__( + self, + message: Optional[str] = None, + details: Optional[Dict[str, Any]] = None, + wrapping: Optional[Sequence[EnumeratedError]] = None, + ) -> None: + """Build a CommandNotAllowedError.""" + super().__init__(ErrorCodes.GENERAL_ERROR, message, details, wrapping) + + +class FixitCommandNotAllowedError(ProtocolEngineError): + """Raised when adding a fixit command to a non-recoverable engine.""" + + def __init__( + self, + message: Optional[str] = None, + details: Optional[Dict[str, Any]] = None, + wrapping: Optional[Sequence[EnumeratedError]] = None, + ) -> None: + """Build a SetupCommandNotAllowedError.""" + super().__init__(ErrorCodes.GENERAL_ERROR, message, details, wrapping) + + class SetupCommandNotAllowedError(ProtocolEngineError): """Raised when adding a setup command to a non-idle/non-paused engine.""" @@ -518,6 +544,19 @@ def __init__( super().__init__(ErrorCodes.GENERAL_ERROR, message, details, wrapping) +class ResumeFromRecoveryNotAllowedError(ProtocolEngineError): + """Raised when attempting to resume a run from recovery that has a fixit command in the queue.""" + + def __init__( + self, + message: Optional[str] = None, + details: Optional[Dict[str, Any]] = None, + wrapping: Optional[Sequence[EnumeratedError]] = None, + ) -> None: + """Build a ResumeFromRecoveryNotAllowedError.""" + super().__init__(ErrorCodes.GENERAL_ERROR, message, details, wrapping) + + class PauseNotAllowedError(ProtocolEngineError): """Raised when attempting to pause a run that is not running.""" diff --git a/api/src/opentrons/protocol_engine/protocol_engine.py b/api/src/opentrons/protocol_engine/protocol_engine.py index 8bb4c91dda3..0c4f2c4b670 100644 --- a/api/src/opentrons/protocol_engine/protocol_engine.py +++ b/api/src/opentrons/protocol_engine/protocol_engine.py @@ -17,7 +17,7 @@ EnumeratedError, ) -from .errors import ProtocolCommandFailedError, ErrorOccurrence +from .errors import ProtocolCommandFailedError, ErrorOccurrence, CommandNotAllowedError from .errors.exceptions import EStopActivatedError from . import commands, slot_standardization from .resources import ModelUtils, ModuleDataProvider @@ -176,7 +176,9 @@ def resume_from_recovery(self) -> None: ) self._action_dispatcher.dispatch(action) - def add_command(self, request: commands.CommandCreate) -> commands.Command: + def add_command( + self, request: commands.CommandCreate, failed_command_id: Optional[str] = None + ) -> commands.Command: """Add a command to the `ProtocolEngine`'s queue. Arguments: @@ -191,16 +193,29 @@ def add_command(self, request: commands.CommandCreate) -> commands.Command: but the engine was not idle or paused. RunStoppedError: the run has been stopped, so no new commands may be added. + CommandNotAllowedError: the request specified a failed command id + with a non fixit command. """ request = slot_standardization.standardize_command( request, self.state_view.config.robot_type ) + if failed_command_id and request.intent != commands.CommandIntent.FIXIT: + raise CommandNotAllowedError( + "failed command id should be supplied with a FIXIT command." + ) + command_id = self._model_utils.generate_id() - request_hash = commands.hash_command_params( - create=request, - last_hash=self._state_store.commands.get_latest_command_hash(), - ) + if request.intent in ( + commands.CommandIntent.SETUP, + commands.CommandIntent.FIXIT, + ): + request_hash = None + else: + request_hash = commands.hash_protocol_command_params( + create=request, + last_hash=self._state_store.commands.get_latest_protocol_command_hash(), + ) action = self.state_view.commands.validate_action_allowed( QueueCommandAction( @@ -208,6 +223,7 @@ def add_command(self, request: commands.CommandCreate) -> commands.Command: request_hash=request_hash, command_id=command_id, created_at=self._model_utils.get_timestamp(), + failed_command_id=failed_command_id, ) ) self._action_dispatcher.dispatch(action) diff --git a/api/src/opentrons/protocol_engine/state/command_history.py b/api/src/opentrons/protocol_engine/state/command_history.py index 6a66a2b8209..b21fca030ae 100644 --- a/api/src/opentrons/protocol_engine/state/command_history.py +++ b/api/src/opentrons/protocol_engine/state/command_history.py @@ -33,6 +33,9 @@ class CommandHistory: _queued_setup_command_ids: OrderedSet[str] """The IDs of queued setup commands, in FIFO order""" + _queued_fixit_command_ids: OrderedSet[str] + """The IDs of queued fixit commands, in FIFO order""" + _running_command_id: Optional[str] """The ID of the currently running command, if any""" @@ -43,6 +46,7 @@ def __init__(self) -> None: self._all_command_ids = [] self._queued_command_ids = OrderedSet() self._queued_setup_command_ids = OrderedSet() + self._queued_fixit_command_ids = OrderedSet() self._commands_by_id = OrderedDict() self._running_command_id = None self._terminal_command_id = None @@ -135,6 +139,10 @@ def get_setup_queue_ids(self) -> OrderedSet[str]: """Get the IDs of all queued setup commands, in FIFO order.""" return self._queued_setup_command_ids + def get_fixit_queue_ids(self) -> OrderedSet[str]: + """Get the IDs of all queued fixit commands, in FIFO order.""" + return self._queued_fixit_command_ids + def clear_queue(self) -> None: """Clears all commands within the queued command ids structure.""" self._queued_command_ids.clear() @@ -143,6 +151,10 @@ def clear_setup_queue(self) -> None: """Clears all commands within the queued setup command ids structure.""" self._queued_setup_command_ids.clear() + def clear_fixit_queue(self) -> None: + """Clears all commands within the queued setup command ids structure.""" + self._queued_fixit_command_ids.clear() + def set_command_queued(self, command: Command) -> None: """Validate and mark a command as queued in the command history.""" assert command.status == CommandStatus.QUEUED @@ -157,6 +169,8 @@ def set_command_queued(self, command: Command) -> None: if command.intent == CommandIntent.SETUP: self._add_to_setup_queue(command.id) + elif command.intent == CommandIntent.FIXIT: + self._add_to_fixit_queue(command.id) else: self._add_to_queue(command.id) @@ -177,6 +191,7 @@ def set_command_running(self, command: Command) -> None: self._remove_queue_id(command.id) self._remove_setup_queue_id(command.id) + self._remove_fixit_queue_id(command.id) def set_command_succeeded(self, command: Command) -> None: """Validate and mark a command as succeeded in the command history.""" @@ -239,6 +254,10 @@ def _add_to_setup_queue(self, command_id: str) -> None: """Add a new ID to the queued setup.""" self._queued_setup_command_ids.add(command_id) + def _add_to_fixit_queue(self, command_id: str) -> None: + """Add a new ID to the queued fixit.""" + self._queued_fixit_command_ids.add(command_id) + def _remove_queue_id(self, command_id: str) -> None: """Remove a specific command from the queued command ids structure.""" self._queued_command_ids.discard(command_id) @@ -247,6 +266,10 @@ def _remove_setup_queue_id(self, command_id: str) -> None: """Remove a specific command from the queued setup command ids structure.""" self._queued_setup_command_ids.discard(command_id) + def _remove_fixit_queue_id(self, command_id: str) -> None: + """Remove a specific command from the queued fixit command ids structure.""" + self._queued_fixit_command_ids.discard(command_id) + def _set_terminal_command_id(self, command_id: str) -> None: """Set the ID of the most recently dequeued command.""" self._terminal_command_id = command_id diff --git a/api/src/opentrons/protocol_engine/state/commands.py b/api/src/opentrons/protocol_engine/state/commands.py index b5805251046..f9d7643b728 100644 --- a/api/src/opentrons/protocol_engine/state/commands.py +++ b/api/src/opentrons/protocol_engine/state/commands.py @@ -38,6 +38,8 @@ ErrorOccurrence, RobotDoorOpenError, SetupCommandNotAllowedError, + FixitCommandNotAllowedError, + ResumeFromRecoveryNotAllowedError, PauseNotAllowedError, UnexpectedProtocolError, ProtocolCommandFailedError, @@ -184,8 +186,8 @@ class CommandState: finish_error: Optional[ErrorOccurrence] """The error that happened during the post-run finish steps (homing & dropping tips), if any.""" - latest_command_hash: Optional[str] - """The latest hash value received in a QueueCommandAction. + latest_protocol_command_hash: Optional[str] + """The latest PROTOCOL command hash value received in a QueueCommandAction. This value can be used to generate future hashes. """ @@ -219,7 +221,7 @@ def __init__( recovery_target_command_id=None, run_completed_at=None, run_started_at=None, - latest_command_hash=None, + latest_protocol_command_hash=None, stopped_by_estop=False, ) @@ -241,12 +243,13 @@ def handle_action(self, action: Action) -> None: # noqa: C901 params=action.request.params, # type: ignore[arg-type] intent=action.request.intent, status=CommandStatus.QUEUED, + failedCommandId=action.failed_command_id, ) self._state.command_history.set_command_queued(queued_command) if action.request_hash is not None: - self._state.latest_command_hash = action.request_hash + self._state.latest_protocol_command_hash = action.request_hash elif isinstance(action, RunCommandAction): prev_entry = self._state.command_history.get(action.command_id) @@ -321,6 +324,20 @@ def handle_action(self, action: Action) -> None: # noqa: C901 self._state.command_history.clear_queue() else: assert_never(action.type) + elif prev_entry.command.intent == CommandIntent.FIXIT: + other_command_ids_to_fail = ( + self._state.command_history.get_fixit_queue_ids() + ) + for command_id in other_command_ids_to_fail: + # TODO(mc, 2022-06-06): add new "cancelled" status or similar + self._update_to_failed( + command_id=command_id, + failed_at=action.failed_at, + error_occurrence=None, + error_recovery_type=None, + notes=None, + ) + self._state.command_history.clear_fixit_queue() else: assert_never(prev_entry.command.intent) @@ -339,6 +356,7 @@ def handle_action(self, action: Action) -> None: # noqa: C901 self._state.queue_status = QueueStatus.PAUSED elif isinstance(action, ResumeFromRecoveryAction): + self._state.command_history.clear_fixit_queue() self._state.queue_status = QueueStatus.RUNNING self._state.recovery_target_command_id = None @@ -606,9 +624,18 @@ def get_next_to_execute(self) -> Optional[str]: if self._state.run_result: raise RunStoppedError("Engine was stopped") + # if queue is in recovery mode, return the next fixit command + next_fixit_cmd = self._state.command_history.get_fixit_queue_ids().head(None) + if next_fixit_cmd and self._state.queue_status == QueueStatus.AWAITING_RECOVERY: + return next_fixit_cmd + # if there is a setup command queued, prioritize it next_setup_cmd = self._state.command_history.get_setup_queue_ids().head(None) - if self._state.queue_status != QueueStatus.PAUSED and next_setup_cmd: + if ( + self._state.queue_status + not in [QueueStatus.PAUSED, QueueStatus.AWAITING_RECOVERY] + and next_setup_cmd + ): return next_setup_cmd # if the queue is running, return the next protocol command @@ -816,12 +843,28 @@ def validate_action_allowed( # noqa: C901 raise SetupCommandNotAllowedError( "Setup commands are not allowed after run has started." ) + elif action.request.intent == CommandIntent.FIXIT: + if self._state.queue_status != QueueStatus.AWAITING_RECOVERY: + raise FixitCommandNotAllowedError( + "Fixit commands are not allowed when the run is not in a recoverable state." + ) + else: + return action else: return action elif isinstance(action, ResumeFromRecoveryAction): if self.get_status() != EngineStatus.AWAITING_RECOVERY: - raise NotImplementedError() + raise ResumeFromRecoveryNotAllowedError( + "Cannot resume from recovery if the run is not in recovery mode." + ) + elif ( + self.get_status() == EngineStatus.AWAITING_RECOVERY + and len(self._state.command_history.get_fixit_queue_ids()) > 0 + ): + raise ResumeFromRecoveryNotAllowedError( + "Cannot resume from recovery while there are fixit commands in the queue." + ) else: return action @@ -873,6 +916,6 @@ def get_status(self) -> EngineStatus: # SETUP and we're currently a setup command? return EngineStatus.IDLE - def get_latest_command_hash(self) -> Optional[str]: + def get_latest_protocol_command_hash(self) -> Optional[str]: """Get the command hash of the last queued command, if any.""" - return self._state.latest_command_hash + return self._state.latest_protocol_command_hash diff --git a/api/tests/opentrons/protocol_engine/commands/test_hash_command_params.py b/api/tests/opentrons/protocol_engine/commands/test_hash_command_params.py index 098ce53c321..9988854a9d4 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_hash_command_params.py +++ b/api/tests/opentrons/protocol_engine/commands/test_hash_command_params.py @@ -2,7 +2,9 @@ from opentrons.protocol_engine import CommandIntent from opentrons.protocol_engine import commands -from opentrons.protocol_engine.commands.hash_command_params import hash_command_params +from opentrons.protocol_engine.commands.hash_command_params import ( + hash_protocol_command_params, +) def test_equivalent_commands() -> None: @@ -20,10 +22,14 @@ def test_equivalent_commands() -> None: params=commands.WaitForDurationParams(seconds=123) ) - assert hash_command_params(b, None) == hash_command_params(c, None) + assert hash_protocol_command_params(b, None) == hash_protocol_command_params( + c, None + ) - a_hash = hash_command_params(a, None) - assert hash_command_params(b, a_hash) == hash_command_params(c, a_hash) + a_hash = hash_protocol_command_params(a, None) + assert hash_protocol_command_params(b, a_hash) == hash_protocol_command_params( + c, a_hash + ) def test_nonequivalent_commands() -> None: @@ -32,26 +38,31 @@ def test_nonequivalent_commands() -> None: params=commands.BlowOutInPlaceParams( pipetteId="abc123", flowRate=123, - ) + ), + intent=CommandIntent.PROTOCOL, ) b = commands.WaitForDurationCreate( params=commands.WaitForDurationParams(seconds=123) ) - assert hash_command_params(a, None) != hash_command_params(b, None) + assert hash_protocol_command_params(a, None) != hash_protocol_command_params( + b, None + ) def test_repeated_commands() -> None: """Repeated commands should hash differently, even though they're equivalent in isolation.""" a = commands.WaitForDurationCreate( - params=commands.WaitForDurationParams(seconds=123) + params=commands.WaitForDurationParams(seconds=123), + intent=CommandIntent.PROTOCOL, ) b = commands.WaitForDurationCreate( - params=commands.WaitForDurationParams(seconds=123) + params=commands.WaitForDurationParams(seconds=123), + intent=CommandIntent.PROTOCOL, ) - a_hash = hash_command_params(a, None) - b_hash = hash_command_params(b, a_hash) + a_hash = hash_protocol_command_params(a, None) + b_hash = hash_protocol_command_params(b, a_hash) assert a_hash != b_hash @@ -61,4 +72,4 @@ def test_setup_command() -> None: params=commands.WaitForDurationParams(seconds=123), intent=CommandIntent.SETUP, ) - assert hash_command_params(setup_command, None) is None + assert hash_protocol_command_params(setup_command, None) is None diff --git a/api/tests/opentrons/protocol_engine/state/command_fixtures.py b/api/tests/opentrons/protocol_engine/state/command_fixtures.py index 191dd49bd48..b8b47648b3a 100644 --- a/api/tests/opentrons/protocol_engine/state/command_fixtures.py +++ b/api/tests/opentrons/protocol_engine/state/command_fixtures.py @@ -24,6 +24,7 @@ def create_queued_command( command_id: str = "command-id", command_key: str = "command-key", command_type: str = "command-type", + intent: cmd.CommandIntent = cmd.CommandIntent.PROTOCOL, params: Optional[BaseModel] = None, ) -> cmd.Command: """Given command data, build a pending command model.""" @@ -36,6 +37,7 @@ def create_queued_command( createdAt=datetime(year=2021, month=1, day=1), status=cmd.CommandStatus.QUEUED, params=params or BaseModel(), + intent=intent, ), ) diff --git a/api/tests/opentrons/protocol_engine/state/test_command_history.py b/api/tests/opentrons/protocol_engine/state/test_command_history.py index c6344141281..3c84b86e07f 100644 --- a/api/tests/opentrons/protocol_engine/state/test_command_history.py +++ b/api/tests/opentrons/protocol_engine/state/test_command_history.py @@ -5,6 +5,7 @@ from opentrons.protocol_engine.errors.exceptions import CommandDoesNotExistError from opentrons.protocol_engine.state.command_history import CommandHistory, CommandEntry +from opentrons.protocol_engine.commands import CommandIntent, CommandStatus from .command_fixtures import ( create_queued_command, @@ -18,6 +19,15 @@ def create_queued_command_entry( return CommandEntry(create_queued_command(command_id=command_id), index) +def create_fixit_command_entry( + command_id: str = "command-id", index: int = 0 +) -> CommandEntry: + """Create a command entry for a fixit command.""" + return CommandEntry( + create_queued_command(command_id=command_id, intent=CommandIntent.FIXIT), index + ) + + @pytest.fixture def command_history() -> CommandHistory: """Instantiates a CommandHistory instance.""" @@ -161,6 +171,14 @@ def test_get_setup_queue_ids(command_history: CommandHistory) -> None: assert command_history.get_setup_queue_ids() == OrderedSet(["0", "1"]) +def test_get_fixit_queue_ids(command_history: CommandHistory) -> None: + """It should return the IDs of all commands in the setup queue.""" + assert command_history.get_fixit_queue_ids() == OrderedSet() + command_history._add_to_fixit_queue("0") + command_history._add_to_fixit_queue("1") + assert command_history.get_fixit_queue_ids() == OrderedSet(["0", "1"]) + + def test_set_command_entry(command_history: CommandHistory) -> None: """It should set the command entry for the given ID.""" command_entry = create_queued_command_entry() @@ -184,6 +202,41 @@ def test_set_running_command_id(command_history: CommandHistory) -> None: assert command_history.get_running_command() == command_entry +def test_set_fixit_running_command_id(command_history: CommandHistory) -> None: + """It should set the ID of the currently running fixit command.""" + command_entry = create_queued_command() + command_history.set_command_queued(command_entry) + running_command = command_entry.copy( + update={ + "status": CommandStatus.RUNNING, + } + ) + command_history.set_command_running(running_command) + finished_command = command_entry.copy( + update={ + "status": CommandStatus.SUCCEEDED, + } + ) + command_history.set_command_succeeded(finished_command) + fixit_command_entry = create_queued_command( + command_id="fixit-id", intent=CommandIntent.FIXIT + ) + command_history.set_command_queued(fixit_command_entry) + fixit_running_command = fixit_command_entry.copy( + update={ + "status": CommandStatus.RUNNING, + } + ) + command_history.set_command_running(fixit_running_command) + current_running_command = command_history.get_running_command() + assert current_running_command is not None + assert current_running_command.command == fixit_running_command + assert command_history.get_all_commands() == [ + finished_command, + fixit_running_command, + ] + + def test_add_to_queue(command_history: CommandHistory) -> None: """It should add the given ID to the queue.""" command_history._add_to_queue("0") @@ -196,6 +249,13 @@ def test_add_to_setup_queue(command_history: CommandHistory) -> None: assert command_history.get_setup_queue_ids() == OrderedSet(["0"]) +def test_add_to_fixit_queue(command_history: CommandHistory) -> None: + """It should add the given ID to the setup queue.""" + fixit_command = create_queued_command(intent=CommandIntent.FIXIT) + command_history.set_command_queued(fixit_command) + assert command_history.get_fixit_queue_ids() == OrderedSet(["command-id"]) + + def test_clear_queue(command_history: CommandHistory) -> None: """It should clear all commands in the queue.""" command_history._add_to_queue("0") @@ -212,6 +272,19 @@ def test_clear_setup_queue(command_history: CommandHistory) -> None: assert command_history.get_setup_queue_ids() == OrderedSet() +def test_clear_fixit_queue(command_history: CommandHistory) -> None: + """It should clear all commands in the setup queue.""" + command_history.set_command_queued( + create_queued_command(command_id="0", intent=CommandIntent.FIXIT) + ) + command_history.set_command_queued( + create_queued_command(command_id="1", intent=CommandIntent.FIXIT) + ) + assert command_history.get_fixit_queue_ids() == OrderedSet(["0", "1"]) + command_history.clear_fixit_queue() + assert command_history.get_fixit_queue_ids() == OrderedSet() + + def test_remove_id_from_queue(command_history: CommandHistory) -> None: """It should remove the given ID from the queue.""" command_history._add_to_queue("0") diff --git a/api/tests/opentrons/protocol_engine/state/test_command_store_old.py b/api/tests/opentrons/protocol_engine/state/test_command_store_old.py index 52d5aa961ce..60cdf27838f 100644 --- a/api/tests/opentrons/protocol_engine/state/test_command_store_old.py +++ b/api/tests/opentrons/protocol_engine/state/test_command_store_old.py @@ -84,7 +84,7 @@ def test_initial_state( failed_command=None, command_error_recovery_types={}, recovery_target_command_id=None, - latest_command_hash=None, + latest_protocol_command_hash=None, stopped_by_estop=False, ) @@ -254,7 +254,7 @@ def test_command_queue_with_hash() -> None: ) assert subject.state.command_history.get("command-id-1").command.key == "abc123" - assert subject.state.latest_command_hash == "abc123" + assert subject.state.latest_protocol_command_hash == "abc123" subject.handle_action( QueueCommandAction( @@ -265,7 +265,7 @@ def test_command_queue_with_hash() -> None: ) ) - assert subject.state.latest_command_hash == "def456" + assert subject.state.latest_protocol_command_hash == "def456" def test_command_queue_and_unqueue() -> None: @@ -518,7 +518,7 @@ def test_command_store_handles_pause_action(pause_source: PauseSource) -> None: failed_command=None, command_error_recovery_types={}, recovery_target_command_id=None, - latest_command_hash=None, + latest_protocol_command_hash=None, stopped_by_estop=False, ) @@ -545,7 +545,7 @@ def test_command_store_handles_play_action(pause_source: PauseSource) -> None: command_error_recovery_types={}, recovery_target_command_id=None, run_started_at=datetime(year=2021, month=1, day=1), - latest_command_hash=None, + latest_protocol_command_hash=None, stopped_by_estop=False, ) assert subject.state.command_history.get_running_command() is None @@ -577,7 +577,7 @@ def test_command_store_handles_finish_action() -> None: command_error_recovery_types={}, recovery_target_command_id=None, run_started_at=datetime(year=2021, month=1, day=1), - latest_command_hash=None, + latest_protocol_command_hash=None, stopped_by_estop=False, ) assert subject.state.command_history.get_running_command() is None @@ -629,7 +629,7 @@ def test_command_store_handles_stop_action( command_error_recovery_types={}, recovery_target_command_id=None, run_started_at=datetime(year=2021, month=1, day=1), - latest_command_hash=None, + latest_protocol_command_hash=None, stopped_by_estop=from_estop, ) assert subject.state.command_history.get_running_command() is None @@ -660,7 +660,7 @@ def test_command_store_cannot_restart_after_should_stop() -> None: command_error_recovery_types={}, recovery_target_command_id=None, run_started_at=None, - latest_command_hash=None, + latest_protocol_command_hash=None, stopped_by_estop=False, ) assert subject.state.command_history.get_running_command() is None @@ -792,7 +792,7 @@ def test_command_store_wraps_unknown_errors() -> None: failed_command=None, command_error_recovery_types={}, recovery_target_command_id=None, - latest_command_hash=None, + latest_protocol_command_hash=None, stopped_by_estop=False, ) assert subject.state.command_history.get_running_command() is None @@ -855,7 +855,7 @@ def __init__(self, message: str) -> None: command_error_recovery_types={}, recovery_target_command_id=None, run_started_at=None, - latest_command_hash=None, + latest_protocol_command_hash=None, stopped_by_estop=False, ) assert subject.state.command_history.get_running_command() is None @@ -888,7 +888,7 @@ def test_command_store_ignores_stop_after_graceful_finish() -> None: command_error_recovery_types={}, recovery_target_command_id=None, run_started_at=datetime(year=2021, month=1, day=1), - latest_command_hash=None, + latest_protocol_command_hash=None, stopped_by_estop=False, ) assert subject.state.command_history.get_running_command() is None @@ -921,7 +921,7 @@ def test_command_store_ignores_finish_after_non_graceful_stop() -> None: command_error_recovery_types={}, recovery_target_command_id=None, run_started_at=datetime(year=2021, month=1, day=1), - latest_command_hash=None, + latest_protocol_command_hash=None, stopped_by_estop=False, ) assert subject.state.command_history.get_running_command() is None @@ -950,7 +950,7 @@ def test_handles_hardware_stopped() -> None: command_error_recovery_types={}, recovery_target_command_id=None, run_started_at=None, - latest_command_hash=None, + latest_protocol_command_hash=None, stopped_by_estop=False, ) assert subject.state.command_history.get_running_command() is None diff --git a/api/tests/opentrons/protocol_engine/state/test_command_view_old.py b/api/tests/opentrons/protocol_engine/state/test_command_view_old.py index a9b5fc92cc3..19a2515a3e6 100644 --- a/api/tests/opentrons/protocol_engine/state/test_command_view_old.py +++ b/api/tests/opentrons/protocol_engine/state/test_command_view_old.py @@ -46,7 +46,7 @@ ) -def get_command_view( +def get_command_view( # noqa: C901 queue_status: QueueStatus = QueueStatus.SETUP, run_completed_at: Optional[datetime] = None, run_started_at: Optional[datetime] = None, @@ -55,6 +55,7 @@ def get_command_view( running_command_id: Optional[str] = None, queued_command_ids: Sequence[str] = (), queued_setup_command_ids: Sequence[str] = (), + queued_fixit_command_ids: Sequence[str] = (), run_error: Optional[errors.ErrorOccurrence] = None, failed_command: Optional[CommandEntry] = None, command_error_recovery_types: Optional[Dict[str, ErrorRecoveryType]] = None, @@ -74,6 +75,9 @@ def get_command_view( if queued_setup_command_ids: for command_id in queued_setup_command_ids: command_history._add_to_setup_queue(command_id) + if queued_fixit_command_ids: + for command_id in queued_fixit_command_ids: + command_history._add_to_fixit_queue(command_id) if commands: for index, command in enumerate(commands): command_history._add( @@ -93,7 +97,7 @@ def get_command_view( command_error_recovery_types=command_error_recovery_types or {}, recovery_target_command_id=recovery_target_command_id, run_started_at=run_started_at, - latest_command_hash=latest_command_hash, + latest_protocol_command_hash=latest_command_hash, stopped_by_estop=False, ) @@ -133,6 +137,7 @@ def test_get_next_to_execute_returns_first_queued() -> None: subject = get_command_view( queue_status=QueueStatus.RUNNING, queued_command_ids=["command-id-1", "command-id-2"], + queued_fixit_command_ids=["fixit-id-1", "fixit-id-2"], ) assert subject.get_next_to_execute() == "command-id-1" @@ -155,6 +160,24 @@ def test_get_next_to_execute_prioritizes_setup_command_queue( assert subject.get_next_to_execute() == "setup-command-id" +@pytest.mark.parametrize( + "queue_status", + [QueueStatus.AWAITING_RECOVERY], +) +def test_get_next_to_execute_prioritizes_fixit_command_queue( + queue_status: QueueStatus, +) -> None: + """It should prioritize fixit command queue over protocol command queue.""" + subject = get_command_view( + queue_status=queue_status, + queued_command_ids=["command-id-1", "command-id-2"], + queued_setup_command_ids=["setup-command-id"], + queued_fixit_command_ids=["fixit-1", "fixit-2"], + ) + + assert subject.get_next_to_execute() == "fixit-1" + + def test_get_next_to_execute_returns_none_when_no_queued() -> None: """It should return None if there are no queued commands.""" subject = get_command_view( @@ -186,6 +209,20 @@ def test_get_next_to_execute_returns_no_commands_if_paused() -> None: queue_status=QueueStatus.PAUSED, queued_setup_command_ids=["setup-id-1", "setup-id-2"], queued_command_ids=["command-id-1", "command-id-2"], + queued_fixit_command_ids=["fixit-id-1", "fixit-id-2"], + ) + result = subject.get_next_to_execute() + + assert result is None + + +def test_get_next_to_execute_returns_no_commands_if_awaiting_recovery_no_fixit() -> None: + """It should not return any type of command if the engine is awaiting-recovery.""" + subject = get_command_view( + queue_status=QueueStatus.AWAITING_RECOVERY, + queued_setup_command_ids=["setup-id-1", "setup-id-2"], + queued_command_ids=["command-id-1", "command-id-2"], + queued_fixit_command_ids=[], ) result = subject.get_next_to_execute() @@ -486,12 +523,69 @@ class ActionAllowedSpec(NamedTuple): ), expected_error=errors.SetupCommandNotAllowedError, ), - # Resuming from error recovery is not implemented yet. - # https://opentrons.atlassian.net/browse/EXEC-301 + # fixit command is disallowed if not in recovery mode ActionAllowedSpec( - subject=get_command_view(), + subject=get_command_view(queue_status=QueueStatus.RUNNING), + action=QueueCommandAction( + request=cmd.HomeCreate( + params=cmd.HomeParams(), + intent=cmd.CommandIntent.FIXIT, + ), + request_hash=None, + command_id="command-id", + created_at=datetime(year=2021, month=1, day=1), + ), + expected_error=errors.FixitCommandNotAllowedError, + ), + ActionAllowedSpec( + subject=get_command_view( + queue_status=QueueStatus.AWAITING_RECOVERY, + failed_command=CommandEntry( + index=2, + command=create_failed_command( + command_id="command-id-3", + error=ErrorOccurrence( + id="error-id", + errorType="ProtocolEngineError", + createdAt=datetime(year=2022, month=2, day=2), + detail="oh no", + errorCode=ErrorCodes.GENERAL_ERROR.value.code, + ), + ), + ), + ), + action=QueueCommandAction( + request=cmd.HomeCreate( + params=cmd.HomeParams(), + intent=cmd.CommandIntent.FIXIT, + ), + request_hash=None, + command_id="command-id", + created_at=datetime(year=2021, month=1, day=1), + ), + expected_error=None, + ), + # resume from recovery not allowed if fixit commands in queue + ActionAllowedSpec( + subject=get_command_view( + queue_status=QueueStatus.AWAITING_RECOVERY, + queued_fixit_command_ids=["fixit-id-1", "fixit-id-2"], + failed_command=CommandEntry( + index=2, + command=create_failed_command( + command_id="command-id-3", + error=ErrorOccurrence( + id="error-id", + errorType="ProtocolEngineError", + createdAt=datetime(year=2022, month=2, day=2), + detail="oh no", + errorCode=ErrorCodes.GENERAL_ERROR.value.code, + ), + ), + ), + ), action=ResumeFromRecoveryAction(), - expected_error=NotImplementedError, + expected_error=errors.ResumeFromRecoveryNotAllowedError, ), ] @@ -931,4 +1025,4 @@ def test_get_slice_default_cursor_queued() -> None: def test_get_latest_command_hash() -> None: """It should get the latest command hash from state, if set.""" subject = get_command_view(latest_command_hash="abc123") - assert subject.get_latest_command_hash() == "abc123" + assert subject.get_latest_protocol_command_hash() == "abc123" diff --git a/api/tests/opentrons/protocol_engine/test_protocol_engine.py b/api/tests/opentrons/protocol_engine/test_protocol_engine.py index e3f7b315e4d..4816708fa57 100644 --- a/api/tests/opentrons/protocol_engine/test_protocol_engine.py +++ b/api/tests/opentrons/protocol_engine/test_protocol_engine.py @@ -17,6 +17,9 @@ from opentrons.protocols.models import LabwareDefinition from opentrons.protocol_engine import ProtocolEngine, commands, slot_standardization +from opentrons.protocol_engine.errors.exceptions import ( + CommandNotAllowedError, +) from opentrons.protocol_engine.types import ( DeckType, LabwareOffset, @@ -126,9 +129,9 @@ def _mock_slot_standardization_module( def _mock_hash_command_params_module( decoy: Decoy, monkeypatch: pytest.MonkeyPatch ) -> None: - hash_command_params = commands.hash_command_params + hash_command_params = commands.hash_protocol_command_params monkeypatch.setattr( - commands, "hash_command_params", decoy.mock(func=hash_command_params) + commands, "hash_protocol_command_params", decoy.mock(func=hash_command_params) ) @@ -180,7 +183,9 @@ def test_add_command( original_request = commands.WaitForResumeCreate( params=commands.WaitForResumeParams() ) - standardized_request = commands.HomeCreate(params=commands.HomeParams()) + standardized_request = commands.HomeCreate( + params=commands.HomeParams(), intent=commands.CommandIntent.PROTOCOL + ) queued = commands.Home( id="command-id", key="command-key", @@ -200,9 +205,13 @@ def test_add_command( decoy.when(model_utils.generate_id()).then_return("command-id") decoy.when(model_utils.get_timestamp()).then_return(created_at) - decoy.when(state_store.commands.get_latest_command_hash()).then_return("abc") + decoy.when(state_store.commands.get_latest_protocol_command_hash()).then_return( + "abc" + ) decoy.when( - commands.hash_command_params(create=standardized_request, last_hash="abc") + commands.hash_protocol_command_params( + create=standardized_request, last_hash="abc" + ) ).then_return("123") def _stub_queued(*_a: object, **_k: object) -> None: @@ -242,6 +251,105 @@ def _stub_queued(*_a: object, **_k: object) -> None: assert result == queued +def test_add_fixit_command( + decoy: Decoy, + state_store: StateStore, + action_dispatcher: ActionDispatcher, + model_utils: ModelUtils, + subject: ProtocolEngine, +) -> None: + """It should add a fixit command to the state from a request.""" + created_at = datetime(year=2021, month=1, day=1) + original_request = commands.WaitForResumeCreate( + params=commands.WaitForResumeParams() + ) + standardized_request = commands.HomeCreate( + params=commands.HomeParams(), intent=commands.CommandIntent.FIXIT + ) + queued = commands.Home( + id="command-id", + key="command-key", + status=commands.CommandStatus.QUEUED, + createdAt=created_at, + params=commands.HomeParams(), + ) + + robot_type: RobotType = "OT-3 Standard" + decoy.when(state_store.config).then_return( + Config(robot_type=robot_type, deck_type=DeckType.OT3_STANDARD) + ) + + decoy.when( + slot_standardization.standardize_command(original_request, robot_type) + ).then_return(standardized_request) + + decoy.when(model_utils.generate_id()).then_return("command-id") + decoy.when(model_utils.get_timestamp()).then_return(created_at) + + def _stub_queued(*_a: object, **_k: object) -> None: + decoy.when(state_store.commands.get("command-id")).then_return(queued) + + decoy.when( + state_store.commands.validate_action_allowed( + QueueCommandAction( + command_id="command-id", + created_at=created_at, + request=standardized_request, + request_hash=None, + ) + ) + ).then_return( + QueueCommandAction( + command_id="command-id-validated", + created_at=created_at, + request=standardized_request, + request_hash=None, + ) + ) + + decoy.when( + action_dispatcher.dispatch( + QueueCommandAction( + command_id="command-id-validated", + created_at=created_at, + request=standardized_request, + request_hash=None, + ) + ), + ).then_do(_stub_queued) + + result = subject.add_command(original_request) + assert result == queued + + +def test_add_fixit_command_raises( + decoy: Decoy, + state_store: StateStore, + action_dispatcher: ActionDispatcher, + model_utils: ModelUtils, + subject: ProtocolEngine, +) -> None: + """It should raise if a failedCommandId is supplied without a fixit command.""" + original_request = commands.WaitForResumeCreate( + params=commands.WaitForResumeParams() + ) + standardized_request = commands.HomeCreate( + params=commands.HomeParams(), intent=commands.CommandIntent.PROTOCOL + ) + + robot_type: RobotType = "OT-3 Standard" + decoy.when(state_store.config).then_return( + Config(robot_type=robot_type, deck_type=DeckType.OT3_STANDARD) + ) + + decoy.when( + slot_standardization.standardize_command(original_request, robot_type) + ).then_return(standardized_request) + + with pytest.raises(CommandNotAllowedError): + subject.add_command(original_request, "id-123") + + async def test_add_and_execute_command( decoy: Decoy, state_store: StateStore, diff --git a/robot-server/robot_server/runs/router/commands_router.py b/robot-server/robot_server/runs/router/commands_router.py index 734d1a26066..47a64c5d800 100644 --- a/robot-server/robot_server/runs/router/commands_router.py +++ b/robot-server/robot_server/runs/router/commands_router.py @@ -56,11 +56,18 @@ class CommandNotFound(ErrorDetails): title: str = "Run Command Not Found" +class SetupCommandNotAllowed(ErrorDetails): + """An error if a given run setup command is not allowed.""" + + id: Literal["SetupCommandNotAllowed"] = "SetupCommandNotAllowed" + title: str = "Setup Command Not Allowed" + + class CommandNotAllowed(ErrorDetails): """An error if a given run command is not allowed.""" id: Literal["CommandNotAllowed"] = "CommandNotAllowed" - title: str = "Setup Command Not Allowed" + title: str = "Command Not Allowed" class CommandLinkMeta(BaseModel): @@ -128,6 +135,7 @@ async def get_current_run_engine_from_url( - Setup commands (`data.source == "setup"`) - Protocol commands (`data.source == "protocol"`) + - Fixit commands (`data.source == "fixit"`) Setup commands may be enqueued before the run has been started. You could use setup commands to prepare a module or @@ -138,6 +146,11 @@ async def get_current_run_engine_from_url( If you are running a protocol from a file(s), then you will likely not need to enqueue protocol commands using this endpoint. + Fixit commands may be enqueued while the run is `awaiting-recovery` state. + These commands are intended to fix a failed command. + They will be executed right after the failed command + and only if the run is in a `awaiting-recovery` state. + Once enqueued, setup commands will execute immediately with priority, while protocol commands will wait until a `play` action is issued. A play action may be issued while setup commands are still queued, @@ -153,8 +166,9 @@ async def get_current_run_engine_from_url( status.HTTP_201_CREATED: {"model": SimpleBody[pe_commands.Command]}, status.HTTP_404_NOT_FOUND: {"model": ErrorBody[RunNotFound]}, status.HTTP_409_CONFLICT: { - "model": ErrorBody[Union[RunStopped, CommandNotAllowed]] + "model": ErrorBody[Union[RunStopped, SetupCommandNotAllowed]] }, + status.HTTP_400_BAD_REQUEST: {"model": ErrorBody[CommandNotAllowed]}, }, ) async def create_run_command( @@ -187,6 +201,12 @@ async def create_run_command( " the default was 30 seconds, not infinite." ), ), + failedCommandId: Optional[str] = Query( + default=None, + description=( + "FIXIT command use only. Reference of the failed command id we are trying to fix." + ), + ), protocol_engine: ProtocolEngine = Depends(get_current_run_engine_from_url), check_estop: bool = Depends(require_estop_in_good_state), ) -> PydanticResponse[SimpleBody[pe_commands.Command]]: @@ -199,6 +219,8 @@ async def create_run_command( Else, return immediately. Comes from a query parameter in the URL. timeout: The maximum time, in seconds, to wait before returning. Comes from a query parameter in the URL. + failedCommandId: FIXIT command use only. + Reference of the failed command id we are trying to fix. protocol_engine: The run's `ProtocolEngine` on which the new command will be enqueued. check_estop: Dependency to verify the estop is in a valid state. @@ -207,14 +229,17 @@ async def create_run_command( # behavior is to pass through `command_intent` without overriding it command_intent = request_body.data.intent or pe_commands.CommandIntent.SETUP command_create = request_body.data.copy(update={"intent": command_intent}) - try: - command = protocol_engine.add_command(command_create) + command = protocol_engine.add_command( + request=command_create, failed_command_id=failedCommandId + ) except pe_errors.SetupCommandNotAllowedError as e: - raise CommandNotAllowed.from_exc(e).as_error(status.HTTP_409_CONFLICT) + raise SetupCommandNotAllowed.from_exc(e).as_error(status.HTTP_409_CONFLICT) except pe_errors.RunStoppedError as e: raise RunStopped.from_exc(e).as_error(status.HTTP_409_CONFLICT) + except pe_errors.CommandNotAllowedError as e: + raise CommandNotAllowed.from_exc(e).as_error(status.HTTP_400_BAD_REQUEST) if waitUntilComplete: timeout_sec = None if timeout is None else timeout / 1000.0 diff --git a/robot-server/tests/runs/router/test_commands_router.py b/robot-server/tests/runs/router/test_commands_router.py index fa5e47ada9a..93adb46fa53 100644 --- a/robot-server/tests/runs/router/test_commands_router.py +++ b/robot-server/tests/runs/router/test_commands_router.py @@ -114,10 +114,11 @@ def _stub_queued_command_state(*_a: object, **_k: object) -> pe_commands.Command decoy.when( mock_protocol_engine.add_command( - pe_commands.WaitForResumeCreate( + request=pe_commands.WaitForResumeCreate( params=pe_commands.WaitForResumeParams(message="Hello"), intent=pe_commands.CommandIntent.SETUP, - ) + ), + failed_command_id=None, ) ).then_do(_stub_queued_command_state) @@ -125,6 +126,7 @@ def _stub_queued_command_state(*_a: object, **_k: object) -> pe_commands.Command request_body=RequestModelWithCommandCreate(data=command_request), waitUntilComplete=False, protocol_engine=mock_protocol_engine, + failedCommandId=None, ) assert result.content.data == command_once_added @@ -132,6 +134,33 @@ def _stub_queued_command_state(*_a: object, **_k: object) -> pe_commands.Command decoy.verify(await mock_protocol_engine.wait_for_command("command-id"), times=0) +async def test_create_command_with_failed_command_raises( + decoy: Decoy, + mock_protocol_engine: ProtocolEngine, +) -> None: + """It should return 400 bad request.""" + command_create = pe_commands.HomeCreate(params=pe_commands.HomeParams()) + + decoy.when( + mock_protocol_engine.add_command( + pe_commands.HomeCreate( + params=pe_commands.HomeParams(), + intent=pe_commands.CommandIntent.SETUP, + ), + failed_command_id="123", + ) + ).then_raise(pe_errors.CommandNotAllowedError()) + + with pytest.raises(ApiError): + await create_run_command( + RequestModelWithCommandCreate(data=command_create), + waitUntilComplete=False, + timeout=42, + protocol_engine=mock_protocol_engine, + failedCommandId="123", + ) + + async def test_create_run_command_blocking_completion( decoy: Decoy, mock_protocol_engine: ProtocolEngine, @@ -171,7 +200,7 @@ def _stub_completed_command_state(*_a: object, **_k: object) -> None: mock_protocol_engine.state_view.commands.get("command-id") ).then_return(command_once_completed) - decoy.when(mock_protocol_engine.add_command(command_request)).then_do( + decoy.when(mock_protocol_engine.add_command(command_request, None)).then_do( _stub_queued_command_state ) @@ -184,6 +213,7 @@ def _stub_completed_command_state(*_a: object, **_k: object) -> None: waitUntilComplete=True, timeout=999, protocol_engine=mock_protocol_engine, + failedCommandId=None, ) assert result.content.data == command_once_completed @@ -200,7 +230,7 @@ async def test_add_conflicting_setup_command( intent=pe_commands.CommandIntent.SETUP, ) - decoy.when(mock_protocol_engine.add_command(command_request)).then_raise( + decoy.when(mock_protocol_engine.add_command(command_request, None)).then_raise( pe_errors.SetupCommandNotAllowedError("oh no") ) @@ -209,6 +239,7 @@ async def test_add_conflicting_setup_command( request_body=RequestModelWithCommandCreate(data=command_request), waitUntilComplete=False, protocol_engine=mock_protocol_engine, + failedCommandId=None, ) assert exc_info.value.status_code == 409 @@ -228,7 +259,7 @@ async def test_add_command_to_stopped_engine( intent=pe_commands.CommandIntent.SETUP, ) - decoy.when(mock_protocol_engine.add_command(command_request)).then_raise( + decoy.when(mock_protocol_engine.add_command(command_request, None)).then_raise( pe_errors.RunStoppedError("oh no") ) @@ -237,6 +268,7 @@ async def test_add_command_to_stopped_engine( request_body=RequestModelWithCommandCreate(data=command_request), waitUntilComplete=False, protocol_engine=mock_protocol_engine, + failedCommandId=None, ) assert exc_info.value.status_code == 409 diff --git a/shared-data/command/schemas/8.json b/shared-data/command/schemas/8.json index a17be9ee690..f3c5bb38b27 100644 --- a/shared-data/command/schemas/8.json +++ b/shared-data/command/schemas/8.json @@ -339,7 +339,7 @@ "CommandIntent": { "title": "CommandIntent", "description": "Run intent for a given command.\n\nProps:\n PROTOCOL: the command is part of the protocol run itself.\n SETUP: the command is part of the setup phase of a run.", - "enum": ["protocol", "setup"], + "enum": ["protocol", "setup", "fixit"], "type": "string" }, "AspirateCreate": { From 737c58c9cb0669e27ef8aad7a4a3cce93c30c09a Mon Sep 17 00:00:00 2001 From: Jamey Huffnagle Date: Mon, 22 Apr 2024 16:04:59 -0400 Subject: [PATCH 186/194] fix(robot-server): notify /runs when a non-current run is deleted (#14974) Closes RQA-2599 --- .../robot_server/runs/run_data_manager.py | 3 +- .../publishers/runs_publisher.py | 30 ++++++++++--------- .../publishers/test_runs_publisher.py | 2 +- 3 files changed, 19 insertions(+), 16 deletions(-) diff --git a/robot-server/robot_server/runs/run_data_manager.py b/robot-server/robot_server/runs/run_data_manager.py index 154a1584823..8548104911b 100644 --- a/robot-server/robot_server/runs/run_data_manager.py +++ b/robot-server/robot_server/runs/run_data_manager.py @@ -284,7 +284,8 @@ async def delete(self, run_id: str) -> None: """ if run_id == self._engine_store.current_run_id: await self._engine_store.clear() - await self._runs_publisher.clean_up_current_run() + + await self._runs_publisher.clean_up_run(run_id=run_id) self._run_store.remove(run_id=run_id) diff --git a/robot-server/robot_server/service/notifications/publishers/runs_publisher.py b/robot-server/robot_server/service/notifications/publishers/runs_publisher.py index b6744fbc90a..fef23c8a875 100644 --- a/robot-server/robot_server/service/notifications/publishers/runs_publisher.py +++ b/robot-server/robot_server/service/notifications/publishers/runs_publisher.py @@ -71,12 +71,12 @@ async def initialize( ) self._engine_state_slice = EngineStateSlice() - await self._publish_runs_advise_refetch_async() + await self._publish_runs_advise_refetch_async(run_id=run_id) - async def clean_up_current_run(self) -> None: - """Publish final refetch and unsubscribe flags.""" - await self._publish_runs_advise_refetch_async() - await self._publish_runs_advise_unsubscribe_async() + async def clean_up_run(self, run_id: str) -> None: + """Publish final refetch and unsubscribe flags for the given run.""" + await self._publish_runs_advise_refetch_async(run_id=run_id) + await self._publish_runs_advise_unsubscribe_async(run_id=run_id) async def _publish_current_command(self) -> None: """Publishes the equivalent of GET /runs/:runId/commands?cursor=null&pageLength=1.""" @@ -84,20 +84,20 @@ async def _publish_current_command(self) -> None: topic=Topics.RUNS_CURRENT_COMMAND ) - async def _publish_runs_advise_refetch_async(self) -> None: + async def _publish_runs_advise_refetch_async(self, run_id: str) -> None: """Publish a refetch flag for relevant runs topics.""" + await self._client.publish_advise_refetch_async(topic=Topics.RUNS) + if self._run_hooks is not None: - await self._client.publish_advise_refetch_async(topic=Topics.RUNS) await self._client.publish_advise_refetch_async( - topic=f"{Topics.RUNS}/{self._run_hooks.run_id}" + topic=f"{Topics.RUNS}/{run_id}" ) - async def _publish_runs_advise_unsubscribe_async(self) -> None: + async def _publish_runs_advise_unsubscribe_async(self, run_id: str) -> None: """Publish an unsubscribe flag for relevant runs topics.""" - if self._run_hooks is not None: - await self._client.publish_advise_unsubscribe_async( - topic=f"{Topics.RUNS}/{self._run_hooks.run_id}" - ) + await self._client.publish_advise_unsubscribe_async( + topic=f"{Topics.RUNS}/{run_id}" + ) async def _handle_current_command_change(self) -> None: """Publish a refetch flag if the current command has changed.""" @@ -121,7 +121,9 @@ async def _handle_engine_status_change(self) -> None: and self._engine_state_slice.state_summary_status != current_state_summary.status ): - await self._publish_runs_advise_refetch_async() + await self._publish_runs_advise_refetch_async( + run_id=self._run_hooks.run_id + ) self._engine_state_slice.state_summary_status = ( current_state_summary.status ) diff --git a/robot-server/tests/service/notifications/publishers/test_runs_publisher.py b/robot-server/tests/service/notifications/publishers/test_runs_publisher.py index 29797dbf83a..a889664cbee 100644 --- a/robot-server/tests/service/notifications/publishers/test_runs_publisher.py +++ b/robot-server/tests/service/notifications/publishers/test_runs_publisher.py @@ -71,7 +71,7 @@ async def test_clean_up_current_run( """It should publish to appropriate topics at the end of a run.""" await runs_publisher.initialize("1234", AsyncMock(), AsyncMock()) - await runs_publisher.clean_up_current_run() + await runs_publisher.clean_up_run(run_id="1234") notification_client.publish_advise_refetch_async.assert_any_await(topic=Topics.RUNS) notification_client.publish_advise_refetch_async.assert_any_await( From 433ef44ae2d632e50446e5b2bcaecacc08544a74 Mon Sep 17 00:00:00 2001 From: Brent Hagen Date: Mon, 22 Apr 2024 16:59:08 -0400 Subject: [PATCH 187/194] feat(api-client,app,react-api-client): upload splash logo from desktop app (#14941) adds the upload input component, api-client, and react-api-client functions needed to upload a splash logo from the factory mode slideout closes PLAT-283 --- api-client/src/robot/index.ts | 2 + api-client/src/robot/types.ts | 5 + api-client/src/robot/updateRobotSetting.ts | 18 ++ api-client/src/system/createSplash.ts | 24 +++ api-client/src/system/index.ts | 1 + .../localization/en/device_settings.json | 4 + app/src/molecules/FileUpload/index.tsx | 60 +++++++ app/src/molecules/UploadInput/index.tsx | 34 ++-- .../FactoryModeSlideout.tsx | 156 +++++++++++++++--- components/src/ui-style-constants/spacing.ts | 1 + react-api-client/src/robot/index.ts | 1 + .../robot/useUpdateRobotSettingMutation.ts | 68 ++++++++ react-api-client/src/system/index.ts | 1 + .../src/system/useCreateSplashMutation.ts | 58 +++++++ 14 files changed, 397 insertions(+), 36 deletions(-) create mode 100644 api-client/src/robot/updateRobotSetting.ts create mode 100644 api-client/src/system/createSplash.ts create mode 100644 app/src/molecules/FileUpload/index.tsx create mode 100644 react-api-client/src/robot/useUpdateRobotSettingMutation.ts create mode 100644 react-api-client/src/system/useCreateSplashMutation.ts diff --git a/api-client/src/robot/index.ts b/api-client/src/robot/index.ts index 588a2f7a80e..55052d7b7c8 100644 --- a/api-client/src/robot/index.ts +++ b/api-client/src/robot/index.ts @@ -4,6 +4,7 @@ export { acknowledgeEstopDisengage } from './acknowledgeEstopDisengage' export { getLights } from './getLights' export { setLights } from './setLights' export { getRobotSettings } from './getRobotSettings' +export { updateRobotSetting } from './updateRobotSetting' export type { DoorStatus, @@ -15,4 +16,5 @@ export type { RobotSettingsField, RobotSettingsResponse, SetLightsData, + UpdateRobotSettingRequest, } from './types' diff --git a/api-client/src/robot/types.ts b/api-client/src/robot/types.ts index 088d78fa5c8..41ef7f1281e 100644 --- a/api-client/src/robot/types.ts +++ b/api-client/src/robot/types.ts @@ -38,6 +38,11 @@ export interface RobotSettingsField { export type RobotSettings = RobotSettingsField[] +export interface UpdateRobotSettingRequest { + id: string + value: boolean | null +} + export interface RobotSettingsResponse { settings: RobotSettings links?: { restart?: string } diff --git a/api-client/src/robot/updateRobotSetting.ts b/api-client/src/robot/updateRobotSetting.ts new file mode 100644 index 00000000000..a5775abaeee --- /dev/null +++ b/api-client/src/robot/updateRobotSetting.ts @@ -0,0 +1,18 @@ +import { POST, request } from '../request' + +import type { ResponsePromise } from '../request' +import type { HostConfig } from '../types' +import type { RobotSettingsResponse, UpdateRobotSettingRequest } from './types' + +export function updateRobotSetting( + config: HostConfig, + id: string, + value: boolean +): ResponsePromise { + return request( + POST, + '/settings', + { id, value }, + config + ) +} diff --git a/api-client/src/system/createSplash.ts b/api-client/src/system/createSplash.ts new file mode 100644 index 00000000000..fd0b11bd575 --- /dev/null +++ b/api-client/src/system/createSplash.ts @@ -0,0 +1,24 @@ +import { POST, request } from '../request' +import type { ResponsePromise } from '../request' +import type { HostConfig } from '../types' + +export function createSplash( + config: HostConfig, + file: File +): ResponsePromise { + // sanitize file name to ensure no spaces + const renamedFile = new File([file], file.name.replace(' ', '_'), { + type: 'image/png', + }) + + const formData = new FormData() + formData.append('file', renamedFile) + + // eslint-disable-next-line @typescript-eslint/no-invalid-void-type + return request( + POST, + '/system/oem_mode/upload_splash', + formData, + config + ) +} diff --git a/api-client/src/system/index.ts b/api-client/src/system/index.ts index 025a303a5b5..3c63202c31f 100644 --- a/api-client/src/system/index.ts +++ b/api-client/src/system/index.ts @@ -1,4 +1,5 @@ export { createAuthorization } from './createAuthorization' export { createRegistration } from './createRegistration' +export { createSplash } from './createSplash' export { getConnections } from './getConnections' export * from './types' diff --git a/app/src/assets/localization/en/device_settings.json b/app/src/assets/localization/en/device_settings.json index 3aec18d24a6..711ce0451d7 100644 --- a/app/src/assets/localization/en/device_settings.json +++ b/app/src/assets/localization/en/device_settings.json @@ -29,6 +29,7 @@ "check_for_updates": "Check for updates", "checking_for_updates": "Checking for updates", "choose": "Choose...", + "choose_file": "Choose file", "choose_network_type": "Choose network type", "choose_reset_settings": "Choose reset settings", "clear_all_data": "Clear all data", @@ -293,6 +294,9 @@ "update_robot_software": "Update robot software manually with a local file (.zip)", "updating": "Updating", "update_requires_restarting_robot": "Updating the robot software requires restarting the robot", + "upload_custom_logo_description": "Upload a logo for the robot to display during boot up. If no file is uploaded, we will display an anonymous logo.", + "upload_custom_logo_dimensions": "The logo must fit within dimensions 1024 x 600 and be a PNG file (.png).", + "upload_custom_logo": "Upload custom logo", "usage_settings": "Usage Settings", "usb": "USB", "usb_to_ethernet_description": "Looking for USB-to-Ethernet Adapter info?", diff --git a/app/src/molecules/FileUpload/index.tsx b/app/src/molecules/FileUpload/index.tsx new file mode 100644 index 00000000000..5e0fa7b0017 --- /dev/null +++ b/app/src/molecules/FileUpload/index.tsx @@ -0,0 +1,60 @@ +import * as React from 'react' +import { css } from 'styled-components' + +import { + ALIGN_CENTER, + BORDERS, + Btn, + COLORS, + DIRECTION_COLUMN, + Flex, + Icon, + JUSTIFY_SPACE_BETWEEN, + SPACING, + StyledText, +} from '@opentrons/components' + +const FILE_UPLOAD_STYLE = css` +&:hover > svg { + background: ${COLORS.black90}${COLORS.opacity20HexCode}; +} +&:active > svg { + background: ${COLORS.black90}${COLORS.opacity20HexCode}}; +} +` + +interface FileUploadProps { + file: File + fileError: string | null + handleClick: () => unknown +} + +export function FileUpload({ + file, + fileError, + handleClick, +}: FileUploadProps): JSX.Element { + return ( + + + + {file.name} + + + + {fileError != null ? ( + + {fileError} + + ) : null} + + ) +} diff --git a/app/src/molecules/UploadInput/index.tsx b/app/src/molecules/UploadInput/index.tsx index ea98b4735f3..45982e20ff2 100644 --- a/app/src/molecules/UploadInput/index.tsx +++ b/app/src/molecules/UploadInput/index.tsx @@ -45,11 +45,19 @@ const StyledInput = styled.input` export interface UploadInputProps { onUpload: (file: File) => unknown onClick?: () => void + uploadButtonText?: string uploadText?: string | JSX.Element dragAndDropText?: string | JSX.Element } export function UploadInput(props: UploadInputProps): JSX.Element | null { + const { + dragAndDropText, + onClick, + onUpload, + uploadButtonText, + uploadText, + } = props const { t } = useTranslation('protocol_info') const fileInput = React.useRef(null) @@ -60,7 +68,7 @@ export function UploadInput(props: UploadInputProps): JSX.Element | null { const handleDrop: React.DragEventHandler = e => { e.preventDefault() e.stopPropagation() - Array.from(e.dataTransfer.files).forEach(f => props.onUpload(f)) + Array.from(e.dataTransfer.files).forEach(f => onUpload(f)) setIsFileOverDropZone(false) } const handleDragEnter: React.DragEventHandler = e => { @@ -81,11 +89,11 @@ export function UploadInput(props: UploadInputProps): JSX.Element | null { } const handleClick: React.MouseEventHandler = _event => { - props.onClick != null ? props.onClick() : fileInput.current?.click() + onClick != null ? onClick() : fileInput.current?.click() } const onChange: React.ChangeEventHandler = event => { - ;[...(event.target.files ?? [])].forEach(f => props.onUpload(f)) + ;[...(event.target.files ?? [])].forEach(f => onUpload(f)) if ('value' in event.currentTarget) event.currentTarget.value = '' } @@ -97,18 +105,20 @@ export function UploadInput(props: UploadInputProps): JSX.Element | null { alignItems={ALIGN_CENTER} gridGap={SPACING.spacing24} > - - {props.uploadText} - + {uploadText != null ? ( + + {uploadText} + + ) : null} - {t('upload')} + {uploadButtonText ?? t('upload')} - {props.dragAndDropText} + {dragAndDropText} (1) const [toggleValue, setToggleValue] = React.useState(false) + const [file, setFile] = React.useState(null) + const [fileError, setFileError] = React.useState(null) + const [isUploading, setIsUploading] = React.useState(false) + + const onFinishCompleteClick = (): void => { + dispatch(restartRobot(robotName)) + onCloseClick() + setIsUploading(false) + } + + const { createSplash } = useCreateSplashMutation({ + onSuccess: () => { + onFinishCompleteClick() + }, + }) + + const { updateRobotSetting } = useUpdateRobotSettingMutation({ + onSuccess: () => { + if (toggleValue && file != null) { + createSplash({ file }) + } else { + onFinishCompleteClick() + } + }, + }) const { handleSubmit, @@ -76,9 +108,30 @@ export function FactoryModeSlideout({ } const handleCompleteClick: React.MouseEventHandler = () => { - dispatch(updateSetting(robotName, 'enableOEMMode', toggleValue)) - dispatch(restartRobot(robotName)) - onCloseClick() + setIsUploading(true) + updateRobotSetting({ id: 'enableOEMMode', value: toggleValue }) + } + + const handleChooseFile = (file: File): void => { + // validation for file type + if (file.type !== 'image/png') { + setFileError('Incorrect file type') + setFile(file) + } else { + const imgUrl = URL.createObjectURL(file) + const logoImage = new Image() + logoImage.src = imgUrl + logoImage.onload = () => { + // validation for ODD screen size + if ( + logoImage.naturalWidth !== 1024 || + logoImage.naturalHeight !== 600 + ) { + setFileError('Incorrect image dimensions') + } + setFile(file) + } + } } React.useEffect(() => { @@ -103,8 +156,20 @@ export function FactoryModeSlideout({ ) : null} {currentStep === 2 ? ( - - {t('complete_and_restart_robot')} + + {isUploading ? ( + + ) : ( + t('complete_and_restart_robot') + )} ) : null} @@ -143,24 +208,67 @@ export function FactoryModeSlideout({ ) : null} {currentStep === 2 ? ( - - - {t('oem_mode')} - - - - - {toggleValue ? t('on') : t('off')} + + + + {t('oem_mode')} + + + + {toggleValue ? t('on') : t('off')} + + + {t('branded:oem_mode_description')} - {t('branded:oem_mode_description')} + {toggleValue ? ( + + + + {t('upload_custom_logo')} + + + {t('upload_custom_logo_description')} + + + {t('upload_custom_logo_dimensions')} + + + {file == null ? ( + handleChooseFile(file)} + dragAndDropText={ + + , + }} + /> + + } + /> + ) : ( + { + setFile(null) + setFileError(null) + }} + /> + )} + + ) : null} ) : null} diff --git a/components/src/ui-style-constants/spacing.ts b/components/src/ui-style-constants/spacing.ts index bdd4dbcab26..2fd0e0c9ecd 100644 --- a/components/src/ui-style-constants/spacing.ts +++ b/components/src/ui-style-constants/spacing.ts @@ -9,6 +9,7 @@ export const spacing20 = '1.25rem' as const // 20px export const spacing24 = '1.5rem' as const // 24px export const spacing32 = '2rem' as const // 32px export const spacing40 = '2.5rem' as const // 40px +export const spacing44 = '2.75rem' as const // 44px export const spacing48 = '3rem' as const // 48px export const spacing60 = '3.75rem' as const // 60px export const spacing68 = '4.25rem' as const // 68px diff --git a/react-api-client/src/robot/index.ts b/react-api-client/src/robot/index.ts index 4b296d6a4fe..0ac1c3341b5 100644 --- a/react-api-client/src/robot/index.ts +++ b/react-api-client/src/robot/index.ts @@ -4,3 +4,4 @@ export { useLightsQuery } from './useLightsQuery' export { useAcknowledgeEstopDisengageMutation } from './useAcknowledgeEstopDisengageMutation' export { useSetLightsMutation } from './useSetLightsMutation' export { useRobotSettingsQuery } from './useRobotSettingsQuery' +export { useUpdateRobotSettingMutation } from './useUpdateRobotSettingMutation' diff --git a/react-api-client/src/robot/useUpdateRobotSettingMutation.ts b/react-api-client/src/robot/useUpdateRobotSettingMutation.ts new file mode 100644 index 00000000000..83765fb5a70 --- /dev/null +++ b/react-api-client/src/robot/useUpdateRobotSettingMutation.ts @@ -0,0 +1,68 @@ +import { useMutation } from 'react-query' +import { updateRobotSetting } from '@opentrons/api-client' +import { useHost } from '../api' + +import type { AxiosError } from 'axios' +import type { + UseMutateFunction, + UseMutationOptions, + UseMutationResult, +} from 'react-query' +import type { + ErrorResponse, + HostConfig, + RobotSettings, +} from '@opentrons/api-client' + +export interface UpdateRobotSettingVariables { + id: string + value: boolean +} + +export type UseUpdateRobotSettingMutationResult = UseMutationResult< + RobotSettings, + AxiosError, + UpdateRobotSettingVariables +> & { + updateRobotSetting: UseMutateFunction< + RobotSettings, + AxiosError, + UpdateRobotSettingVariables + > +} + +export type UseUpdateRobotSettingnMutationOptions = UseMutationOptions< + RobotSettings, + AxiosError, + UpdateRobotSettingVariables +> + +export function useUpdateRobotSettingMutation( + options: UseUpdateRobotSettingnMutationOptions = {} +): UseUpdateRobotSettingMutationResult { + const host = useHost() + // const queryClient = useQueryClient() + + const mutation = useMutation< + RobotSettings, + AxiosError, + UpdateRobotSettingVariables + >( + [host, 'robot_settings'], + ({ id, value }) => + updateRobotSetting(host as HostConfig, id, value).then(response => { + // TODO: investigate ODD top level behavior when invalidating this query + // queryClient + // .invalidateQueries([host, 'robot_settings']) + // .catch((e: Error) => { + // throw e + // }) + return response.data?.settings ?? [] + }), + options + ) + return { + ...mutation, + updateRobotSetting: mutation.mutate, + } +} diff --git a/react-api-client/src/system/index.ts b/react-api-client/src/system/index.ts index 10dc4d8ba66..faabb1e9f35 100644 --- a/react-api-client/src/system/index.ts +++ b/react-api-client/src/system/index.ts @@ -1,2 +1,3 @@ export { useAuthorization } from './useAuthorization' export { useConnectionsQuery } from './useConnectionsQuery' +export { useCreateSplashMutation } from './useCreateSplashMutation' diff --git a/react-api-client/src/system/useCreateSplashMutation.ts b/react-api-client/src/system/useCreateSplashMutation.ts new file mode 100644 index 00000000000..783dc1cf7b4 --- /dev/null +++ b/react-api-client/src/system/useCreateSplashMutation.ts @@ -0,0 +1,58 @@ +import { useMutation } from 'react-query' +import { createSplash } from '@opentrons/api-client' +import { useHost } from '../api' + +import type { AxiosError, AxiosResponse } from 'axios' +import type { + UseMutationResult, + UseMutationOptions, + UseMutateFunction, +} from 'react-query' +import type { ErrorResponse, HostConfig } from '@opentrons/api-client' + +export interface CreateSplashRequestData { + file: File +} +export type UseCreateSplashMutationResult = UseMutationResult< + AxiosResponse, + AxiosError, + CreateSplashRequestData +> & { + createSplash: UseMutateFunction< + AxiosResponse, + AxiosError, + CreateSplashRequestData + > +} + +export type UseCreateSplashMutationOptions = UseMutationOptions< + AxiosResponse, + AxiosError, + CreateSplashRequestData +> + +export function useCreateSplashMutation( + options: UseCreateSplashMutationOptions = {}, + hostOverride?: HostConfig | null +): UseCreateSplashMutationResult { + const contextHost = useHost() + const host = + hostOverride != null ? { ...contextHost, ...hostOverride } : contextHost + + const mutation = useMutation< + AxiosResponse, + AxiosError, + CreateSplashRequestData + >( + [host, 'splash'], + ({ file }) => + createSplash(host as HostConfig, file).catch(e => { + throw e + }), + options + ) + return { + ...mutation, + createSplash: mutation.mutate, + } +} From ff5e0c0896cd42d7d2eaa9b3b404e82fb8154150 Mon Sep 17 00:00:00 2001 From: Alise Au <20424172+ahiuchingau@users.noreply.github.com> Date: Mon, 22 Apr 2024 18:04:31 -0400 Subject: [PATCH 188/194] fix(api): remove homing patch fix for right mount when a 96-channel is attached (#14975) # Overview We now [unconditionally home the axis](https://github.com/Opentrons/opentrons/pull/14955) before performing the homing move so we can now remove this patch. If we leave this in, the head R is going to raise an error when the robot boots up the first time because it will not be able to update the position estimation. # Test Plan # Changelog # Review requests # Risk assessment --- api/src/opentrons/hardware_control/ot3api.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/api/src/opentrons/hardware_control/ot3api.py b/api/src/opentrons/hardware_control/ot3api.py index 93763876575..37f1f43e75c 100644 --- a/api/src/opentrons/hardware_control/ot3api.py +++ b/api/src/opentrons/hardware_control/ot3api.py @@ -1521,14 +1521,8 @@ async def _home_axis(self, axis: Axis) -> None: # G, Q should be handled in the backend through `self._home()` assert axis not in [Axis.G, Axis.Q] - # TODO(CM): This is a temporary fix in response to the right mount causing - # errors while trying to home on startup or attachment. We should remove this - # when we fix this issue in the firmware. - enable_right_mount_on_startup = ( - self._gantry_load == GantryLoad.HIGH_THROUGHPUT and axis == Axis.Z_R - ) encoder_ok = self._backend.check_encoder_status([axis]) - if encoder_ok or enable_right_mount_on_startup: + if encoder_ok: # enable motor (if needed) and update estimation await self._enable_before_update_estimation(axis) From 3b7058e6671f226ab6fd97851e0cab220716e40b Mon Sep 17 00:00:00 2001 From: koji Date: Mon, 22 Apr 2024 20:55:36 -0400 Subject: [PATCH 189/194] fix(app): add robotSerialNumber to proceedToRun event (#14976) * fix(app): add robotSerialNumber to proceedToRun event --- .../Devices/HistoricalProtocolRunOverflowMenu.tsx | 10 ++++++++-- .../HistoricalProtocolRunOverflowMenu.test.tsx | 13 ++++++++++--- .../__tests__/useProtocolRunAnalyticsData.test.tsx | 4 ++-- .../RobotDashboard/RecentRunProtocolCard.tsx | 7 +++++-- .../__tests__/RecentRunProtocolCard.test.tsx | 7 +++++-- app/src/pages/RunSummary/index.tsx | 9 +++++++-- app/src/redux/discovery/__fixtures__/index.ts | 1 + 7 files changed, 38 insertions(+), 13 deletions(-) diff --git a/app/src/organisms/Devices/HistoricalProtocolRunOverflowMenu.tsx b/app/src/organisms/Devices/HistoricalProtocolRunOverflowMenu.tsx index bf06e0db263..7f4bc54b6e1 100644 --- a/app/src/organisms/Devices/HistoricalProtocolRunOverflowMenu.tsx +++ b/app/src/organisms/Devices/HistoricalProtocolRunOverflowMenu.tsx @@ -32,7 +32,7 @@ import { ANALYTICS_PROTOCOL_RUN_AGAIN, } from '../../redux/analytics' import { getRobotUpdateDisplayInfo } from '../../redux/robot-update' -import { useDownloadRunLog, useTrackProtocolRunEvent } from './hooks' +import { useDownloadRunLog, useTrackProtocolRunEvent, useRobot } from './hooks' import { useIsEstopNotDisengaged } from '../../resources/devices/hooks/useIsEstopNotDisengaged' import type { Run } from '@opentrons/api-client' @@ -132,6 +132,9 @@ function MenuDropdown(props: MenuDropdownProps): JSX.Element { const { trackProtocolRunEvent } = useTrackProtocolRunEvent(runId, robotName) const { reset } = useRunControls(runId, onResetSuccess) const { deleteRun } = useDeleteRunMutation() + const robot = useRobot(robotName) + const robotSerialNumber = + robot?.health?.robot_serial ?? robot?.serverHealth?.serialNumber ?? null const handleResetClick: React.MouseEventHandler = ( e @@ -142,7 +145,10 @@ function MenuDropdown(props: MenuDropdownProps): JSX.Element { reset() trackEvent({ name: ANALYTICS_PROTOCOL_PROCEED_TO_RUN, - properties: { sourceLocation: 'HistoricalProtocolRun' }, + properties: { + sourceLocation: 'HistoricalProtocolRun', + robotSerialNumber, + }, }) trackProtocolRunEvent({ name: ANALYTICS_PROTOCOL_RUN_AGAIN }) } diff --git a/app/src/organisms/Devices/__tests__/HistoricalProtocolRunOverflowMenu.test.tsx b/app/src/organisms/Devices/__tests__/HistoricalProtocolRunOverflowMenu.test.tsx index f7d537e88ff..c436bc04960 100644 --- a/app/src/organisms/Devices/__tests__/HistoricalProtocolRunOverflowMenu.test.tsx +++ b/app/src/organisms/Devices/__tests__/HistoricalProtocolRunOverflowMenu.test.tsx @@ -5,23 +5,24 @@ import '@testing-library/jest-dom/vitest' import { renderWithProviders } from '../../../__testing-utils__' import { when } from 'vitest-when' import { MemoryRouter } from 'react-router-dom' -import { UseQueryResult } from 'react-query' import { useAllCommandsQuery, useDeleteRunMutation, } from '@opentrons/react-api-client' import { i18n } from '../../../i18n' import runRecord from '../../../organisms/RunDetails/__fixtures__/runRecord.json' -import { useDownloadRunLog, useTrackProtocolRunEvent } from '../hooks' +import { useDownloadRunLog, useTrackProtocolRunEvent, useRobot } from '../hooks' import { useRunControls } from '../../RunTimeControl/hooks' import { useTrackEvent, ANALYTICS_PROTOCOL_PROCEED_TO_RUN, } from '../../../redux/analytics' +import { mockConnectableRobot } from '../../../redux/discovery/__fixtures__' import { getRobotUpdateDisplayInfo } from '../../../redux/robot-update' import { useIsEstopNotDisengaged } from '../../../resources/devices/hooks/useIsEstopNotDisengaged' import { HistoricalProtocolRunOverflowMenu } from '../HistoricalProtocolRunOverflowMenu' +import type { UseQueryResult } from 'react-query' import type { CommandsData } from '@opentrons/api-client' vi.mock('../../../redux/analytics') @@ -104,6 +105,9 @@ describe('HistoricalProtocolRunOverflowMenu', () => { robotName: ROBOT_NAME, robotIsBusy: false, } + when(vi.mocked(useRobot)) + .calledWith(ROBOT_NAME) + .thenReturn(mockConnectableRobot) }) it('renders the correct menu when a runId is present', () => { @@ -122,7 +126,10 @@ describe('HistoricalProtocolRunOverflowMenu', () => { fireEvent.click(rerunBtn) expect(mockTrackEvent).toHaveBeenCalledWith({ name: ANALYTICS_PROTOCOL_PROCEED_TO_RUN, - properties: { sourceLocation: 'HistoricalProtocolRun' }, + properties: { + robotSerialNumber: 'mock-serial', + sourceLocation: 'HistoricalProtocolRun', + }, }) expect(useRunControls).toHaveBeenCalled() expect(mockTrackProtocolRunEvent).toHaveBeenCalled() diff --git a/app/src/organisms/Devices/hooks/__tests__/useProtocolRunAnalyticsData.test.tsx b/app/src/organisms/Devices/hooks/__tests__/useProtocolRunAnalyticsData.test.tsx index ce08a6cab90..72d8084df6b 100644 --- a/app/src/organisms/Devices/hooks/__tests__/useProtocolRunAnalyticsData.test.tsx +++ b/app/src/organisms/Devices/hooks/__tests__/useProtocolRunAnalyticsData.test.tsx @@ -131,7 +131,7 @@ describe('useProtocolAnalysisErrors hook', () => { protocolText: 'hashedString', protocolType: '', robotType: 'OT-2 Standard', - robotSerialNumber: '', + robotSerialNumber: 'mock-serial', }, runTime: '1:00:00', }) @@ -160,7 +160,7 @@ describe('useProtocolAnalysisErrors hook', () => { protocolText: 'hashedString', protocolType: 'json', robotType: 'OT-2 Standard', - robotSerialNumber: '', + robotSerialNumber: 'mock-serial', }, runTime: '1:00:00', }) diff --git a/app/src/organisms/OnDeviceDisplay/RobotDashboard/RecentRunProtocolCard.tsx b/app/src/organisms/OnDeviceDisplay/RobotDashboard/RecentRunProtocolCard.tsx index 21d293c7d5f..2f640e7e522 100644 --- a/app/src/organisms/OnDeviceDisplay/RobotDashboard/RecentRunProtocolCard.tsx +++ b/app/src/organisms/OnDeviceDisplay/RobotDashboard/RecentRunProtocolCard.tsx @@ -29,7 +29,10 @@ import { } from '@opentrons/api-client' import { ODD_FOCUS_VISIBLE } from '../../../atoms/buttons//constants' -import { useTrackEvent } from '../../../redux/analytics' +import { + useTrackEvent, + ANALYTICS_PROTOCOL_PROCEED_TO_RUN, +} from '../../../redux/analytics' import { Skeleton } from '../../../atoms/Skeleton' import { useMissingProtocolHardware } from '../../../pages/Protocols/hooks' import { useCloneRun } from '../../ProtocolUpload/hooks' @@ -147,7 +150,7 @@ export function ProtocolWithLastRun({ } else { cloneRun() trackEvent({ - name: 'proceedToRun', + name: ANALYTICS_PROTOCOL_PROCEED_TO_RUN, properties: { sourceLocation: 'RecentRunProtocolCard' }, }) } diff --git a/app/src/organisms/OnDeviceDisplay/RobotDashboard/__tests__/RecentRunProtocolCard.test.tsx b/app/src/organisms/OnDeviceDisplay/RobotDashboard/__tests__/RecentRunProtocolCard.test.tsx index 10de409948a..e1a54944a99 100644 --- a/app/src/organisms/OnDeviceDisplay/RobotDashboard/__tests__/RecentRunProtocolCard.test.tsx +++ b/app/src/organisms/OnDeviceDisplay/RobotDashboard/__tests__/RecentRunProtocolCard.test.tsx @@ -20,7 +20,10 @@ import { i18n } from '../../../../i18n' import { Skeleton } from '../../../../atoms/Skeleton' import { useMissingProtocolHardware } from '../../../../pages/Protocols/hooks' import { useTrackProtocolRunEvent } from '../../../Devices/hooks' -import { useTrackEvent } from '../../../../redux/analytics' +import { + useTrackEvent, + ANALYTICS_PROTOCOL_PROCEED_TO_RUN, +} from '../../../../redux/analytics' import { useCloneRun } from '../../../ProtocolUpload/hooks' import { useRerunnableStatusText } from '../hooks' import { RecentRunProtocolCard } from '../' @@ -250,7 +253,7 @@ describe('RecentRunProtocolCard', () => { expect(button).toHaveStyle(`background-color: ${COLORS.green40}`) fireEvent.click(button) expect(mockTrackEvent).toHaveBeenCalledWith({ - name: 'proceedToRun', + name: ANALYTICS_PROTOCOL_PROCEED_TO_RUN, properties: { sourceLocation: 'RecentRunProtocolCard' }, }) // TODO(BC, 08/30/23): reintroduce check for tracking when tracking is reintroduced lazily diff --git a/app/src/pages/RunSummary/index.tsx b/app/src/pages/RunSummary/index.tsx index e76a73ce1b9..7666cc8ada6 100644 --- a/app/src/pages/RunSummary/index.tsx +++ b/app/src/pages/RunSummary/index.tsx @@ -57,6 +57,7 @@ import { // ANALYTICS_PROTOCOL_RUN_CANCEL, ANALYTICS_PROTOCOL_RUN_AGAIN, ANALYTICS_PROTOCOL_RUN_FINISH, + ANALYTICS_PROTOCOL_PROCEED_TO_RUN, } from '../../redux/analytics' import { getLocalRobot } from '../../redux/discovery' import { RunFailedModal } from '../../organisms/OnDeviceDisplay/RunningProtocol' @@ -124,6 +125,10 @@ export function RunSummary(): JSX.Element { const [showRunAgainSpinner, setShowRunAgainSpinner] = React.useState( false ) + const robotSerialNumber = + localRobot?.health?.robot_serial ?? + localRobot?.serverHealth?.serialNumber ?? + null let headerText = t('run_complete_splash') if (runStatus === RUN_STATUS_FAILED) { @@ -167,8 +172,8 @@ export function RunSummary(): JSX.Element { setShowRunAgainSpinner(true) reset() trackEvent({ - name: 'proceedToRun', - properties: { sourceLocation: 'RunSummary' }, + name: ANALYTICS_PROTOCOL_PROCEED_TO_RUN, + properties: { sourceLocation: 'RunSummary', robotSerialNumber }, }) trackProtocolRunEvent({ name: ANALYTICS_PROTOCOL_RUN_AGAIN }) } diff --git a/app/src/redux/discovery/__fixtures__/index.ts b/app/src/redux/discovery/__fixtures__/index.ts index 329e18504dd..ea7a4e0f195 100644 --- a/app/src/redux/discovery/__fixtures__/index.ts +++ b/app/src/redux/discovery/__fixtures__/index.ts @@ -18,6 +18,7 @@ export const mockHealthResponse = { api_version: '0.0.0-mock', fw_version: '0.0.0-mock', system_version: '0.0.0-mock', + robot_serial: 'mock-serial', logs: [] as string[], protocol_api_version: [2, 0] as [number, number], } From 4794f558718757940543643035a233af96d298a4 Mon Sep 17 00:00:00 2001 From: koji Date: Tue, 23 Apr 2024 08:16:45 -0400 Subject: [PATCH 190/194] feat(opentrons-ai-client) add input textbox to container (#14968) * feat(opentrons-ai-client) add input textbox to container --- components/src/icons/icon-data.ts | 5 + opentrons-ai-client/package.json | 1 + .../localization/en/protocol_generator.json | 1 + .../__tests__/InputPrompt.test.tsx | 29 ++++ .../src/molecules/InputPrompt/index.tsx | 149 ++++++++++++++++++ .../src/molecules/PromptGuide/index.tsx | 1 - .../src/molecules/SidePanel/index.tsx | 1 - .../__tests__/ChatContainer.test.tsx | 7 + .../src/organisms/ChatContainer/index.tsx | 41 ++++- 9 files changed, 227 insertions(+), 8 deletions(-) create mode 100644 opentrons-ai-client/src/molecules/InputPrompt/__tests__/InputPrompt.test.tsx create mode 100644 opentrons-ai-client/src/molecules/InputPrompt/index.tsx diff --git a/components/src/icons/icon-data.ts b/components/src/icons/icon-data.ts index c805a8bbfba..e4f43123e13 100644 --- a/components/src/icons/icon-data.ts +++ b/components/src/icons/icon-data.ts @@ -632,6 +632,11 @@ export const ICON_DATA_BY_NAME = { 'M8.01487 8.84912C8.47511 8.84912 8.84821 8.47603 8.84821 8.01579C8.84821 7.55555 8.47511 7.18245 8.01487 7.18245C7.55464 7.18245 7.18154 7.55555 7.18154 8.01579C7.18154 8.47603 7.55464 8.84912 8.01487 8.84912Z M8.66654 0.928711V2.36089C11.27 2.66533 13.3354 4.73075 13.6398 7.33418H15.072V8.66751H13.6398C13.3354 11.2709 11.27 13.3363 8.66654 13.6408V15.073H7.3332V13.6408C4.72979 13.3363 2.66437 11.2709 2.35992 8.66751H0.927734V7.33418H2.35992C2.66436 4.73075 4.72978 2.66533 7.3332 2.36089V0.928711H8.66654ZM12.2944 7.33418H11.6184C11.2502 7.33418 10.9518 7.63266 10.9518 8.00085C10.9518 8.36904 11.2502 8.66751 11.6184 8.66751H12.2944C12.0071 10.5336 10.5326 12.008 8.66654 12.2953V11.6194C8.66654 11.2512 8.36806 10.9527 7.99987 10.9527C7.63168 10.9527 7.3332 11.2512 7.3332 11.6194V12.2953C5.46716 12.008 3.99268 10.5336 3.70536 8.66751H4.38132C4.74951 8.66751 5.04798 8.36904 5.04798 8.00085C5.04798 7.63266 4.74951 7.33418 4.38132 7.33418H3.70536C3.99267 5.46812 5.46715 3.99364 7.3332 3.70632V4.38229C7.3332 4.75048 7.63168 5.04896 7.99987 5.04896C8.36806 5.04896 8.66654 4.75048 8.66654 4.38229V3.70632C10.5326 3.99364 12.0071 5.46812 12.2944 7.33418Z', viewBox: '0 0 16 16', }, + send: { + path: + 'M6.96216 26.6667V5.33337L32.2955 16L6.96216 26.6667ZM9.62882 22.6667L25.4288 16L9.62882 9.33337V14L17.6288 16L9.62882 18V22.6667Z', + viewBox: '0 0 32 32', + }, settings: { path: 'M12,15.5A3.5,3.5 0 0,1 8.5,12A3.5,3.5 0 0,1 12,8.5A3.5,3.5 0 0,1 15.5,12A3.5,3.5 0 0,1 12,15.5M19.43,12.97C19.47,12.65 19.5,12.33 19.5,12C19.5,11.67 19.47,11.34 19.43,11L21.54,9.37C21.73,9.22 21.78,8.95 21.66,8.73L19.66,5.27C19.54,5.05 19.27,4.96 19.05,5.05L16.56,6.05C16.04,5.66 15.5,5.32 14.87,5.07L14.5,2.42C14.46,2.18 14.25,2 14,2H10C9.75,2 9.54,2.18 9.5,2.42L9.13,5.07C8.5,5.32 7.96,5.66 7.44,6.05L4.95,5.05C4.73,4.96 4.46,5.05 4.34,5.27L2.34,8.73C2.21,8.95 2.27,9.22 2.46,9.37L4.57,11C4.53,11.34 4.5,11.67 4.5,12C4.5,12.33 4.53,12.65 4.57,12.97L2.46,14.63C2.27,14.78 2.21,15.05 2.34,15.27L4.34,18.73C4.46,18.95 4.73,19.03 4.95,18.95L7.44,17.94C7.96,18.34 8.5,18.68 9.13,18.93L9.5,21.58C9.54,21.82 9.75,22 10,22H14C14.25,22 14.46,21.82 14.5,21.58L14.87,18.93C15.5,18.67 16.04,18.34 16.56,17.94L19.05,18.95C19.27,19.03 19.54,18.95 19.66,18.73L21.66,15.27C21.78,15.05 21.73,14.78 21.54,14.63L19.43,12.97Z', diff --git a/opentrons-ai-client/package.json b/opentrons-ai-client/package.json index 39d4f6d275c..d8ea50136ff 100644 --- a/opentrons-ai-client/package.json +++ b/opentrons-ai-client/package.json @@ -25,6 +25,7 @@ "react": "18.2.0", "react-dom": "18.2.0", "react-error-boundary": "^4.0.10", + "react-hook-form": "7.50.1", "react-i18next": "13.5.0", "react-markdown": "9.0.1", "styled-components": "5.3.6" diff --git a/opentrons-ai-client/src/assets/localization/en/protocol_generator.json b/opentrons-ai-client/src/assets/localization/en/protocol_generator.json index 80d273abffe..7911774f748 100644 --- a/opentrons-ai-client/src/assets/localization/en/protocol_generator.json +++ b/opentrons-ai-client/src/assets/localization/en/protocol_generator.json @@ -2,6 +2,7 @@ "api": "API: An API level is 2.15", "application": "Application: Your protocol's name, describing what it does.", "commands": "Commands: List the protocol's steps, specifying quantities in microliters and giving exact source and destination locations.", + "disclaimer": "OpentronsAI can make mistakes. Review your protocol before running it on an Opentrons robot.", "got_feedback": "Got feedback? We love to hear it.", "make_sure_your_prompt": "Make sure your prompt includes the following:", "metadata": "Metadata: Three pieces of information.", diff --git a/opentrons-ai-client/src/molecules/InputPrompt/__tests__/InputPrompt.test.tsx b/opentrons-ai-client/src/molecules/InputPrompt/__tests__/InputPrompt.test.tsx new file mode 100644 index 00000000000..f46d0722119 --- /dev/null +++ b/opentrons-ai-client/src/molecules/InputPrompt/__tests__/InputPrompt.test.tsx @@ -0,0 +1,29 @@ +import React from 'react' +import { describe, it, expect } from 'vitest' +import { fireEvent, screen } from '@testing-library/react' +import { renderWithProviders } from '../../../__testing-utils__' +import { i18n } from '../../../i18n' +import { InputPrompt } from '../index' + +const render = () => { + return renderWithProviders(, { i18nInstance: i18n }) +} + +describe('InputPrompt', () => { + it('should render textarea and disabled button', () => { + render() + screen.getByRole('textbox') + screen.queryByPlaceholderText('Type your prompt...') + screen.getByRole('button') + expect(screen.getByRole('button')).toBeDisabled() + }) + + it('should make send button not disabled when a user inputs something in textarea', () => { + render() + const textbox = screen.getByRole('textbox') + fireEvent.change(textbox, { target: { value: ['test'] } }) + expect(screen.getByRole('button')).not.toBeDisabled() + }) + + // ToDo (kk:04/19/2024) add more test cases +}) diff --git a/opentrons-ai-client/src/molecules/InputPrompt/index.tsx b/opentrons-ai-client/src/molecules/InputPrompt/index.tsx new file mode 100644 index 00000000000..c9702b7773d --- /dev/null +++ b/opentrons-ai-client/src/molecules/InputPrompt/index.tsx @@ -0,0 +1,149 @@ +import React from 'react' +import { useTranslation } from 'react-i18next' +import styled, { css } from 'styled-components' +import { useForm } from 'react-hook-form' + +import { + ALIGN_CENTER, + BORDERS, + Btn, + COLORS, + DIRECTION_ROW, + DISPLAY_FLEX, + Flex, + Icon, + JUSTIFY_CENTER, + SPACING, + TYPOGRAPHY, +} from '@opentrons/components' + +import type { SubmitHandler } from 'react-hook-form' + +// ToDo (kk:04/19/2024) Note this interface will be used by prompt buttons in SidePanel +// interface InputPromptProps {} + +interface InputType { + userPrompt: string +} + +export function InputPrompt(/* props: InputPromptProps */): JSX.Element { + const { t } = useTranslation('protocol_generator') + const { register, handleSubmit, watch } = useForm({ + defaultValues: { + userPrompt: '', + }, + }) + const userPrompt = watch('userPrompt') ?? '' + + const onSubmit: SubmitHandler = async data => { + // ToDo (kk: 04/19/2024) call api + const { userPrompt } = data + console.log('user prompt', userPrompt) + } + + return ( + handleSubmit(onSubmit)}> + + + + + + ) +} + +const StyledForm = styled.form` + width: 100%; +` + +const StyledTextarea = styled.textarea` + resize: none; + min-height: 3.75rem; + background-color: ${COLORS.white}; + border: none; + outline: none; + padding: 0; + box-shadow: none; + color: ${COLORS.black90}; + width: 100%; + font-size: ${TYPOGRAPHY.fontSize20}; + line-height: ${TYPOGRAPHY.lineHeight24}; + ::placeholder { + position: absolute; + top: 50%; + transform: translateY(-50%); + } +` + +interface PlayButtonProps { + onPlay?: () => void + disabled?: boolean + isLoading?: boolean +} + +function PlayButton({ + onPlay, + disabled = false, + isLoading = false, +}: PlayButtonProps): JSX.Element { + const playButtonStyle = css` + -webkit-tap-highlight-color: transparent; + &:focus { + background-color: ${COLORS.blue60}; + color: ${COLORS.white}; + } + + &:hover { + background-color: ${COLORS.blue50}; + color: ${COLORS.white}; + } + + &:focus-visible { + background-color: ${COLORS.blue50}; + } + + &:active { + background-color: ${COLORS.blue60}; + color: ${COLORS.white}; + } + + &:disabled { + background-color: ${COLORS.grey35}; + color: ${COLORS.grey50}; + } + ` + return ( + + + + ) +} diff --git a/opentrons-ai-client/src/molecules/PromptGuide/index.tsx b/opentrons-ai-client/src/molecules/PromptGuide/index.tsx index 16d995d5cfa..3cb4c69cc51 100644 --- a/opentrons-ai-client/src/molecules/PromptGuide/index.tsx +++ b/opentrons-ai-client/src/molecules/PromptGuide/index.tsx @@ -24,7 +24,6 @@ export function PromptGuide(): JSX.Element { backgroundColor={COLORS.grey30} borderRadius={BORDERS.borderRadius12} gridGap={SPACING.spacing32} - width="58.125rem" > {t('what_typeof_protocol')} diff --git a/opentrons-ai-client/src/molecules/SidePanel/index.tsx b/opentrons-ai-client/src/molecules/SidePanel/index.tsx index a53927c0293..9a408e2a732 100644 --- a/opentrons-ai-client/src/molecules/SidePanel/index.tsx +++ b/opentrons-ai-client/src/molecules/SidePanel/index.tsx @@ -26,7 +26,6 @@ export function SidePanel(): JSX.Element { flexDirection={DIRECTION_COLUMN} backgroundColor={COLORS.black90} width="24.375rem" - height="64rem" > {/* logo */} diff --git a/opentrons-ai-client/src/organisms/ChatContainer/__tests__/ChatContainer.test.tsx b/opentrons-ai-client/src/organisms/ChatContainer/__tests__/ChatContainer.test.tsx index 26eb7b0a2b5..406e7889878 100644 --- a/opentrons-ai-client/src/organisms/ChatContainer/__tests__/ChatContainer.test.tsx +++ b/opentrons-ai-client/src/organisms/ChatContainer/__tests__/ChatContainer.test.tsx @@ -4,9 +4,11 @@ import { describe, it, vi, beforeEach } from 'vitest' import { renderWithProviders } from '../../../__testing-utils__' import { i18n } from '../../../i18n' import { PromptGuide } from '../../../molecules/PromptGuide' +import { InputPrompt } from '../../../molecules/InputPrompt' import { ChatContainer } from '../index' vi.mock('../../../molecules/PromptGuide') +vi.mock('../../../molecules/InputPrompt') const render = (): ReturnType => { return renderWithProviders(, { @@ -17,11 +19,16 @@ const render = (): ReturnType => { describe('ChatContainer', () => { beforeEach(() => { vi.mocked(PromptGuide).mockReturnValue(
    mock PromptGuide
    ) + vi.mocked(InputPrompt).mockReturnValue(
    mock InputPrompt
    ) }) it('should render prompt guide and text', () => { render() screen.getByText('OpentronsAI') screen.getByText('mock PromptGuide') + screen.getByText('mock InputPrompt') + screen.getByText( + 'OpentronsAI can make mistakes. Review your protocol before running it on an Opentrons robot.' + ) }) // ToDo (kk:04/16/2024) Add more test cases diff --git a/opentrons-ai-client/src/organisms/ChatContainer/index.tsx b/opentrons-ai-client/src/organisms/ChatContainer/index.tsx index 2a6542c8e68..be6c4d619da 100644 --- a/opentrons-ai-client/src/organisms/ChatContainer/index.tsx +++ b/opentrons-ai-client/src/organisms/ChatContainer/index.tsx @@ -1,35 +1,64 @@ import React from 'react' import { useTranslation } from 'react-i18next' +import { css } from 'styled-components' import { COLORS, DIRECTION_COLUMN, - FLEX_MAX_CONTENT, Flex, + POSITION_ABSOLUTE, + POSITION_RELATIVE, SPACING, StyledText, + TYPOGRAPHY, } from '@opentrons/components' import { PromptGuide } from '../../molecules/PromptGuide' +import { InputPrompt } from '../../molecules/InputPrompt' export function ChatContainer(): JSX.Element { const { t } = useTranslation('protocol_generator') const isDummyInitial = true return ( {/* This will be updated when input textbox and function are implemented */} {isDummyInitial ? ( - {t('opentronsai')} - + + {t('opentronsai')} + + + + + + {t('disclaimer')} + + ) : null} ) } + +const DISCLAIMER_TEXT_STYLE = css` + color: ${COLORS.grey55}; + font-size: ${TYPOGRAPHY.fontSize20}; + line-height: ${TYPOGRAPHY.lineHeight24}; + text-align: ${TYPOGRAPHY.textAlignCenter}; +` From cfefcbc024d6e8902e19f5b614abb875ffff239b Mon Sep 17 00:00:00 2001 From: Caila Marashaj <98041399+caila-marashaj@users.noreply.github.com> Date: Tue, 23 Apr 2024 11:34:55 -0400 Subject: [PATCH 191/194] feat(api): add option to ignore different tip presence states (#14980) ## Overview This code adds an argument called `ht_operational_sensor` to `get_tip_presence_status`, that when used tells the api to only return the tip presence state of the instrument probe type specified. This allows calibration and partial tip flows to execute and check against their expected tip status without failing. ## TODO A follow-up pr will go up using this parameter for the `get_tip_presence` call in the calibration flow. ## Review Requests I'll most likely address any non-blocking change requests in a follow-up pr so we can cut the internal release as fast as possible, but let me know if: - `ht_operational_sensor` makes sense or if we can think of a better name - we should otherwise go about anything differently here. --- .../backends/flex_protocol.py | 4 +- .../backends/ot3controller.py | 10 +++- .../hardware_control/backends/ot3simulator.py | 6 ++- .../backends/tip_presence_manager.py | 34 ++++++++++++-- api/src/opentrons/hardware_control/ot3api.py | 12 +++-- .../backends/test_ot3_tip_presence_manager.py | 47 ++++++++++++++++++- 6 files changed, 101 insertions(+), 12 deletions(-) diff --git a/api/src/opentrons/hardware_control/backends/flex_protocol.py b/api/src/opentrons/hardware_control/backends/flex_protocol.py index 53efde79a23..7bd2969de6b 100644 --- a/api/src/opentrons/hardware_control/backends/flex_protocol.py +++ b/api/src/opentrons/hardware_control/backends/flex_protocol.py @@ -383,7 +383,9 @@ async def capacitive_pass( def subsystems(self) -> Dict[SubSystem, SubSystemState]: ... - async def get_tip_status(self, mount: OT3Mount) -> TipStateType: + async def get_tip_status( + self, mount: OT3Mount, ht_operation_sensor: Optional[InstrumentProbeType] = None + ) -> TipStateType: ... def current_tip_state(self, mount: OT3Mount) -> Optional[bool]: diff --git a/api/src/opentrons/hardware_control/backends/ot3controller.py b/api/src/opentrons/hardware_control/backends/ot3controller.py index 9316fb67e90..ea0b610f8b4 100644 --- a/api/src/opentrons/hardware_control/backends/ot3controller.py +++ b/api/src/opentrons/hardware_control/backends/ot3controller.py @@ -1521,8 +1521,14 @@ async def update_tip_detector(self, mount: OT3Mount, sensor_count: int) -> None: async def teardown_tip_detector(self, mount: OT3Mount) -> None: await self._tip_presence_manager.clear_detector(mount) - async def get_tip_status(self, mount: OT3Mount) -> TipStateType: - return await self.tip_presence_manager.get_tip_status(mount) + async def get_tip_status( + self, + mount: OT3Mount, + ht_operational_sensor: Optional[InstrumentProbeType] = None, + ) -> TipStateType: + return await self.tip_presence_manager.get_tip_status( + mount, ht_operational_sensor + ) def current_tip_state(self, mount: OT3Mount) -> Optional[bool]: return self.tip_presence_manager.current_tip_state(mount) diff --git a/api/src/opentrons/hardware_control/backends/ot3simulator.py b/api/src/opentrons/hardware_control/backends/ot3simulator.py index b96be54026e..26d6237e9a3 100644 --- a/api/src/opentrons/hardware_control/backends/ot3simulator.py +++ b/api/src/opentrons/hardware_control/backends/ot3simulator.py @@ -780,7 +780,11 @@ def subsystems(self) -> Dict[SubSystem, SubSystemState]: for axis in self._present_axes } - async def get_tip_status(self, mount: OT3Mount) -> TipStateType: + async def get_tip_status( + self, + mount: OT3Mount, + ht_operational_sensor: Optional[InstrumentProbeType] = None, + ) -> TipStateType: return TipStateType(self._sim_tip_state[mount]) def current_tip_state(self, mount: OT3Mount) -> Optional[bool]: diff --git a/api/src/opentrons/hardware_control/backends/tip_presence_manager.py b/api/src/opentrons/hardware_control/backends/tip_presence_manager.py index 9d2be3901da..0e46d713955 100644 --- a/api/src/opentrons/hardware_control/backends/tip_presence_manager.py +++ b/api/src/opentrons/hardware_control/backends/tip_presence_manager.py @@ -3,7 +3,7 @@ from typing import cast, Callable, Optional, List, Set from typing_extensions import TypedDict, Literal -from opentrons.hardware_control.types import TipStateType, OT3Mount +from opentrons.hardware_control.types import TipStateType, OT3Mount, InstrumentProbeType from opentrons_hardware.drivers.can_bus import CanMessenger from opentrons_hardware.firmware_bindings.constants import NodeId @@ -14,8 +14,11 @@ from opentrons_shared_data.errors.exceptions import ( TipDetectorNotFound, UnmatchedTipPresenceStates, + GeneralError, ) +from .ot3utils import sensor_id_for_instrument + log = logging.getLogger(__name__) TipListener = Callable[[OT3Mount, bool], None] @@ -111,7 +114,24 @@ def current_tip_state(self, mount: OT3Mount) -> Optional[bool]: return state @staticmethod - def _get_tip_presence(results: List[tip_types.TipNotification]) -> TipStateType: + def _get_tip_presence( + results: List[tip_types.TipNotification], + ht_operational_sensor: Optional[InstrumentProbeType] = None, + ) -> TipStateType: + """ + We can use ht_operational_sensor used to specify that we only care + about the status of one tip presence sensor on a high throughput + pipette, and the other is allowed to be different. + """ + if ht_operational_sensor: + target_sensor_id = sensor_id_for_instrument(ht_operational_sensor) + for r in results: + if r.sensor == target_sensor_id: + return TipStateType(r.presence) + # raise an error if requested sensor response isn't found + raise GeneralError( + message=f"Requested status for sensor {ht_operational_sensor} not found." + ) # more than one sensor reported, we have to check if their states match if len(set(r.presence for r in results)) > 1: raise UnmatchedTipPresenceStates( @@ -119,9 +139,15 @@ def _get_tip_presence(results: List[tip_types.TipNotification]) -> TipStateType: ) return TipStateType(results[0].presence) - async def get_tip_status(self, mount: OT3Mount) -> TipStateType: + async def get_tip_status( + self, + mount: OT3Mount, + ht_operational_sensor: Optional[InstrumentProbeType] = None, + ) -> TipStateType: detector = self.get_detector(mount) - return self._get_tip_presence(await detector.request_tip_status()) + return self._get_tip_presence( + await detector.request_tip_status(), ht_operational_sensor + ) def get_detector(self, mount: OT3Mount) -> TipDetector: detector = self._detectors[self._get_key(mount)] diff --git a/api/src/opentrons/hardware_control/ot3api.py b/api/src/opentrons/hardware_control/ot3api.py index 37f1f43e75c..dbc76181f24 100644 --- a/api/src/opentrons/hardware_control/ot3api.py +++ b/api/src/opentrons/hardware_control/ot3api.py @@ -2072,6 +2072,7 @@ async def _high_throughput_check_tip(self) -> AsyncIterator[None]: async def get_tip_presence_status( self, mount: Union[top_types.Mount, OT3Mount], + ht_operational_sensor: Optional[InstrumentProbeType] = None, ) -> TipStateType: """ Check tip presence status. If a high throughput pipette is present, @@ -2085,14 +2086,19 @@ async def get_tip_presence_status( and self._gantry_load == GantryLoad.HIGH_THROUGHPUT ): await stack.enter_async_context(self._high_throughput_check_tip()) - result = await self._backend.get_tip_status(real_mount) + result = await self._backend.get_tip_status( + real_mount, ht_operational_sensor + ) return result async def verify_tip_presence( - self, mount: Union[top_types.Mount, OT3Mount], expected: TipStateType + self, + mount: Union[top_types.Mount, OT3Mount], + expected: TipStateType, + ht_operational_sensor: Optional[InstrumentProbeType] = None, ) -> None: real_mount = OT3Mount.from_mount(mount) - status = await self.get_tip_presence_status(real_mount) + status = await self.get_tip_presence_status(real_mount, ht_operational_sensor) if status != expected: raise FailedTipStateCheck(expected, status.value) diff --git a/api/tests/opentrons/hardware_control/backends/test_ot3_tip_presence_manager.py b/api/tests/opentrons/hardware_control/backends/test_ot3_tip_presence_manager.py index 543f7b3b400..6ea39738fc2 100644 --- a/api/tests/opentrons/hardware_control/backends/test_ot3_tip_presence_manager.py +++ b/api/tests/opentrons/hardware_control/backends/test_ot3_tip_presence_manager.py @@ -2,7 +2,7 @@ from typing import AsyncIterator, Dict from decoy import Decoy -from opentrons.hardware_control.types import OT3Mount, TipStateType +from opentrons.hardware_control.types import OT3Mount, TipStateType, InstrumentProbeType from opentrons.hardware_control.backends.tip_presence_manager import TipPresenceManager from opentrons_hardware.hardware_control.tip_presence import ( TipDetector, @@ -110,6 +110,51 @@ async def test_get_tip_status_for_high_throughput( result == expected_type +@pytest.mark.parametrize( + "tip_presence,expected_type,sensor_to_look_at", + [ + ( + {SensorId.S0: False, SensorId.S1: False}, + TipStateType.ABSENT, + InstrumentProbeType.PRIMARY, + ), + ( + {SensorId.S0: True, SensorId.S1: True}, + TipStateType.PRESENT, + InstrumentProbeType.SECONDARY, + ), + ( + {SensorId.S0: False, SensorId.S1: True}, + TipStateType.ABSENT, + InstrumentProbeType.PRIMARY, + ), + ( + {SensorId.S0: False, SensorId.S1: True}, + TipStateType.PRESENT, + InstrumentProbeType.SECONDARY, + ), + ], +) +async def test_allow_different_tip_states_ht( + subject: TipPresenceManager, + tip_detector_controller: TipDetectorController, + tip_presence: Dict[SensorId, bool], + expected_type: TipStateType, + sensor_to_look_at: InstrumentProbeType, +) -> None: + mount = OT3Mount.LEFT + await tip_detector_controller.retrieve_tip_status_highthroughput(tip_presence) + + result = await subject.get_tip_status(mount, sensor_to_look_at) + result == expected_type + + # if sensor_to_look_at is not used, different tip states + # should result in an UnmatchedTipStates error + if len(set(tip_presence[t] for t in tip_presence)) > 1: + with pytest.raises(UnmatchedTipPresenceStates): + result = await subject.get_tip_status(mount) + + @pytest.mark.parametrize( "tip_presence", [ From ce97b9165ed23663dab94bb1a75bb3864b33905f Mon Sep 17 00:00:00 2001 From: Jamey Huffnagle Date: Tue, 23 Apr 2024 12:09:41 -0400 Subject: [PATCH 192/194] fix(app): fix infinitely re-rendering/never rendering firmware success toasts (#14981) Closes RQA-2588 There are two issues with rendering firmware update toasts. First, the toasts depend on a request id stored as state within the ModuleCard, but ModuleCards most often unrender during the firmware update process, so this state is lost. This causes the toast never to render. The second issue is that sometimes, given the timing of the polling for attached modules, the module is always attached during the firmware update, thereby causing the module card not to unrender. When this happens, the useEffect hook responsible for making the success toast has conditional logic that is always true after an update, causing the toast to render infinitely. The solution to is to lift the request id state out of the module card itself and then abstract away the storage/retrieval via a utility hook, which is utilized by all parent components of ModuleCard. Also, the shouldRenderToast logic should be calculated only on the initial render. --- .../localization/en/device_details.json | 2 +- .../Devices/InstrumentsAndModules.tsx | 6 ++ .../ProtocolRun/ProtocolRunModuleControls.tsx | 11 ++++ .../ModuleCard/__tests__/ModuleCard.test.tsx | 16 ++--- .../ModuleCard/__tests__/utils.test.ts | 34 +++++++++- app/src/organisms/ModuleCard/index.tsx | 44 ++++++------- app/src/organisms/ModuleCard/utils.ts | 62 +++++++++++++++++++ 7 files changed, 143 insertions(+), 32 deletions(-) diff --git a/app/src/assets/localization/en/device_details.json b/app/src/assets/localization/en/device_details.json index df0d7c743e2..d3fdab0b04c 100644 --- a/app/src/assets/localization/en/device_details.json +++ b/app/src/assets/localization/en/device_details.json @@ -58,7 +58,7 @@ "firmware_update_needed": "Instrument firmware update needed. Start the update on the robot's touchscreen.", "firmware_update_available": "Firmware update available.", "firmware_update_failed": "Failed to update module firmware", - "firmware_update_installation_successful": "Installation successful", + "firmware_updated_successfully": "Firmware updated successfully", "firmware_update_occurring": "Firmware update in progress...", "fixture": "Fixture", "have_not_run_description": "After you run some protocols, they will appear here.", diff --git a/app/src/organisms/Devices/InstrumentsAndModules.tsx b/app/src/organisms/Devices/InstrumentsAndModules.tsx index 07b78af63cb..04068e8e21c 100644 --- a/app/src/organisms/Devices/InstrumentsAndModules.tsx +++ b/app/src/organisms/Devices/InstrumentsAndModules.tsx @@ -33,6 +33,7 @@ import { PipetteCard } from './PipetteCard' import { FlexPipetteCard } from './PipetteCard/FlexPipetteCard' import { GripperCard } from '../GripperCard' import { useIsEstopNotDisengaged } from '../../resources/devices/hooks/useIsEstopNotDisengaged' +import { useModuleApiRequests } from '../ModuleCard/utils' import type { BadGripper, @@ -62,6 +63,7 @@ export function InstrumentsAndModules({ const currentRunId = useCurrentRunId() const { isRunTerminal, isRunRunning } = useRunStatuses() const isEstopNotDisengaged = useIsEstopNotDisengaged(robotName) + const [getLatestRequestId, handleModuleApiRequests] = useModuleApiRequests() const { data: attachedInstruments } = useInstrumentsQuery({ refetchInterval: EQUIPMENT_POLL_MS, @@ -218,6 +220,8 @@ export function InstrumentsAndModules({ attachPipetteRequired={attachPipetteRequired} calibratePipetteRequired={calibratePipetteRequired} updatePipetteFWRequired={updatePipetteFWRequired} + latestRequestId={getLatestRequestId(module.serialNumber)} + handleModuleApiRequests={handleModuleApiRequests} /> ))}
    @@ -267,6 +271,8 @@ export function InstrumentsAndModules({ attachPipetteRequired={attachPipetteRequired} calibratePipetteRequired={calibratePipetteRequired} updatePipetteFWRequired={updatePipetteFWRequired} + latestRequestId={getLatestRequestId(module.serialNumber)} + handleModuleApiRequests={handleModuleApiRequests} /> ))}
    diff --git a/app/src/organisms/Devices/ProtocolRun/ProtocolRunModuleControls.tsx b/app/src/organisms/Devices/ProtocolRun/ProtocolRunModuleControls.tsx index fa9aad2e7d1..4930efee2d3 100644 --- a/app/src/organisms/Devices/ProtocolRun/ProtocolRunModuleControls.tsx +++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunModuleControls.tsx @@ -10,6 +10,8 @@ import { } from '@opentrons/components' import { ModuleCard } from '../../ModuleCard' import { useModuleRenderInfoForProtocolById } from '../hooks' +import { useModuleApiRequests } from '../../ModuleCard/utils' + import type { BadPipette, PipetteData } from '@opentrons/api-client' interface PipetteStatus { @@ -77,6 +79,7 @@ export const ProtocolRunModuleControls = ({ calibratePipetteRequired, updatePipetteFWRequired, } = usePipetteIsReady() + const [getLatestRequestId, handleModuleApiRequests] = useModuleApiRequests() const moduleRenderInfoForProtocolById = useModuleRenderInfoForProtocolById( runId, @@ -120,6 +123,10 @@ export const ProtocolRunModuleControls = ({ attachPipetteRequired={attachPipetteRequired} calibratePipetteRequired={calibratePipetteRequired} updatePipetteFWRequired={updatePipetteFWRequired} + latestRequestId={getLatestRequestId( + module.attachedModuleMatch.serialNumber + )} + handleModuleApiRequests={handleModuleApiRequests} /> ) : null )} @@ -141,6 +148,10 @@ export const ProtocolRunModuleControls = ({ attachPipetteRequired={attachPipetteRequired} calibratePipetteRequired={calibratePipetteRequired} updatePipetteFWRequired={updatePipetteFWRequired} + latestRequestId={getLatestRequestId( + module.attachedModuleMatch.serialNumber + )} + handleModuleApiRequests={handleModuleApiRequests} /> ) : null )} diff --git a/app/src/organisms/ModuleCard/__tests__/ModuleCard.test.tsx b/app/src/organisms/ModuleCard/__tests__/ModuleCard.test.tsx index 74ca18bef61..8c6dbcfd025 100644 --- a/app/src/organisms/ModuleCard/__tests__/ModuleCard.test.tsx +++ b/app/src/organisms/ModuleCard/__tests__/ModuleCard.test.tsx @@ -24,7 +24,6 @@ import { getRequestById, PENDING, SUCCESS, - useDispatchApiRequest, } from '../../../redux/robot-api' import { useCurrentRunStatus } from '../../RunTimeControl/hooks' import { useToaster } from '../../ToasterOven' @@ -43,7 +42,7 @@ import type { MagneticModule, ThermocyclerModule, } from '../../../redux/modules/types' -import type { DispatchApiRequestType } from '../../../redux/robot-api' +import type { Mock } from 'vitest' vi.mock('../ErrorInfo') vi.mock('../MagneticModuleData') @@ -182,6 +181,8 @@ const mockMakeSnackbar = vi.fn() const mockMakeToast = vi.fn() const mockEatToast = vi.fn() +const MOCK_LATEST_REQUEST_ID = '1234' + const render = (props: React.ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, @@ -189,10 +190,12 @@ const render = (props: React.ComponentProps) => { } describe('ModuleCard', () => { - let dispatchApiRequest: DispatchApiRequestType let props: React.ComponentProps + let mockHandleModuleApiRequests: Mock beforeEach(() => { + mockHandleModuleApiRequests = vi.fn() + props = { module: mockMagneticModule, robotName: mockRobot.name, @@ -200,14 +203,11 @@ describe('ModuleCard', () => { attachPipetteRequired: false, calibratePipetteRequired: false, updatePipetteFWRequired: false, + handleModuleApiRequests: mockHandleModuleApiRequests, + latestRequestId: MOCK_LATEST_REQUEST_ID, } - dispatchApiRequest = vi.fn() vi.mocked(ErrorInfo).mockReturnValue(null) - vi.mocked(useDispatchApiRequest).mockReturnValue([ - dispatchApiRequest, - ['id'], - ]) vi.mocked(MagneticModuleData).mockReturnValue(
    Mock Magnetic Module Data
    ) diff --git a/app/src/organisms/ModuleCard/__tests__/utils.test.ts b/app/src/organisms/ModuleCard/__tests__/utils.test.ts index 311c9676da0..5798efeb827 100644 --- a/app/src/organisms/ModuleCard/__tests__/utils.test.ts +++ b/app/src/organisms/ModuleCard/__tests__/utils.test.ts @@ -1,4 +1,5 @@ -import { describe, expect, it } from 'vitest' +import { describe, expect, it, vi, beforeEach } from 'vitest' +import { renderHook, act } from '@testing-library/react' import { mockHeaterShaker, @@ -9,7 +10,10 @@ import { mockThermocycler, mockThermocyclerGen2, } from '../../../redux/modules/__fixtures__' -import { getModuleCardImage } from '../utils' +import { getModuleCardImage, useModuleApiRequests } from '../utils' +import { useDispatchApiRequest } from '../../../redux/robot-api' + +vi.mock('../../../redux/robot-api') const mockThermocyclerGen2ClosedLid = { id: 'thermocycler_id2', @@ -83,3 +87,29 @@ describe('getModuleCardImage', () => { ) }) }) + +const updateModuleAction = { meta: { requestId: '12345' } } +const MOCK_ROBOT_NAME = 'MOCK_ROBOT' +const MOCK_SERIAL_NUMBER = '1234' +const mockDispatchApiRequest = () => updateModuleAction + +describe('useModuleApiRequests', () => { + beforeEach(() => { + vi.mocked(useDispatchApiRequest).mockReturnValue([ + mockDispatchApiRequest, + ] as any) + }) + + it('should dispatch an API request and update requestIdsBySerial on handleModuleApiRequests', () => { + const { result } = renderHook(() => useModuleApiRequests()) + + act(() => { + result.current[1](MOCK_ROBOT_NAME, MOCK_SERIAL_NUMBER) + }) + + expect(result.current[0](MOCK_SERIAL_NUMBER)).toEqual( + updateModuleAction.meta.requestId + ) + expect(result.current[0]('NON_EXISTENT_SERIAL')).toBeNull() + }) +}) diff --git a/app/src/organisms/ModuleCard/index.tsx b/app/src/organisms/ModuleCard/index.tsx index c2c42151eda..52f3ed99f65 100644 --- a/app/src/organisms/ModuleCard/index.tsx +++ b/app/src/organisms/ModuleCard/index.tsx @@ -1,7 +1,6 @@ import * as React from 'react' import { Trans, useTranslation } from 'react-i18next' import { useDispatch, useSelector } from 'react-redux' -import last from 'lodash/last' import { useHistory } from 'react-router-dom' import { @@ -31,9 +30,7 @@ import { import { RUN_STATUS_FINISHING, RUN_STATUS_RUNNING } from '@opentrons/api-client' import { OverflowBtn } from '../../atoms/MenuList/OverflowBtn' -import { updateModule } from '../../redux/modules' import { - useDispatchApiRequest, getRequestById, PENDING, FAILURE, @@ -85,6 +82,8 @@ interface ModuleCardProps { attachPipetteRequired: boolean calibratePipetteRequired: boolean updatePipetteFWRequired: boolean + latestRequestId: string | null + handleModuleApiRequests: (robotName: string, serialNumber: string) => void runId?: string slotName?: string } @@ -100,6 +99,8 @@ export const ModuleCard = (props: ModuleCardProps): JSX.Element | null => { attachPipetteRequired, calibratePipetteRequired, updatePipetteFWRequired, + latestRequestId, + handleModuleApiRequests, } = props const dispatch = useDispatch() const { @@ -115,13 +116,12 @@ export const ModuleCard = (props: ModuleCardProps): JSX.Element | null => { const [hasSecondary, setHasSecondary] = React.useState(false) const [showAboutModule, setShowAboutModule] = React.useState(false) const [showTestShake, setShowTestShake] = React.useState(false) - const [showHSWizard, setShowHSWizard] = React.useState(false) - const [showFWBanner, setShowFWBanner] = React.useState(true) - const [showCalModal, setShowCalModal] = React.useState(false) + const [showHSWizard, setShowHSWizard] = React.useState(false) + const [showFWBanner, setShowFWBanner] = React.useState(true) + const [showCalModal, setShowCalModal] = React.useState(false) const [targetProps, tooltipProps] = useHoverTooltip() const history = useHistory() - const [dispatchApiRequest, requestIds] = useDispatchApiRequest() const runStatus = useCurrentRunStatus({ onSettled: data => { if (data == null) { @@ -138,29 +138,31 @@ export const ModuleCard = (props: ModuleCardProps): JSX.Element | null => { (!attachPipetteRequired ?? false) && (!calibratePipetteRequired ?? false) && (!updatePipetteFWRequired ?? false) - const latestRequestId = last(requestIds) + const latestRequest = useSelector(state => - latestRequestId ? getRequestById(state, latestRequestId) : null + latestRequestId != null ? getRequestById(state, latestRequestId) : null ) - const isEstopNotDisengaged = useIsEstopNotDisengaged(robotName) - const handleCloseErrorModal = (): void => { - if (latestRequestId != null) { - dispatch(dismissRequest(latestRequestId)) - } + const hasUpdated = + !module.hasAvailableUpdate && latestRequest?.status === SUCCESS + const [showFirmwareToast, setShowFirmwareToast] = React.useState(hasUpdated) + const { makeToast } = useToaster() + if (showFirmwareToast) { + makeToast(t('firmware_updated_successfully'), SUCCESS_TOAST) + setShowFirmwareToast(false) } const handleFirmwareUpdateClick = (): void => { - robotName && - dispatchApiRequest(updateModule(robotName, module.serialNumber)) + robotName && handleModuleApiRequests(robotName, module.serialNumber) } - const { makeToast } = useToaster() - React.useEffect(() => { - if (!module.hasAvailableUpdate && latestRequest?.status === SUCCESS) { - makeToast(t('firmware_update_installation_successful'), SUCCESS_TOAST) + const isEstopNotDisengaged = useIsEstopNotDisengaged(robotName) + + const handleCloseErrorModal = (): void => { + if (latestRequestId != null) { + dispatch(dismissRequest(latestRequestId)) } - }, [module.hasAvailableUpdate, latestRequest?.status, makeToast, t]) + } const isPending = latestRequest?.status === PENDING const hotToTouch: IconProps = { name: 'ot-hot-to-touch' } diff --git a/app/src/organisms/ModuleCard/utils.ts b/app/src/organisms/ModuleCard/utils.ts index c80cfa2c4fe..dfd136bfcfc 100644 --- a/app/src/organisms/ModuleCard/utils.ts +++ b/app/src/organisms/ModuleCard/utils.ts @@ -1,3 +1,9 @@ +import * as React from 'react' +import last from 'lodash/last' + +import { useDispatchApiRequest } from '../../redux/robot-api' +import { updateModule } from '../../redux/modules' + import magneticModule from '../../assets/images/magnetic_module_gen_2_transparent.png' import temperatureModule from '../../assets/images/temp_deck_gen_2_transparent.png' import thermoModuleGen1Closed from '../../assets/images/thermocycler_closed.png' @@ -5,6 +11,7 @@ import thermoModuleGen1Opened from '../../assets/images/thermocycler_open_transp import heaterShakerModule from '../../assets/images/heater_shaker_module_transparent.png' import thermoModuleGen2Closed from '../../assets/images/thermocycler_gen_2_closed.png' import thermoModuleGen2Opened from '../../assets/images/thermocycler_gen_2_opened.png' + import type { AttachedModule } from '../../redux/modules/types' export function getModuleCardImage(attachedModule: AttachedModule): string { @@ -35,3 +42,58 @@ export function getModuleCardImage(attachedModule: AttachedModule): string { return 'unknown module model, this is an error' } } + +type RequestIdsBySerialNumber = Record +type HandleModuleApiRequestsType = (robotName: string, moduleId: string) => void +type GetLatestRequestIdType = (moduleId: string) => string | null + +export function useModuleApiRequests(): [ + GetLatestRequestIdType, + HandleModuleApiRequestsType +] { + const [dispatchApiRequest] = useDispatchApiRequest() + const [ + requestIdsBySerial, + setRequestIdsBySerial, + ] = React.useState({}) + + const handleModuleApiRequests = ( + robotName: string, + serialNumber: string + ): void => { + const action = dispatchApiRequest(updateModule(robotName, serialNumber)) + const { requestId } = action.meta + + if (requestId != null) { + if (serialNumber in requestIdsBySerial) { + setRequestIdsBySerial((prevState: RequestIdsBySerialNumber) => { + const existingRequestIds = prevState[serialNumber] || [] + return { + ...prevState, + [serialNumber]: [...existingRequestIds, requestId], + } + }) + } else { + setRequestIdsBySerial(prevState => { + return { + ...prevState, + [serialNumber]: [requestId], + } + }) + } + } + } + + const getLatestRequestId = React.useCallback( + (serialNumber: string): string | null => { + if (serialNumber in requestIdsBySerial) { + return last(requestIdsBySerial[serialNumber]) ?? null + } else { + return null + } + }, + [requestIdsBySerial] + ) + + return [getLatestRequestId, handleModuleApiRequests] +} From daa51ddff4d067b9db5c6e0f8c227abc14b67d64 Mon Sep 17 00:00:00 2001 From: Max Marrone Date: Tue, 23 Apr 2024 13:05:11 -0400 Subject: [PATCH 193/194] fix(api): Filter out `air_gap()` calls as higher-order commands (#14985) --- .../protocol_runner/legacy_command_mapper.py | 1 + .../smoke_tests/test_legacy_command_mapper.py | 44 +++++++++++++++++++ .../test_legacy_command_mapper.py | 1 + 3 files changed, 46 insertions(+) diff --git a/api/src/opentrons/protocol_runner/legacy_command_mapper.py b/api/src/opentrons/protocol_runner/legacy_command_mapper.py index e835a6af8e6..9243f50f70d 100644 --- a/api/src/opentrons/protocol_runner/legacy_command_mapper.py +++ b/api/src/opentrons/protocol_runner/legacy_command_mapper.py @@ -79,6 +79,7 @@ def __init__(self, wrapping_exc: BaseException) -> None: legacy_command_types.DISTRIBUTE, legacy_command_types.TRANSFER, legacy_command_types.RETURN_TIP, + legacy_command_types.AIR_GAP, } diff --git a/api/tests/opentrons/protocol_runner/smoke_tests/test_legacy_command_mapper.py b/api/tests/opentrons/protocol_runner/smoke_tests/test_legacy_command_mapper.py index 5d6595227b9..c8950cbe090 100644 --- a/api/tests/opentrons/protocol_runner/smoke_tests/test_legacy_command_mapper.py +++ b/api/tests/opentrons/protocol_runner/smoke_tests/test_legacy_command_mapper.py @@ -5,6 +5,7 @@ """ from datetime import datetime from pathlib import Path +from textwrap import dedent from typing import List import pytest @@ -753,3 +754,46 @@ async def test_zero_volume_dispense_commands( labwareId=load_well_plate.result.labwareId, wellName="D7", ) + + +async def test_air_gap(tmp_path: Path) -> None: + """An `air_gap()` should be mapped to an `aspirate`. + + This covers RQA-2621. + """ + path = tmp_path / "protocol.py" + path.write_text( + dedent( + """\ + metadata = {"apiLevel": "2.13"} + def run(protocol): + # Prep: + tip_rack = protocol.load_labware("opentrons_96_tiprack_300ul", 1) + well_plate = protocol.load_labware("biorad_96_wellplate_200ul_pcr", 2) + pipette = protocol.load_instrument("p300_single_gen2", mount="left", tip_racks=[tip_rack]) + pipette.pick_up_tip() + + # Test: + pipette.move_to(well_plate["A1"].top()) + pipette.air_gap(100) + """ + ) + ) + result_commands = await simulate_and_get_commands(path) + [ + initial_home, + load_tip_rack, + load_well_plate, + load_pipette, + pick_up_tip, + move_to_well, + air_gap_aspirate, + ] = result_commands + assert isinstance(initial_home, commands.Home) + assert isinstance(load_tip_rack, commands.LoadLabware) + assert isinstance(load_well_plate, commands.LoadLabware) + assert isinstance(load_pipette, commands.LoadPipette) + assert isinstance(pick_up_tip, commands.PickUpTip) + # TODO(mm, 2024-04-23): This commands.Custom looks wrong. This should be a commands.MoveToWell. + assert isinstance(move_to_well, commands.Custom) + assert isinstance(air_gap_aspirate, commands.Aspirate) diff --git a/api/tests/opentrons/protocol_runner/test_legacy_command_mapper.py b/api/tests/opentrons/protocol_runner/test_legacy_command_mapper.py index f0412878856..a0581001a82 100644 --- a/api/tests/opentrons/protocol_runner/test_legacy_command_mapper.py +++ b/api/tests/opentrons/protocol_runner/test_legacy_command_mapper.py @@ -579,6 +579,7 @@ def test_map_pause() -> None: "command.DISTRIBUTE", "command.TRANSFER", "command.RETURN_TIP", + "command.AIR_GAP", ], ) def test_filter_higher_order_commands(command_type: str) -> None: From 26929a2d92715ab23d7a4832f2548a2bab565d11 Mon Sep 17 00:00:00 2001 From: Nick Diehl <47604184+ncdiehl11@users.noreply.github.com> Date: Tue, 23 Apr 2024 13:19:10 -0400 Subject: [PATCH 194/194] fix(app): clone run with RTPs from HistoricalProtocolRun (#14959) closes RQA-2601 --- api-client/src/runs/constants.ts | 11 ++++ api-client/src/runs/index.ts | 2 +- api-client/src/runs/types.ts | 3 +- .../localization/en/protocol_setup.json | 2 + .../Devices/ProtocolRun/ProtocolRunHeader.tsx | 12 ++--- .../ProtocolRunRunTimeParameters.tsx | 51 +++++++++++++++---- .../ProtocolRunRuntimeParameters.test.tsx | 17 +++++-- .../organisms/Devices/hooks/useRunStatuses.ts | 12 ++--- .../InterventionModal/__fixtures__/index.ts | 1 + .../RecentRunProtocolCarousel.test.tsx | 1 + .../hooks/__tests__/useCloneRun.test.tsx | 13 +++-- .../ProtocolUpload/hooks/useCloneRun.ts | 48 ++++++++++++----- app/src/organisms/RunPreview/index.tsx | 26 ++++++++-- .../RunTimeControl/__fixtures__/index.ts | 9 ++++ .../RunTimeControl/__tests__/hooks.test.tsx | 2 +- app/src/organisms/RunTimeControl/hooks.ts | 10 ++-- .../molecules/ParametersTable/InfoScreen.tsx | 30 ++++++++--- .../src/runs/__fixtures__/runs.ts | 2 + .../src/runs/useAllCommandsQuery.ts | 14 +++-- 19 files changed, 201 insertions(+), 65 deletions(-) create mode 100644 api-client/src/runs/constants.ts diff --git a/api-client/src/runs/constants.ts b/api-client/src/runs/constants.ts new file mode 100644 index 00000000000..9f0d8293ef6 --- /dev/null +++ b/api-client/src/runs/constants.ts @@ -0,0 +1,11 @@ +import { + RUN_STATUS_FAILED, + RUN_STATUS_STOPPED, + RUN_STATUS_SUCCEEDED, +} from './types' + +export const RUN_STATUSES_TERMINAL = [ + RUN_STATUS_SUCCEEDED, + RUN_STATUS_FAILED, + RUN_STATUS_STOPPED, +] diff --git a/api-client/src/runs/index.ts b/api-client/src/runs/index.ts index fa38dade02f..1d62755d4c5 100644 --- a/api-client/src/runs/index.ts +++ b/api-client/src/runs/index.ts @@ -10,6 +10,6 @@ export { getCommands } from './commands/getCommands' export { createRunAction } from './createRunAction' export * from './createLabwareOffset' export * from './createLabwareDefinition' - +export * from './constants' export * from './types' export type { CreateRunData } from './createRun' diff --git a/api-client/src/runs/types.ts b/api-client/src/runs/types.ts index 7e6ec2b0ee7..36c5f9a3a20 100644 --- a/api-client/src/runs/types.ts +++ b/api-client/src/runs/types.ts @@ -4,6 +4,7 @@ import type { LoadedPipette, ModuleModel, RunTimeCommand, + RunTimeParameter, } from '@opentrons/shared-data' import type { ResourceLink, ErrorDetails } from '../types' export * from './commands/types' @@ -47,7 +48,7 @@ export interface LegacyGoodRunData { modules: LoadedModule[] protocolId?: string labwareOffsets?: LabwareOffset[] - runTimeParameterValues?: RunTimeParameterCreateData + runTimeParameters: RunTimeParameter[] } export interface KnownGoodRunData extends LegacyGoodRunData { diff --git a/app/src/assets/localization/en/protocol_setup.json b/app/src/assets/localization/en/protocol_setup.json index 3bb871d4e64..74fbf93d3c2 100644 --- a/app/src/assets/localization/en/protocol_setup.json +++ b/app/src/assets/localization/en/protocol_setup.json @@ -48,6 +48,7 @@ "confirm_values": "Confirm values", "connect_all_hardware": "Connect and calibrate all hardware first", "connect_all_mod": "Connect all modules first", + "connect_modules_for_controls": "Connect modules to see controls", "connection_info_not_available": "Connection info not available once run has started", "connection_status": "Connection Status", "currently_configured": "Currently configured", @@ -236,6 +237,7 @@ "run_disabled_modules_and_calibration_not_complete": "Make sure robot calibration is complete and all modules are connected before proceeding to run", "run_disabled_modules_not_connected": "Make sure all modules are connected before proceeding to run", "run_labware_position_check": "run labware position check", + "run_never_started": "Run was never started", "run": "Run", "secure_labware_instructions": "Secure labware instructions", "secure_labware_modal": "Securing labware to the {{name}}", diff --git a/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader.tsx b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader.tsx index 0bfa08ce47b..53cdf10f46c 100644 --- a/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader.tsx +++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader.tsx @@ -15,6 +15,7 @@ import { RUN_STATUS_SUCCEEDED, RUN_STATUS_BLOCKED_BY_OPEN_DOOR, RUN_STATUS_AWAITING_RECOVERY, + RUN_STATUSES_TERMINAL, } from '@opentrons/api-client' import { useModulesQuery, @@ -128,11 +129,6 @@ const CANCELLABLE_STATUSES = [ RUN_STATUS_IDLE, RUN_STATUS_AWAITING_RECOVERY, ] -const RUN_OVER_STATUSES: RunStatus[] = [ - RUN_STATUS_FAILED, - RUN_STATUS_STOPPED, - RUN_STATUS_SUCCEEDED, -] interface ProtocolRunHeaderProps { protocolRunHeaderRef: React.RefObject | null @@ -214,7 +210,11 @@ export function ProtocolRunHeader({ if (runStatus === RUN_STATUS_IDLE) { setShowDropTipBanner(true) setPipettesWithTip([]) - } else if (runStatus != null && RUN_OVER_STATUSES.includes(runStatus)) { + } else if ( + runStatus != null && + // @ts-expect-error runStatus expected to possibly not be terminal + RUN_STATUSES_TERMINAL.includes(runStatus) + ) { getPipettesWithTipAttached({ host, runId, diff --git a/app/src/organisms/Devices/ProtocolRun/ProtocolRunRunTimeParameters.tsx b/app/src/organisms/Devices/ProtocolRun/ProtocolRunRunTimeParameters.tsx index ea7ec478415..b7a253fdeca 100644 --- a/app/src/organisms/Devices/ProtocolRun/ProtocolRunRunTimeParameters.tsx +++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunRunTimeParameters.tsx @@ -1,6 +1,11 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' import styled, { css } from 'styled-components' +import { + RUN_ACTION_TYPE_PLAY, + RUN_STATUS_STOPPED, + RUN_STATUSES_TERMINAL, +} from '@opentrons/api-client' import { formatRunTimeParameterValue } from '@opentrons/shared-data' import { ALIGN_CENTER, @@ -23,8 +28,11 @@ import { Banner } from '../../../atoms/Banner' import { Divider } from '../../../atoms/structure' import { Tooltip } from '../../../atoms/Tooltip' import { useMostRecentCompletedAnalysis } from '../../LabwarePositionCheck/useMostRecentCompletedAnalysis' +import { useRunStatus } from '../../RunTimeControl/hooks' +import { useNotifyRunQuery } from '../../../resources/runs' import type { RunTimeParameter } from '@opentrons/shared-data' +import type { RunStatus } from '@opentrons/api-client' interface ProtocolRunRuntimeParametersProps { runId: string @@ -34,13 +42,31 @@ export function ProtocolRunRuntimeParameters({ }: ProtocolRunRuntimeParametersProps): JSX.Element { const { t } = useTranslation('protocol_setup') const mostRecentAnalysis = useMostRecentCompletedAnalysis(runId) - const runTimeParameters = mostRecentAnalysis?.runTimeParameters ?? [] - const hasParameter = runTimeParameters.length > 0 - - const hasCustomValues = runTimeParameters.some( + const runStatus = useRunStatus(runId) + const isRunTerminal = + runStatus == null + ? false + : (RUN_STATUSES_TERMINAL as RunStatus[]).includes(runStatus) + // we access runTimeParameters from the run record rather than the most recent analysis + // because the most recent analysis may not reflect the selected run (e.g. cloning a run + // from a historical protocol run from the device details page) + const run = useNotifyRunQuery(runId).data + const runTimeParameters = + (isRunTerminal + ? run?.data?.runTimeParameters + : mostRecentAnalysis?.runTimeParameters) ?? [] + const hasRunTimeParameters = runTimeParameters.length > 0 + const hasCustomRunTimeParameterValues = runTimeParameters.some( parameter => parameter.value !== parameter.default ) + const runActions = run?.data.actions + const hasRunStarted = runActions?.some( + action => action.actionType === RUN_ACTION_TYPE_PLAY + ) + const isRunCancelledWithoutStarting = + !hasRunStarted && runStatus === RUN_STATUS_STOPPED + return ( <> {t('parameters')} - {hasParameter ? ( + {hasRunTimeParameters ? ( - {hasCustomValues ? t('custom_values') : t('default_values')} + {hasCustomRunTimeParameterValues + ? t('custom_values') + : t('default_values')} ) : null} - {hasParameter ? ( + {hasRunTimeParameters ? ( ) : null}
    - {!hasParameter ? ( + {!hasRunTimeParameters ? ( - + ) : ( <> diff --git a/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunRuntimeParameters.test.tsx b/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunRuntimeParameters.test.tsx index f683986c26b..4be025a491e 100644 --- a/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunRuntimeParameters.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunRuntimeParameters.test.tsx @@ -1,12 +1,17 @@ import * as React from 'react' +import { UseQueryResult } from 'react-query' import { describe, it, vi, beforeEach, afterEach, expect } from 'vitest' import { screen } from '@testing-library/react' import { when } from 'vitest-when' +import { Run } from '@opentrons/api-client' import { InfoScreen } from '@opentrons/components' import { renderWithProviders } from '../../../../__testing-utils__' import { i18n } from '../../../../i18n' import { useMostRecentCompletedAnalysis } from '../../../LabwarePositionCheck/useMostRecentCompletedAnalysis' +import { useRunStatus } from '../../../RunTimeControl/hooks' +import { useNotifyRunQuery } from '../../../../resources/runs' +import { mockSucceededRun } from '../../../RunTimeControl/__fixtures__' import { ProtocolRunRuntimeParameters } from '../ProtocolRunRunTimeParameters' @@ -23,6 +28,8 @@ vi.mock('@opentrons/components', async importOriginal => { } }) vi.mock('../../../LabwarePositionCheck/useMostRecentCompletedAnalysis') +vi.mock('../../../RunTimeControl/hooks') +vi.mock('../../../../resources/runs') const RUN_ID = 'mockId' @@ -100,13 +107,17 @@ describe('ProtocolRunRuntimeParameters', () => { .thenReturn({ runTimeParameters: mockRunTimeParameterData, } as CompletedProtocolAnalysis) + vi.mocked(useRunStatus).mockReturnValue('running') + vi.mocked(useNotifyRunQuery).mockReturnValue(({ + data: { data: mockSucceededRun }, + } as unknown) as UseQueryResult) }) afterEach(() => { vi.resetAllMocks() }) - it('should render title, and banner when RunTimeParameters are note empty and all values are default', () => { + it('should render title, and banner when RunTimeParameters are not empty and all values are default', () => { render(props) screen.getByText('Parameters') screen.getByText('Default values') @@ -116,7 +127,7 @@ describe('ProtocolRunRuntimeParameters', () => { screen.getByText('Value') }) - it('should render title, and banner when RunTimeParameters are note empty and some value is changed', () => { + it('should render title, and banner when RunTimeParameters are not empty and some value is changed', () => { vi.mocked(useMostRecentCompletedAnalysis).mockReturnValue({ runTimeParameters: [ ...mockRunTimeParameterData, @@ -139,7 +150,7 @@ describe('ProtocolRunRuntimeParameters', () => { screen.getByText('Value') }) - it('should render RunTimeParameters when RunTimeParameters are note empty', () => { + it('should render RunTimeParameters when RunTimeParameters are not empty', () => { render(props) screen.getByText('Dry Run') screen.getByText('Off') diff --git a/app/src/organisms/Devices/hooks/useRunStatuses.ts b/app/src/organisms/Devices/hooks/useRunStatuses.ts index bba83f76299..887de586f8e 100644 --- a/app/src/organisms/Devices/hooks/useRunStatuses.ts +++ b/app/src/organisms/Devices/hooks/useRunStatuses.ts @@ -1,15 +1,15 @@ import { + RUN_STATUSES_TERMINAL, RUN_STATUS_AWAITING_RECOVERY, - RUN_STATUS_FAILED, RUN_STATUS_IDLE, RUN_STATUS_PAUSED, RUN_STATUS_RUNNING, - RUN_STATUS_STOPPED, - RUN_STATUS_SUCCEEDED, } from '@opentrons/api-client' import { useCurrentRunId } from '../../ProtocolUpload/hooks' import { useRunStatus } from '../../RunTimeControl/hooks' +import type { RunStatus } from '@opentrons/api-client' + interface RunStatusesInfo { isRunStill: boolean isRunTerminal: boolean @@ -29,9 +29,9 @@ export function useRunStatuses(): RunStatusesInfo { runStatus === RUN_STATUS_RUNNING || runStatus === RUN_STATUS_AWAITING_RECOVERY const isRunTerminal = - runStatus === RUN_STATUS_SUCCEEDED || - runStatus === RUN_STATUS_STOPPED || - runStatus === RUN_STATUS_FAILED + runStatus != null + ? (RUN_STATUSES_TERMINAL as RunStatus[]).includes(runStatus) + : false const isRunStill = isRunTerminal || isRunIdle return { isRunStill, isRunTerminal, isRunIdle, isRunRunning } diff --git a/app/src/organisms/InterventionModal/__fixtures__/index.ts b/app/src/organisms/InterventionModal/__fixtures__/index.ts index b6d631f4c97..2611fe19b03 100644 --- a/app/src/organisms/InterventionModal/__fixtures__/index.ts +++ b/app/src/organisms/InterventionModal/__fixtures__/index.ts @@ -188,6 +188,7 @@ export const mockRunData: RunData = { pipettes: [], labware: [mockLabwareOnModule, mockLabwareOnSlot, mockLabwareOffDeck], modules: [mockModule], + runTimeParameters: [], } export const mockLabwareRenderInfo = [ diff --git a/app/src/organisms/OnDeviceDisplay/RobotDashboard/__tests__/RecentRunProtocolCarousel.test.tsx b/app/src/organisms/OnDeviceDisplay/RobotDashboard/__tests__/RecentRunProtocolCarousel.test.tsx index 85e956ed977..8bc3a481843 100644 --- a/app/src/organisms/OnDeviceDisplay/RobotDashboard/__tests__/RecentRunProtocolCarousel.test.tsx +++ b/app/src/organisms/OnDeviceDisplay/RobotDashboard/__tests__/RecentRunProtocolCarousel.test.tsx @@ -26,6 +26,7 @@ const mockRun = { pipettes: [], protocolId: 'mockSortedProtocolID', status: 'stopped', + runTimeParameters: [], } const render = ( diff --git a/app/src/organisms/ProtocolUpload/hooks/__tests__/useCloneRun.test.tsx b/app/src/organisms/ProtocolUpload/hooks/__tests__/useCloneRun.test.tsx index af388d30930..40726be91bf 100644 --- a/app/src/organisms/ProtocolUpload/hooks/__tests__/useCloneRun.test.tsx +++ b/app/src/organisms/ProtocolUpload/hooks/__tests__/useCloneRun.test.tsx @@ -4,7 +4,11 @@ import { renderHook } from '@testing-library/react' import { QueryClient, QueryClientProvider } from 'react-query' import { describe, it, beforeEach, afterEach, vi, expect } from 'vitest' -import { useHost, useCreateRunMutation } from '@opentrons/react-api-client' +import { + useHost, + useCreateRunMutation, + useCreateProtocolAnalysisMutation, +} from '@opentrons/react-api-client' import { useCloneRun } from '../useCloneRun' import { useNotifyRunQuery } from '../../../../resources/runs' @@ -30,13 +34,16 @@ describe('useCloneRun hook', () => { id: RUN_ID, protocolId: 'protocolId', labwareOffsets: 'someOffset', - runTimeParameterValues: 'someRtp', + runTimeParameters: [], }, }, } as any) when(vi.mocked(useCreateRunMutation)) .calledWith(expect.anything()) .thenReturn({ createRun: vi.fn() } as any) + vi.mocked(useCreateProtocolAnalysisMutation).mockReturnValue({ + createProtocolAnalysis: vi.fn(), + } as any) const queryClient = new QueryClient() const clientProvider: React.FunctionComponent<{ @@ -61,7 +68,7 @@ describe('useCloneRun hook', () => { expect(mockCreateRun).toHaveBeenCalledWith({ protocolId: 'protocolId', labwareOffsets: 'someOffset', - runTimeParameterValues: 'someRtp', + runTimeParameterValues: {}, }) }) }) diff --git a/app/src/organisms/ProtocolUpload/hooks/useCloneRun.ts b/app/src/organisms/ProtocolUpload/hooks/useCloneRun.ts index 0858544d93c..fe6e3ab3649 100644 --- a/app/src/organisms/ProtocolUpload/hooks/useCloneRun.ts +++ b/app/src/organisms/ProtocolUpload/hooks/useCloneRun.ts @@ -1,10 +1,13 @@ import { useQueryClient } from 'react-query' -import { useHost, useCreateRunMutation } from '@opentrons/react-api-client' - +import { + useHost, + useCreateRunMutation, + useCreateProtocolAnalysisMutation, +} from '@opentrons/react-api-client' import { useNotifyRunQuery } from '../../../resources/runs' -import type { Run } from '@opentrons/api-client' +import type { Run, RunTimeParameterCreateData } from '@opentrons/api-client' interface UseCloneRunResult { cloneRun: () => void @@ -13,28 +16,45 @@ interface UseCloneRunResult { export function useCloneRun( runId: string | null, - onSuccessCallback?: (createRunResponse: Run) => unknown + onSuccessCallback?: (createRunResponse: Run) => unknown, + triggerAnalysis: boolean = false ): UseCloneRunResult { const host = useHost() const queryClient = useQueryClient() const { data: runRecord } = useNotifyRunQuery(runId) + const protocolKey = runRecord?.data.protocolId ?? null + const { createRun, isLoading } = useCreateRunMutation({ onSuccess: response => { - queryClient - .invalidateQueries([host, 'runs']) - .catch((e: Error) => - console.error(`error invalidating runs query: ${e.message}`) - ) + const invalidateRuns = queryClient.invalidateQueries([host, 'runs']) + const invalidateProtocols = queryClient.invalidateQueries([ + host, + 'protocols', + protocolKey, + ]) + Promise.all([invalidateRuns, invalidateProtocols]).catch((e: Error) => + console.error(`error invalidating runs query: ${e.message}`) + ) if (onSuccessCallback != null) onSuccessCallback(response) }, }) + const { createProtocolAnalysis } = useCreateProtocolAnalysisMutation( + protocolKey, + host + ) const cloneRun = (): void => { if (runRecord != null) { - const { - protocolId, - labwareOffsets, - runTimeParameterValues, - } = runRecord.data + const { protocolId, labwareOffsets, runTimeParameters } = runRecord.data + const runTimeParameterValues = runTimeParameters.reduce( + (acc, param) => + param.value !== param.default + ? { ...acc, [param.variableName]: param.value } + : acc, + {} + ) + if (triggerAnalysis && protocolKey != null) { + createProtocolAnalysis({ protocolKey, runTimeParameterValues }) + } createRun({ protocolId, labwareOffsets, runTimeParameterValues }) } else { console.info('failed to clone run record, source run record not found') diff --git a/app/src/organisms/RunPreview/index.tsx b/app/src/organisms/RunPreview/index.tsx index a75257c1952..a7e4aa2591b 100644 --- a/app/src/organisms/RunPreview/index.tsx +++ b/app/src/organisms/RunPreview/index.tsx @@ -3,6 +3,8 @@ import { css } from 'styled-components' import { useTranslation } from 'react-i18next' import { ViewportList, ViewportListRef } from 'react-viewport-list' +import { RUN_STATUSES_TERMINAL } from '@opentrons/api-client' +import { useAllCommandsQuery } from '@opentrons/react-api-client' import { ALIGN_CENTER, BORDERS, @@ -24,6 +26,9 @@ import { CommandText } from '../CommandText' import { Divider } from '../../atoms/structure' import { NAV_BAR_WIDTH } from '../../App/constants' import { CommandIcon } from './CommandIcon' +import { useRunStatus } from '../RunTimeControl/hooks' + +import type { RunStatus } from '@opentrons/api-client' import type { RobotType } from '@opentrons/shared-data' const COLOR_FADE_MS = 500 @@ -41,6 +46,17 @@ export const RunPreviewComponent = ( ): JSX.Element | null => { const { t } = useTranslation('run_details') const robotSideAnalysis = useMostRecentCompletedAnalysis(runId) + const runStatus = useRunStatus(runId) + const isRunTerminal = + runStatus != null + ? (RUN_STATUSES_TERMINAL as RunStatus[]).includes(runStatus) + : false + // we only ever want one request done for terminal runs because this is a heavy request + const commandsFromQuery = useAllCommandsQuery(runId, null, { + staleTime: Infinity, + cacheTime: Infinity, + enabled: isRunTerminal, + }).data?.data const viewPortRef = React.useRef(null) const currentRunCommandKey = useNotifyLastRunCommandKey(runId, { refetchInterval: LIVE_RUN_COMMANDS_POLL_MS, @@ -50,7 +66,9 @@ export const RunPreviewComponent = ( setIsCurrentCommandVisible, ] = React.useState(true) if (robotSideAnalysis == null) return null - const currentRunCommandIndex = robotSideAnalysis.commands.findIndex( + const commands = + (isRunTerminal ? commandsFromQuery : robotSideAnalysis.commands) ?? [] + const currentRunCommandIndex = commands.findIndex( c => c.key === currentRunCommandKey ) @@ -69,7 +87,7 @@ export const RunPreviewComponent = ( {t('run_preview')} - {t('steps_total', { count: robotSideAnalysis.commands.length })} + {t('steps_total', { count: commands.length })} @@ -79,7 +97,7 @@ export const RunPreviewComponent = ( ) : null} - {currentRunCommandIndex === robotSideAnalysis.commands.length - 1 ? ( + {currentRunCommandIndex === commands.length - 1 ? ( {t('end_of_protocol')} diff --git a/app/src/organisms/RunTimeControl/__fixtures__/index.ts b/app/src/organisms/RunTimeControl/__fixtures__/index.ts index 1a18a9a6bcf..33f2e0c4393 100644 --- a/app/src/organisms/RunTimeControl/__fixtures__/index.ts +++ b/app/src/organisms/RunTimeControl/__fixtures__/index.ts @@ -41,6 +41,7 @@ export const mockPausedRun: RunData = { pipettes: [], labware: [], modules: [], + runTimeParameters: [], } export const mockPauseRequestedRun: RunData = { @@ -65,6 +66,7 @@ export const mockPauseRequestedRun: RunData = { pipettes: [], labware: [], modules: [], + runTimeParameters: [], } export const mockRunningRun: RunData = { @@ -94,6 +96,7 @@ export const mockRunningRun: RunData = { pipettes: [], labware: [], modules: [], + runTimeParameters: [], } export const mockFailedRun: RunData = { @@ -133,6 +136,7 @@ export const mockFailedRun: RunData = { pipettes: [], labware: [], modules: [], + runTimeParameters: [], } export const mockStopRequestedRun: RunData = { @@ -167,6 +171,7 @@ export const mockStopRequestedRun: RunData = { pipettes: [], labware: [], modules: [], + runTimeParameters: [], } export const mockStoppedRun: RunData = { @@ -201,6 +206,7 @@ export const mockStoppedRun: RunData = { pipettes: [], labware: [], modules: [], + runTimeParameters: [], } export const mockSucceededRun: RunData = { @@ -230,6 +236,7 @@ export const mockSucceededRun: RunData = { pipettes: [], labware: [], modules: [], + runTimeParameters: [], } export const mockIdleUnstartedRun: RunData = { @@ -243,6 +250,7 @@ export const mockIdleUnstartedRun: RunData = { pipettes: [], labware: [], modules: [], + runTimeParameters: [], } export const mockIdleStartedRun: RunData = { @@ -272,6 +280,7 @@ export const mockIdleStartedRun: RunData = { pipettes: [], labware: [], modules: [], + runTimeParameters: [], } export const mockCommand = { diff --git a/app/src/organisms/RunTimeControl/__tests__/hooks.test.tsx b/app/src/organisms/RunTimeControl/__tests__/hooks.test.tsx index 21adedbd165..a46bc37d865 100644 --- a/app/src/organisms/RunTimeControl/__tests__/hooks.test.tsx +++ b/app/src/organisms/RunTimeControl/__tests__/hooks.test.tsx @@ -61,7 +61,7 @@ describe('useRunControls hook', () => { isStopRunActionLoading: false, }) when(useCloneRun) - .calledWith(mockPausedRun.id, undefined) + .calledWith(mockPausedRun.id, undefined, true) .thenReturn({ cloneRun: mockCloneRun, isLoading: false }) const { result } = renderHook(() => useRunControls(mockPausedRun.id)) diff --git a/app/src/organisms/RunTimeControl/hooks.ts b/app/src/organisms/RunTimeControl/hooks.ts index db042a2ce65..d513fcbe118 100644 --- a/app/src/organisms/RunTimeControl/hooks.ts +++ b/app/src/organisms/RunTimeControl/hooks.ts @@ -12,6 +12,7 @@ import { RUN_STATUS_SUCCEEDED, RUN_ACTION_TYPE_STOP, RUN_STATUS_STOP_REQUESTED, + RUN_STATUSES_TERMINAL, } from '@opentrons/api-client' import { useRunActionMutations } from '@opentrons/react-api-client' @@ -52,7 +53,8 @@ export function useRunControls( const { cloneRun, isLoading: isResetRunLoading } = useCloneRun( runId ?? null, - onCloneRunSuccess + onCloneRunSuccess, + true ) return { @@ -78,11 +80,7 @@ export function useRunStatus( refetchInterval: DEFAULT_STATUS_REFETCH_INTERVAL, enabled: lastRunStatus.current == null || - !([ - RUN_STATUS_FAILED, - RUN_STATUS_SUCCEEDED, - RUN_STATUS_STOPPED, - ] as RunStatus[]).includes(lastRunStatus.current), + !(RUN_STATUSES_TERMINAL as RunStatus[]).includes(lastRunStatus.current), onSuccess: data => (lastRunStatus.current = data?.data?.status ?? null), ...options, }) diff --git a/components/src/molecules/ParametersTable/InfoScreen.tsx b/components/src/molecules/ParametersTable/InfoScreen.tsx index b9798f828e3..cd6db0d622b 100644 --- a/components/src/molecules/ParametersTable/InfoScreen.tsx +++ b/components/src/molecules/ParametersTable/InfoScreen.tsx @@ -8,14 +8,32 @@ import { Flex } from '../../primitives' import { ALIGN_CENTER, DIRECTION_COLUMN } from '../../styles' interface InfoScreenProps { - contentType: 'parameters' | 'moduleControls' + contentType: 'parameters' | 'moduleControls' | 'runNotStarted' + t?: any } -export function InfoScreen({ contentType }: InfoScreenProps): JSX.Element { - const bodyText = - contentType === 'parameters' - ? 'No parameters specified in this protocol' - : 'Connect modules to see controls' +export function InfoScreen({ contentType, t }: InfoScreenProps): JSX.Element { + let bodyText: string = '' + switch (contentType) { + case 'parameters': + bodyText = + t != null + ? t('no_parameters_specified_in_protocol') + : 'No parameters specified in this protocol' + break + case 'moduleControls': + bodyText = + t != null + ? t('connect_modules_for_controls') + : 'Connect modules to see controls' + break + case 'runNotStarted': + bodyText = t != null ? t('run_never_started') : 'Run was never started' + break + default: + bodyText = contentType + } + return ( ( runId: string | null, - params: GetCommandsParams = DEFAULT_PARAMS, + params?: GetCommandsParams | null, options: UseQueryOptions = {} ): UseQueryResult { const host = useHost() + const nullCheckedParams = params ?? DEFAULT_PARAMS + const allOptions: UseQueryOptions = { ...options, enabled: host !== null && runId != null && options.enabled !== false, } - const { cursor, pageLength } = params + const { cursor, pageLength } = nullCheckedParams const query = useQuery( [host, 'runs', runId, 'commands', cursor, pageLength], () => { - return getCommands(host as HostConfig, runId as string, params).then( - response => response.data - ) + return getCommands( + host as HostConfig, + runId as string, + nullCheckedParams + ).then(response => response.data) }, allOptions )